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 index b89aa5dff7dfb..c6d65d6eb0032 100644 --- a/components/pixelbin/package.json +++ b/components/pixelbin/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pixelbin", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Pixelbin Components", "main": "pixelbin.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.0" } -} \ No newline at end of file +} diff --git a/components/pixelbin/pixelbin.app.mjs b/components/pixelbin/pixelbin.app.mjs index 11a79187b9bdc..bc15f6ee72e30 100644 --- a/components/pixelbin/pixelbin.app.mjs +++ b/components/pixelbin/pixelbin.app.mjs @@ -1,11 +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: {}, + 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: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + 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/pnpm-lock.yaml b/pnpm-lock.yaml index 767ac384eed5e..5564f82ac4fb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7267,7 +7267,12 @@ importers: '@pipedream/platform': 1.5.1 components/pixelbin: - specifiers: {} + specifiers: + '@pipedream/platform': 3.0.3 + form-data: ^4.0.0 + dependencies: + '@pipedream/platform': 3.0.3 + form-data: 4.0.0 components/pixiebrix: specifiers: