diff --git a/CHANGELOG.md b/CHANGELOG.md index 624b107..e7aaad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.1.0 (June 11, 2025) + +* Added `Delete Object By ID` Action +* Added `Lookup Object By ID` Action + # 1.0.0 (June 03, 2025) * Initial component release diff --git a/README.md b/README.md index 8d687b0..ae338b7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ * [Description](#description) * [Credentials](#credentials) * [Actions](#actions) + * [Delete Object By ID](#delete-object-by-id) + * [Lookup Object By ID](#lookup-object-by-id) * [Make Raw Request](#make-raw-request) ## Description @@ -37,7 +39,41 @@ Now you can create new credentials for the component on the platform: * **Number of retries** (number, optional, 5 by default) - How many times component should retry to make request * **Delay between retries** (number ms, optional, 10000 by default) - How much time wait until new try -## Actions +## Actions + +### Delete Object By ID + +Deletes a single object using its ID. + +#### Configuration Fields + +- **Object Type** - (dropdown, required): The type of the object to delete. + +#### Input Metadata + +- **ID Value** - (string, required): The ID of the object to delete. + +#### Output Metadata + +Returns the ID of the deleted object. + +### Lookup Object By ID + +Retrieves a single object using its ID. + +#### Configuration Fields + +- **Object Type** - (dropdown, required): The type of object to look up. + +#### Input Metadata + +- **ID Value** - (string, required): The ID of the object to look up. + +#### Output Metadata + +Returns an object with the result of the lookup. + +**Known limitation**: Currently, the `Generate Stub Sample` button only allows generating generic metadata, without specific object type details. ### Make Raw Request diff --git a/component.json b/component.json index 1c4bc2e..3c212d7 100644 --- a/component.json +++ b/component.json @@ -1,7 +1,7 @@ { "title": "Podio component", "description": "A smart connector for accessing Podio API", - "version": "1.0.0", + "version": "1.1.0", "authClientTypes": [ "oauth2" ], @@ -33,6 +33,50 @@ } }, "actions": { + "deleteObjectById": { + "main": "./src/actions/deleteObjectById.js", + "title": "Delete Object By ID", + "help": { + "description": "Delete Object By ID", + "link": "/components/podio/index.html#delete-object-by-id" + }, + "fields": { + "objectType": { + "label": "Object Type", + "viewClass": "SelectView", + "prompt": "Please select the type of object to delete", + "required": true, + "order": 10, + "model": "getDeleteByIdObjects" + } + }, + "metadata": { + "in": "./src/schemas/metadata/deleteObjectById/in.json", + "out": "./src/schemas/metadata/deleteObjectById/out.json" + } + }, + "lookupObjectById": { + "main": "./src/actions/lookupObjectById.js", + "title": "Lookup Object By ID", + "help": { + "description": "Lookup Object By ID", + "link": "/components/podio/index.html#lookup-object-by-id" + }, + "fields": { + "objectType": { + "label": "Object Type", + "viewClass": "SelectView", + "prompt": "Please select the type of object to lookup", + "required": true, + "order": 10, + "model": "getLookupByIdObjects" + } + }, + "metadata": { + "in": "src/schemas/metadata/lookupObjectById/in.json", + "out": "src/schemas/metadata/lookupObjectById/out.json" + } + }, "makeRawRequest": { "main": "./src/actions/rawRequest.js", "title": "Make Raw Request", diff --git a/package.json b/package.json index 3d5f60c..66882ad 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,9 @@ "scripts": { "audit": "better-npm-audit audit --level high --production", "lint": "eslint --ext .ts --quiet --fix", - "pretest": "eslint --ext .ts --quiet --fix && find src spec spec-integration -name \\\"*.js\\\" -type f -delete && rm -f verifyCredentials.js", + "pretest": "eslint --ext .ts --quiet --fix && find src spec -name \\\"*.js\\\" -type f -delete && rm -f verifyCredentials.js", "test": "mocha --require ts-node/register \"spec/**/*test.ts\"", - "integrationTest": "mocha --require ts-node/register --timeout 50000 \"spec-integration/**/*test.ts\" \"spec-integration/verifyCredentials.test.ts\"", - "dev:test": "find src spec spec-integration -name \\\"*.js\\\" -type f -delete && rm -f verifyCredentials.js && tsc && npm run test", - "dev:integrationTest": "env LOG_LEVEL=debug find src spec spec-integration -name \\\"*.js\\\" -type f -delete && rm -f verifyCredentials.js && tsc && npm run integrationTest", + "dev:test": "find src spec -name \\\"*.js\\\" -type f -delete && rm -f verifyCredentials.js && tsc && npm run test", "posttest": "tsc" }, "repository": { diff --git a/spec-integration/actions/rawRequest.test.ts b/spec-integration/actions/rawRequest.test.ts deleted file mode 100644 index 6e60616..0000000 --- a/spec-integration/actions/rawRequest.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'assert/strict'; -import { getContext } from '../common'; -import { processAction } from '../../src/actions/rawRequest'; - -describe('rawRequest', () => { - it('should make GET request', async () => { - const cfg = { - secretId: process.env.ELASTICIO_SECRET_ID, - }; - const msg = { body: { method: 'GET', url: '/assets:search?assetTypes=REPORT' } }; - const { body } = await processAction.call(getContext(), msg, cfg); - assert.strictEqual(body.statusCode, 200, `Expected status code to be 200, got ${body.statusCode}`); - assert.ok(body.responseBody.assets, 'Expected assets in response body'); - }); -}); diff --git a/spec-integration/common.ts b/spec-integration/common.ts deleted file mode 100644 index cbd24d7..0000000 --- a/spec-integration/common.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable import/first */ -import * as process from 'process'; - -process.env.LOG_OUTPUT_MODE = 'short'; -process.env.API_RETRY_DELAY = '0'; -import { existsSync } from 'fs'; -import { config } from 'dotenv'; - -import getLogger from '@elastic.io/component-logger'; -import { mock } from 'node:test'; - -if (existsSync('.env')) { - config(); - const { - ELASTICIO_API_URI, - ELASTICIO_API_KEY, - ELASTICIO_API_USERNAME, - ELASTICIO_SECRET_ID, - ELASTICIO_WORKSPACE_ID, - } = process.env; - if (!ELASTICIO_API_URI || !ELASTICIO_API_KEY || !ELASTICIO_API_USERNAME || !ELASTICIO_SECRET_ID || !ELASTICIO_WORKSPACE_ID) { - throw new Error('Please, provide all environment variables'); - } -} else { - throw new Error('Please, provide environment variables to .env'); -} -export const getContext = () => ({ - logger: getLogger(), - emit: mock.fn(), -}); - -export class StatusCodeError extends Error { - response: any; - - constructor(status) { - super(''); - this.response = { status }; - this.message = 'StatusCodeError'; - } -} diff --git a/spec/actions/deleteObjectById.test.ts b/spec/actions/deleteObjectById.test.ts new file mode 100644 index 0000000..2c9bbf3 --- /dev/null +++ b/spec/actions/deleteObjectById.test.ts @@ -0,0 +1,36 @@ +import sinon from 'sinon'; +import chai, { expect } from 'chai'; +import { getContext, StatusCodeError } from '../common'; +import Client from '../../src/Client'; +import { processAction } from '../../src/actions/deleteObjectById'; + +const fakeResponse: any = { + data: {}, + status: 200, + headers: {}, +}; + +chai.use(require('chai-as-promised')); + +describe('"Delete Object by ID" action', async () => { + let execRequest; + describe('One contact found', async () => { + beforeEach(() => { + execRequest = sinon.stub(Client.prototype, 'apiRequest').callsFake(async () => fakeResponse); + }); + afterEach(() => { + sinon.restore(); + }); + it('should successfully delete contact', async () => { + const cfg = { + objectType: 'task' + }; + const msg = { + body: { idValue: 'abc123' }, + }; + const { body } = await processAction.call(getContext(), msg, cfg); + expect(execRequest.callCount).to.equal(1); + expect(body).to.deep.equal({ id: msg.body.idValue }); + }); + }); +}); diff --git a/spec/actions/lookupObjectById.test.ts b/spec/actions/lookupObjectById.test.ts new file mode 100644 index 0000000..c60216b --- /dev/null +++ b/spec/actions/lookupObjectById.test.ts @@ -0,0 +1,37 @@ +import sinon from 'sinon'; +import chai, { expect } from 'chai'; +import { getContext, StatusCodeError } from '../common'; +import Client from '../../src/Client'; +import { processAction } from '../../src/actions/lookupObjectById'; +import task from '../resources/task.json'; + +const fakeResponse: any = { + data: task, + status: 200, + headers: {}, +}; + +chai.use(require('chai-as-promised')); + +describe('"Lookup Object by ID" action', async () => { + let execRequest; + describe('One contact found', async () => { + beforeEach(() => { + execRequest = sinon.stub(Client.prototype, 'apiRequest').callsFake(async () => fakeResponse); + }); + afterEach(() => { + sinon.restore(); + }); + it('should successfully emit contact', async () => { + const cfg = { + objectType: 'task' + }; + const msg = { + body: { idValue: '300975023' }, + }; + const { body } = await processAction.call(getContext(), msg, cfg); + expect(execRequest.callCount).to.equal(1); + expect(body).to.deep.equal(task); + }); + }); +}); diff --git a/spec/resources/task.json b/spec/resources/task.json new file mode 100644 index 0000000..16d9224 --- /dev/null +++ b/spec/resources/task.json @@ -0,0 +1,163 @@ +{ + "comments": [], + "completed_by": null, + "completed_on": null, + "completed_via": null, + "created_by": { + "avatar": null, + "avatar_id": null, + "avatar_type": "file", + "image": null, + "last_seen_on": "2025-06-05 13:29:33", + "name": "Ps Team", + "id": 77032728, + "type": "user", + "url": "https://podio.com/users/77032728", + "user_id": 77032728 + }, + "created_on": "2025-05-28 11:17:52", + "created_via": { + "auth_client_id": 5553, + "display": false, + "id": 5553, + "name": "Welcome Tasks", + "url": null + }, + "description": "Both apps are available here: https://podio.com/site/mobile", + "due_date": "2025-05-29", + "due_on": "2025-05-29 20:59:59", + "due_time": null, + "external_id": null, + "files": [], + "is_liked": false, + "labels": [ + { + "color": "D1F3EC", + "label_id": 5673579, + "text": "Update ladel" + } + ], + "like_count": 0, + "link": "https://podio.com/tasks/300975023", + "presence": { + "user_id": 77032728, + "signature": "0b879da261acb363d95398ddeacb46dd7d5e4e90", + "ref_type": "task", + "ref_id": 300975023 + }, + "private": true, + "push": { + "channel": "/task/300975023", + "timestamp": 1749130272, + "signature": "34226c49a50d4e3d66644a6d6a2382dcdcb0ddea", + "expires_in": 21600 + }, + "recurrence": null, + "ref": { + "created_by": { + "avatar": 128799219, + "avatar_id": 128799219, + "avatar_type": "file", + "image": { + "external_file_id": null, + "file_id": 128799219, + "hosted_by": "podio", + "hosted_by_humanized_name": "Podio", + "link": "https://files.podio.com/128799219", + "link_target": "_blank", + "thumbnail_link": "https://files.podio.com/128799219", + "uuid_link": "https://files.podio.com/768f766d-9ee6-4299-8cbb-b35f679d0a77" + }, + "last_seen_on": "2015-01-08 12:23:54", + "name": "Igor Drobiazko", + "id": 2503534, + "type": "user", + "url": "https://podio.com/users/2503534", + "user_id": 2503534 + }, + "created_on": "2014-10-20 11:34:41", + "created_via": { + "auth_client_id": 1, + "display": false, + "id": 1, + "name": "Podio", + "url": null + }, + "data": { + "item_accounting_info": { + "org_id": 786243, + "limit": 100, + "count": 26640, + "percent": 26640 + }, + "name": "Employee Network", + "org": { + "contract_status": "none", + "domains": [ + "elastic.io" + ], + "image": { + "external_file_id": null, + "file_id": 1177280231, + "hosted_by": "podio", + "hosted_by_humanized_name": "Podio", + "link": "https://files.podio.com/1177280231", + "link_target": "_blank", + "thumbnail_link": "https://files.podio.com/1177280231", + "uuid_link": "https://files.podio.com/0b14697b-413c-43c2-83f2-5203f54a8b20" + }, + "logo": 1177280231, + "name": "elastic.io", + "org_id": 786243, + "premium": false, + "segment": null, + "segment_size": null, + "status": "active", + "tier": null, + "type": "free", + "url": "https://podio.com/elasticio", + "url_label": "elasticio" + }, + "org_id": 786243, + "sharefile_vault_url": null, + "space_id": 2771109, + "type": "emp_network", + "url": "https://podio.com/elasticio/employeenetwork", + "url_label": "employeenetwork" + }, + "id": 2771109, + "link": "https://podio.com/elasticio/employeenetwork", + "title": "Employee Network", + "type": "space", + "type_name": "workspace" + }, + "reminder": null, + "responsible": { + "image": null, + "last_seen_on": "2025-06-05 13:29:33", + "link": "https://podio.com/users/77032728", + "org_id": null, + "profile_id": 271581035, + "type": "user", + "space_id": null, + "user_id": 77032728, + "avatar": null, + "name": "Ps Team" + }, + "rights": [ + "rate", + "update", + "subscribe", + "view", + "add_file", + "comment", + "delete" + ], + "space_id": 2771109, + "started": false, + "status": "active", + "subscribed": true, + "subscribed_count": 1, + "task_id": 300975023, + "text": "Get tasks on the go. Download the iPhone or Android app now." +} diff --git a/src/actions/deleteObjectById.ts b/src/actions/deleteObjectById.ts new file mode 100644 index 0000000..bf46621 --- /dev/null +++ b/src/actions/deleteObjectById.ts @@ -0,0 +1,44 @@ +import { messages } from 'elasticio-node'; +import * as commons from '@elastic.io/component-commons-library'; +import { deleteObjectByIdMapping } from '../utils'; +import deleteByIdObjects from '../schemas/objectTypes/deleteObjectById.json'; +import Client from '../Client'; + +let client: Client; + +export async function processAction(msg: any, cfg: any) { + this.logger.info('"Delete Object By ID" action started'); + + client ||= new Client(msg, cfg); + client.setLogger(this.logger); + + const { objectType } = cfg; + const { idValue } = msg.body; + if (!idValue) { + throw new Error('No "ID Value" provided!'); + } + + let url = deleteObjectByIdMapping[objectType]?.url; + if (!url) throw new Error(`Unsupported Object Type - ${objectType}`); + + url = url.replace('{id}', idValue); + + try { + await client.apiRequest({ url, method: 'DELETE' }); + } catch (err) { + if (err.response) { + throw new Error(commons.getErrMsg(err.response)); + } + throw err; + } + + this.logger.info('"Delete Object By ID" action is done, emitting...'); + return messages.newMessageWithBody({ id: idValue }); +} + +export const getDeleteByIdObjects = async function getDeleteByIdObjects() { + return deleteByIdObjects; +}; + +module.exports.process = processAction; +module.exports.getlookupByIdObjects = getDeleteByIdObjects; diff --git a/src/actions/lookupObjectById.ts b/src/actions/lookupObjectById.ts new file mode 100644 index 0000000..c4adfbe --- /dev/null +++ b/src/actions/lookupObjectById.ts @@ -0,0 +1,45 @@ +import * as commons from '@elastic.io/component-commons-library'; +import { messages } from 'elasticio-node'; +import { lookupObjectByIdMapping } from '../utils'; +import Client from '../Client'; +import lookupByIdObjects from '../schemas/objectTypes/lookupObjectById.json'; + +let client: Client; + +export async function processAction(msg: any, cfg: any) { + this.logger.info('"Lookup Object By ID" action started'); + + client ||= new Client(this, cfg); + client.setLogger(this.logger); + + const { objectType } = cfg; + + const { idValue } = msg.body; + if (!idValue) { + throw new Error('No "ID Value" provided!'); + } + + let url = lookupObjectByIdMapping[objectType]?.url; + if (!url) throw new Error(`Unsupported Object Type - ${objectType}`); + + url = url.replace('{id}', idValue); + + let result; + try { + const response = await client.apiRequest({ url, method: 'GET' }); + result = response.data; + } catch (err) { + if (err.response) throw new Error(commons.getErrMsg(err.response)); + throw err; + } + + this.logger.info('"Lookup Object By ID" action is done, emitting...'); + return messages.newMessageWithBody(result); +} + +export const getLookupByIdObjects = async function getLookupByIdObjects() { + return lookupByIdObjects; +}; + +module.exports.process = processAction; +module.exports.getLookupByIdObjects = getLookupByIdObjects; diff --git a/src/schemas/metadata/deleteObjectById/in.json b/src/schemas/metadata/deleteObjectById/in.json new file mode 100644 index 0000000..0c5418f --- /dev/null +++ b/src/schemas/metadata/deleteObjectById/in.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "idValue": { + "type": "string", + "required": true, + "title": "ID Value" + } + } +} diff --git a/src/schemas/metadata/deleteObjectById/out.json b/src/schemas/metadata/deleteObjectById/out.json new file mode 100644 index 0000000..37a76f6 --- /dev/null +++ b/src/schemas/metadata/deleteObjectById/out.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "string", + "required": true, + "title": "ID" + } + } +} diff --git a/src/schemas/metadata/lookupObjectById/in.json b/src/schemas/metadata/lookupObjectById/in.json new file mode 100644 index 0000000..0c5418f --- /dev/null +++ b/src/schemas/metadata/lookupObjectById/in.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "idValue": { + "type": "string", + "required": true, + "title": "ID Value" + } + } +} diff --git a/src/schemas/metadata/lookupObjectById/out.json b/src/schemas/metadata/lookupObjectById/out.json new file mode 100644 index 0000000..e1a8346 --- /dev/null +++ b/src/schemas/metadata/lookupObjectById/out.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} diff --git a/src/schemas/metadata/rawRequest/in.json b/src/schemas/metadata/rawRequest/in.json new file mode 100644 index 0000000..57c7129 --- /dev/null +++ b/src/schemas/metadata/rawRequest/in.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties": { + "method": { + "type": "string", + "required": true, + "title": "Method", + "enum": [ + "DELETE", + "GET", + "POST", + "PUT" + ] + }, + "url": { + "type": "string", + "required": true, + "title": "URL", + "help": { + "description": "Path of the resource relative to the base URL (here comes a part of the path that goes after 'https://api.podio.com')" + } + }, + "data": { + "type": "object", + "title": "Request Body" + } + } +} diff --git a/src/schemas/metadata/rawRequest/out.json b/src/schemas/metadata/rawRequest/out.json new file mode 100644 index 0000000..cb22f88 --- /dev/null +++ b/src/schemas/metadata/rawRequest/out.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "properties": { + "statusCode": { + "type": "number", + "title": "Status Code", + "required": true + }, + "headers": { + "type": "object", + "title": "HTTP headers", + "required": true, + "properties": {} + }, + "responseBody": { + "type": "object", + "title": "Response Body", + "properties": {} + } + } +} diff --git a/src/schemas/objectTypes/deleteObjectById.json b/src/schemas/objectTypes/deleteObjectById.json new file mode 100644 index 0000000..ac9091b --- /dev/null +++ b/src/schemas/objectTypes/deleteObjectById.json @@ -0,0 +1,17 @@ +{ + "app": "App", + "comment": "Comment", + "file": "File", + "flow": "Flow", + "form": "Form", + "hook": "Hook", + "integration": "Integration", + "item": "Item", + "label": "Label", + "space": "Space", + "status": "Status", + "task": "Task", + "view": "View", + "voting": "Voting Object", + "widget": "Widget" +} diff --git a/src/schemas/objectTypes/lookupObjectById.json b/src/schemas/objectTypes/lookupObjectById.json new file mode 100644 index 0000000..9a9521c --- /dev/null +++ b/src/schemas/objectTypes/lookupObjectById.json @@ -0,0 +1,13 @@ +{ + "action": "Action", + "app": "App", + "batch": "Batch", + "comment": "Comment", + "file": "File", + "flow": "Flow", + "form": "Form", + "item": "Item", + "notification": "Notification", + "organization": "Organization", + "task": "Task" +} diff --git a/src/utils.ts b/src/utils.ts index 4d99516..d887f7c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,3 +31,87 @@ export async function refreshSecret(credentialId: string) { } export const isNumberNaN = (num) => Number(num).toString() === 'NaN'; + +export const deleteObjectByIdMapping = { + app: { + url: '/app/{id}', + }, + comment: { + url: '/comment/{id}', + }, + file: { + url: '/file/{id}', + }, + flow: { + url: '/flow/{id}', + }, + form: { + url: '/form/{id}', + }, + item: { + url: '/item/{id}', + }, + hook: { + url: '/hook/{id}', + }, + integration: { + url: '/integration/{id}', + }, + space: { + url: '/space/{id}', + }, + status: { + url: '/status/{id}', + }, + label: { + url: '/task/label/{id}', + }, + task: { + url: '/task/{id}', + }, + view: { + url: '/view/{id}', + }, + voting: { + url: '/voting/{id}', + }, + widget: { + url: '/widget/{id}', + }, +}; + +export const lookupObjectByIdMapping = { + action: { + url: '/action/{id}', + }, + app: { + url: '/app/{id}', + }, + batch: { + url: '/batch/{id}', + }, + comment: { + url: '/comment/{id}', + }, + file: { + url: '/file/{id}', + }, + flow: { + url: '/flow/{id}', + }, + form: { + url: '/form/{id}', + }, + item: { + url: '/item/{id}', + }, + notification: { + url: '/notification/{id}', + }, + organization: { + url: '/org/{id}', + }, + task: { + url: '/task/{id}', + }, +};