From a45459d7d4230d87db62267e6c00f04be71752ac Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Thu, 5 Dec 2024 15:15:21 -0500 Subject: [PATCH 1/7] ironclad init --- .../actions/create-record/create-record.mjs | 38 ++++ .../launch-workflow/launch-workflow.mjs | 43 ++++ .../update-workflow/update-workflow.mjs | 33 +++ components/ironclad/ironclad.app.mjs | 198 +++++++++++++++++- components/ironclad/package.json | 2 +- .../new-approval-event-instant.mjs | 106 ++++++++++ .../new-workflow-document-event-instant.mjs | 177 ++++++++++++++++ .../new-workflow-event-instant.mjs | 113 ++++++++++ 8 files changed, 706 insertions(+), 4 deletions(-) create mode 100644 components/ironclad/actions/create-record/create-record.mjs create mode 100644 components/ironclad/actions/launch-workflow/launch-workflow.mjs create mode 100644 components/ironclad/actions/update-workflow/update-workflow.mjs create mode 100644 components/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs create mode 100644 components/ironclad/sources/new-workflow-document-event-instant/new-workflow-document-event-instant.mjs create mode 100644 components/ironclad/sources/new-workflow-event-instant/new-workflow-event-instant.mjs 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..bd9baff157a60 --- /dev/null +++ b/components/ironclad/actions/create-record/create-record.mjs @@ -0,0 +1,38 @@ +import ironclad from "../../ironclad.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "ironclad-create-record", + name: "Create Record", + description: "Creates a new record in Ironclad. [See the documentation](/reference/create-a-record)", + version: "0.0.{{ts}}", + type: "action", + props: { + ironclad, + recordData: { + propDefinition: [ + ironclad, + "recordData", + ], + }, + user: { + propDefinition: [ + ironclad, + "user", + ], + optional: true, + }, + tags: { + propDefinition: [ + ironclad, + "tags", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.ironclad.createRecord(this.recordData, this.user, this.tags); + $.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..cf14bc1a67f7b --- /dev/null +++ b/components/ironclad/actions/launch-workflow/launch-workflow.mjs @@ -0,0 +1,43 @@ +import ironclad from "../../ironclad.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "ironclad-launch-workflow", + name: "Launch Workflow", + description: "Launches a new workflow in Ironclad. [See the documentation]()", + version: "0.0.{{ts}}", + type: "action", + props: { + ironclad, + workflowDetails: { + propDefinition: [ + ironclad, + "workflowDetails", + ], + }, + attachments: { + propDefinition: [ + ironclad, + "attachments", + ], + optional: true, + }, + user: { + propDefinition: [ + ironclad, + "user", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.ironclad.launchWorkflow( + this.workflowDetails, + this.attachments, + this.user, + ); + const workflowId = response.id || response.workflowId || "unknown"; + $.export("$summary", `Workflow launched successfully with ID ${workflowId}`); + return response; + }, +}; 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..a644132b9b4b7 --- /dev/null +++ b/components/ironclad/actions/update-workflow/update-workflow.mjs @@ -0,0 +1,33 @@ +import ironclad from "../../ironclad.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "ironclad-update-workflow", + name: "Update Workflow Metadata", + description: "Updates the metadata of an existing workflow. [See the documentation]()/", + version: "0.0.{{ts}}", + type: "action", + props: { + ironclad, + workflowId: { + propDefinition: [ + "ironclad", + "workflowId", + ], + }, + updatedMetadata: { + propDefinition: [ + "ironclad", + "updatedMetadata", + ], + }, + }, + async run({ $ }) { + const response = await this.ironclad.updateWorkflowMetadata( + this.workflowId, + this.updatedMetadata, + ); + $.export("$summary", `Workflow ${this.workflowId} updated successfully`); + return response; + }, +}; diff --git a/components/ironclad/ironclad.app.mjs b/components/ironclad/ironclad.app.mjs index 9cd862d981fe1..45c92033fe72c 100644 --- a/components/ironclad/ironclad.app.mjs +++ b/components/ironclad/ironclad.app.mjs @@ -1,11 +1,203 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "ironclad", - propDefinitions: {}, + version: "0.0.{{ts}}", + propDefinitions: { + selectedEvent: { + type: "string[]", + label: "Selected Events", + description: "Select the Ironclad events to emit", + async options() { + const events = [ + "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", + "*", + ]; + return events.map((event) => ({ + label: event.replace(/_/g, " ").toUpperCase(), + value: event, + })); + }, + }, + workflowDetails: { + type: "object", + label: "Workflow Details", + description: "Details required to launch a new workflow", + properties: { + templateId: { + type: "string", + label: "Template ID", + description: "ID of the workflow template to use", + }, + attributes: { + type: "object", + label: "Attributes", + description: "Workflow attributes as key-value pairs", + }, + }, + }, + attachments: { + type: "string[]", + label: "Attachments", + description: "Optional attachments to include when launching workflow", + optional: true, + }, + user: { + type: "object", + label: "User", + description: "Optional user information for actions that support it", + optional: true, + properties: { + userId: { + type: "string", + label: "User ID", + description: "ID of the user performing the action", + }, + }, + }, + recordData: { + type: "object", + label: "Record Data", + description: "Data required to create a new record in Ironclad", + properties: { + type: { + type: "string", + label: "Record Type", + description: "Type/category of the record", + }, + name: { + type: "string", + label: "Name", + description: "Name/title of the record", + }, + properties: { + type: "object", + label: "Properties", + description: "Metadata properties of the record", + }, + }, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Optional tags for the record", + optional: true, + }, + workflowId: { + type: "string", + label: "Workflow ID", + description: "ID of the workflow to update", + }, + updatedMetadata: { + type: "object", + label: "Updated Metadata", + description: "New metadata to update the workflow", + properties: { + status: { + type: "string", + label: "Status", + description: "New status for the workflow", + }, + comments: { + type: "string", + label: "Comments", + description: "Additional comments for the workflow", + }, + }, + }, + }, methods: { - // this.$auth contains connected account data authKeys() { console.log(Object.keys(this.$auth)); }, + _baseUrl() { + return "https://api.ironcladapp.com"; + }, + async _makeRequest(opts = {}) { + const { + $, method = "GET", path = "/", headers, ...otherOpts + } = opts; + return axios($, { + method, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }, + ...otherOpts, + }); + }, + async launchWorkflow(workflowDetails, attachments, user) { + const data = { + ...workflowDetails, + }; + if (attachments && attachments.length > 0) { + data.attachments = attachments; + } + if (user) { + data.user = user; + } + return await this._makeRequest({ + method: "POST", + path: "/workflows", + data, + }); + }, + async createRecord(recordData, user, tags) { + const data = { + ...recordData, + }; + if (user) { + data.user = user; + } + if (tags && tags.length > 0) { + data.tags = tags; + } + return await this._makeRequest({ + method: "POST", + path: "/records", + data, + }); + }, + async updateWorkflowMetadata(workflowId, updatedMetadata) { + return await this._makeRequest({ + method: "PATCH", + path: `/workflows/${workflowId}`, + data: updatedMetadata, + }); + }, }, -}; \ No newline at end of file +}; diff --git a/components/ironclad/package.json b/components/ironclad/package.json index f5b4c2be43f39..1c79e1665bb9c 100644 --- a/components/ironclad/package.json +++ b/components/ironclad/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} 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..7e1fb9b159480 --- /dev/null +++ b/components/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs @@ -0,0 +1,106 @@ +import ironclad from "../../ironclad.app.mjs"; +import { axios } from "@pipedream/platform"; +import crypto from "crypto"; + +export default { + key: "ironclad-new-approval-event-instant", + name: "New Approval Event Instant", + description: "Emit new event when a fresh approval event is generated. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + ironclad: { + type: "app", + app: "ironclad", + }, + selectedEvent: { + propDefinition: [ + ironclad, + "selectedEvent", + ], + }, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + methods: { + async _createWebhook() { + const callbackUrl = this.http.endpoint; + const events = this.selectedEvent; + + const data = { + callback_url: callbackUrl, + events: events, + }; + + const response = await this.ironclad._makeRequest({ + method: "POST", + path: "/webhooks", + data, + }); + + return response.webhookID; + }, + async _deleteWebhook(webhookId) { + await this.ironclad._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + computeSignature(rawBody) { + const hmac = crypto.createHmac("sha256", this.ironclad.$auth.secret_key); + return hmac.update(rawBody).digest("hex"); + }, + }, + hooks: { + async deploy() { + const workflows = await this.ironclad.listWorkflows(); + const recentWorkflows = workflows.slice(-50); + for (const workflow of recentWorkflows) { + this.$emit(workflow, { + id: workflow.workflowID || workflow.webhookID || workflow.id, + summary: `New approval event: ${workflow.event}`, + ts: Date.parse(workflow.timestamp) || Date.now(), + }); + } + }, + async activate() { + const webhookId = await this._createWebhook(); + await this.db.set("webhookId", webhookId); + }, + async deactivate() { + const webhookId = await this.db.get("webhookId"); + if (webhookId) { + await this._deleteWebhook(webhookId); + await this.db.delete("webhookId"); + } + }, + }, + async run(event) { + const rawBody = event.rawBody; + const receivedSignature = event.headers["X-Ironclad-Signature"]; + const computedSignature = this.computeSignature(rawBody); + + if (computedSignature !== receivedSignature) { + await this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + const payload = JSON.parse(rawBody); + const id = payload.payload.workflowID || payload.webhookID || Date.now(); + const summary = `New approval event: ${payload.payload.event}`; + const ts = Date.parse(payload.timestamp) || Date.now(); + + this.$emit(payload, { + id, + summary, + ts, + }); + }, +}; 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..b0f2cb7066e30 --- /dev/null +++ b/components/ironclad/sources/new-workflow-document-event-instant/new-workflow-document-event-instant.mjs @@ -0,0 +1,177 @@ +import ironclad from "../../ironclad.app.mjs"; +import crypto from "crypto"; +import { axios } from "@pipedream/platform"; + +export default { + 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. [See the documentation](${docsLink})", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + ironclad: { + type: "app", + app: "ironclad", + }, + selectedEvent: { + propDefinition: [ + "ironclad", + "selectedEvent", + ], + }, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + async _getWebhookId() { + return this.db.get("webhookId"); + }, + async _setWebhookId(id) { + await this.db.set("webhookId", id); + }, + async _createWebhook() { + const events = this.selectedEvent; + const callbackUrl = this.http.endpoint; + const data = { + callbackUrl, + events, + }; + const response = await this.ironclad._makeRequest({ + method: "POST", + path: "/webhooks", + data, + }); + return response.webhookID || response.id; + }, + async _deleteWebhook(webhookId) { + await this.ironclad._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + _validateSignature(event) { + const secret = this.ironclad.$auth.webhook_secret; + if (!secret) { + return true; // No secret available, skip validation + } + const signature = event.headers["x-ironclad-signature"]; + if (!signature) { + return false; + } + const hmac = crypto.createHmac("sha256", secret); + hmac.update(JSON.stringify(event.body)); + const computedSignature = hmac.digest("base64"); + return computedSignature === signature; + }, + }, + hooks: { + async deploy() { + const events = this.selectedEvent; + const fetchEvents = async (event) => { + const historicalEvents = await this.ironclad._makeRequest({ + method: "GET", + path: `/events/${event}`, + params: { + limit: 50, + sort: "desc", + }, + }); + for (const evt of historicalEvents.payload) { + this.$emit(evt, { + id: evt.webhookID || evt.workflowID || evt.documentKey || crypto.randomBytes(16).toString("hex"), + summary: `Historical event: ${evt.event}`, + ts: Date.parse(evt.timestamp) || Date.now(), + }); + } + }; + + if (events.includes("*")) { + const allEvents = [ + "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", + ]; + for (const event of allEvents) { + await fetchEvents(event); + } + } else { + for (const event of events) { + await fetchEvents(event); + } + } + }, + async activate() { + const webhookId = await this._createWebhook(); + await this._setWebhookId(webhookId); + }, + async deactivate() { + const webhookId = await this._getWebhookId(); + if (webhookId) { + await this._deleteWebhook(webhookId); + } + }, + }, + async run(event) { + // Validate webhook signature + const isValid = this._validateSignature(event); + if (!isValid) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + const payload = event.body; + + // Determine a unique ID for deduplication + const uniqueId = payload.webhookID || payload.workflowID || payload.documentKey || crypto.randomBytes(16).toString("hex"); + + // Emit the event + this.$emit(payload, { + id: uniqueId, + summary: `New document event: ${payload.event}`, + ts: Date.parse(payload.timestamp) || Date.now(), + }); + + // Respond to the HTTP request + this.http.respond({ + status: 200, + body: "OK", + }); + }, +}; 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..e1b0ed4081e8f --- /dev/null +++ b/components/ironclad/sources/new-workflow-event-instant/new-workflow-event-instant.mjs @@ -0,0 +1,113 @@ +import { axios } from "@pipedream/platform"; +import ironclad from "../../ironclad.app.mjs"; +import crypto from "crypto"; + +export default { + key: "ironclad-new-workflow-event-instant", + name: "Ironclad New Workflow Event (Instant)", + description: "Emit new event when a new workflow event is created. [See the documentation](https://developer.ironcladapp.com/reference/webhooks)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + ironclad: { + type: "app", + app: "ironclad", + }, + selectedEvent: { + propDefinition: [ + ironclad, + "selectedEvent", + ], + }, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + async createWebhook() { + const body = { + events: this.selectedEvent.length > 0 + ? this.selectedEvent + : [ + "*", + ], + url: this.http.endpoint, + }; + const response = await this.ironclad._makeRequest({ + method: "POST", + path: "/webhooks", + data: body, + }); + return response.webhookID; + }, + async deleteWebhook() { + const webhookID = await this.db.get("webhookID"); + if (webhookID) { + await this.ironclad._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookID}`, + }); + } + }, + async getEvents() { + const events = await this.ironclad._makeRequest({ + method: "GET", + path: "/events?limit=50", + }); + return events; + }, + verifySignature(rawBody, signature) { + const secret = this.ironclad.$auth.webhook_secret; + const computedSignature = crypto.createHmac("sha256", secret).update(rawBody) + .digest("base64"); + return computedSignature === signature; + }, + }, + hooks: { + async deploy() { + const events = await this.getEvents(); + events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + for (const event of events) { + this.$emit(event.payload, { + id: event.workflowID || event.webhookID, + summary: `New event: ${event.payload.event}`, + ts: new Date(event.timestamp).getTime(), + }); + } + }, + async activate() { + const webhookID = await this.createWebhook(); + await this.db.set("webhookID", webhookID); + }, + async deactivate() { + await this.deleteWebhook(); + await this.db.delete("webhookID"); + }, + }, + async run(event) { + const signature = event.headers["X-Ironclad-Signature"]; + const rawBody = event.rawBody; + if (!this.verifySignature(rawBody, signature)) { + return this.http.respond({ + status: 401, + body: "Unauthorized", + }); + } + + const payload = event.payload; + if (this.selectedEvent.includes(payload.event) || this.selectedEvent.includes("*")) { + this.$emit(payload, { + id: payload.workflowID || payload.webhookID, + summary: `New event: ${payload.event}`, + ts: new Date(payload.timestamp).getTime() || Date.now(), + }); + } + this.http.respond({ + status: 200, + body: "OK", + }); + }, +}; From 3a89f05a961e875e333dbfebc0a56b3e38e71c62 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Thu, 5 Dec 2024 16:26:44 -0500 Subject: [PATCH 2/7] wip --- .../actions/create-record/create-record.mjs | 84 ++++++++-- .../launch-workflow/launch-workflow.mjs | 1 - .../update-workflow/update-workflow.mjs | 1 - components/ironclad/ironclad.app.mjs | 145 ++++++++---------- components/ironclad/package.json | 5 +- .../new-approval-event-instant.mjs | 6 +- .../new-workflow-document-event-instant.mjs | 6 +- .../new-workflow-event-instant.mjs | 1 - 8 files changed, 141 insertions(+), 108 deletions(-) diff --git a/components/ironclad/actions/create-record/create-record.mjs b/components/ironclad/actions/create-record/create-record.mjs index bd9baff157a60..47abe0cf7b095 100644 --- a/components/ironclad/actions/create-record/create-record.mjs +++ b/components/ironclad/actions/create-record/create-record.mjs @@ -1,37 +1,99 @@ import ironclad from "../../ironclad.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "ironclad-create-record", name: "Create Record", - description: "Creates a new record in Ironclad. [See the documentation](/reference/create-a-record)", + description: "Creates a new record in Ironclad. [See the documentation](https://developer.ironcladapp.com/reference/create-a-record)", version: "0.0.{{ts}}", type: "action", props: { ironclad, - recordData: { + name: { + type: "string", + label: "Name", + description: "Name of the record", + }, + type: { + propDefinition: [ + ironclad, + "recordType", + ], + }, + links: { propDefinition: [ ironclad, - "recordData", + "recordId", ], + type: "string[]", + label: "Links", + description: "Record ID's to link to the new record", }, - user: { + parent: { propDefinition: [ ironclad, - "user", + "recordId", ], - optional: true, + label: "Parent", + description: "Record ID to be set as the parent of the current record", }, - tags: { + children: { propDefinition: [ ironclad, - "tags", + "recordId", ], - optional: true, + 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.getRecordSchema(); + for (const property of this.properties) { + props[property] = { + type: "string", + label: properties[property].displayName, + }; + } + return props; + }, async run({ $ }) { - const response = await this.ironclad.createRecord(this.recordData, this.user, this.tags); + const { properties } = await this.ironclad.getRecordSchema(); + 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 index cf14bc1a67f7b..d9fef26694092 100644 --- a/components/ironclad/actions/launch-workflow/launch-workflow.mjs +++ b/components/ironclad/actions/launch-workflow/launch-workflow.mjs @@ -1,5 +1,4 @@ import ironclad from "../../ironclad.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "ironclad-launch-workflow", diff --git a/components/ironclad/actions/update-workflow/update-workflow.mjs b/components/ironclad/actions/update-workflow/update-workflow.mjs index a644132b9b4b7..94dfd57269a70 100644 --- a/components/ironclad/actions/update-workflow/update-workflow.mjs +++ b/components/ironclad/actions/update-workflow/update-workflow.mjs @@ -1,5 +1,4 @@ import ironclad from "../../ironclad.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "ironclad-update-workflow", diff --git a/components/ironclad/ironclad.app.mjs b/components/ironclad/ironclad.app.mjs index 45c92033fe72c..2bf6fd0efebd4 100644 --- a/components/ironclad/ironclad.app.mjs +++ b/components/ironclad/ironclad.app.mjs @@ -5,6 +5,43 @@ export default { app: "ironclad", version: "0.0.{{ts}}", propDefinitions: { + recordType: { + type: "string", + label: "Type", + description: "The type of the record", + async options() { + const { recordTypes } = await this.getRecordsSchema(); + return (Object.keys(recordTypes)).map((type) => type); + }, + }, + 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.getRecordSchema(); + return (Object.keys(properties)).map((property) => property); + }, + }, selectedEvent: { type: "string[]", label: "Selected Events", @@ -75,91 +112,40 @@ export default { description: "Optional attachments to include when launching workflow", optional: true, }, - user: { - type: "object", - label: "User", - description: "Optional user information for actions that support it", - optional: true, - properties: { - userId: { - type: "string", - label: "User ID", - description: "ID of the user performing the action", - }, - }, - }, - recordData: { - type: "object", - label: "Record Data", - description: "Data required to create a new record in Ironclad", - properties: { - type: { - type: "string", - label: "Record Type", - description: "Type/category of the record", - }, - name: { - type: "string", - label: "Name", - description: "Name/title of the record", - }, - properties: { - type: "object", - label: "Properties", - description: "Metadata properties of the record", - }, - }, - }, - tags: { - type: "string[]", - label: "Tags", - description: "Optional tags for the record", - optional: true, - }, workflowId: { type: "string", label: "Workflow ID", description: "ID of the workflow to update", }, - updatedMetadata: { - type: "object", - label: "Updated Metadata", - description: "New metadata to update the workflow", - properties: { - status: { - type: "string", - label: "Status", - description: "New status for the workflow", - }, - comments: { - type: "string", - label: "Comments", - description: "Additional comments for the workflow", - }, - }, - }, }, methods: { - authKeys() { - console.log(Object.keys(this.$auth)); - }, _baseUrl() { - return "https://api.ironcladapp.com"; + return "https://ironcladapp.com/public/api/v1"; }, - async _makeRequest(opts = {}) { + _makeRequest(opts = {}) { const { - $, method = "GET", path = "/", headers, ...otherOpts + $, path, ...otherOpts } = opts; return axios($, { - method, url: `${this._baseUrl()}${path}`, headers: { - ...headers, - Authorization: `Bearer ${this.$auth.api_token}`, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, }, ...otherOpts, }); }, + getRecordsSchema(opts = {}) { + return this._makeRequest({ + path: "/records/metadata", + ...opts, + }); + }, + listRecords(opts = {}) { + return this._makeRequest({ + path: "/records", + ...opts, + }); + }, async launchWorkflow(workflowDetails, attachments, user) { const data = { ...workflowDetails, @@ -176,27 +162,20 @@ export default { data, }); }, - async createRecord(recordData, user, tags) { - const data = { - ...recordData, - }; - if (user) { - data.user = user; - } - if (tags && tags.length > 0) { - data.tags = tags; - } - return await this._makeRequest({ + createRecord(opts = {}) { + return this._makeRequest({ method: "POST", path: "/records", - data, + ...opts, }); }, - async updateWorkflowMetadata(workflowId, updatedMetadata) { - return await this._makeRequest({ + updateWorkflowMetadata({ + workflowId, ...opts + }) { + return this._makeRequest({ method: "PATCH", - path: `/workflows/${workflowId}`, - data: updatedMetadata, + path: `workflows/${workflowId}/attributes`, + ...opts, }); }, }, diff --git a/components/ironclad/package.json b/components/ironclad/package.json index 1c79e1665bb9c..2f5b2a90419dd 100644 --- a/components/ironclad/package.json +++ b/components/ironclad/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/ironclad", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Ironclad Components", "main": "ironclad.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/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs b/components/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs index 7e1fb9b159480..437389c490bf9 100644 --- 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 @@ -1,5 +1,4 @@ import ironclad from "../../ironclad.app.mjs"; -import { axios } from "@pipedream/platform"; import crypto from "crypto"; export default { @@ -10,10 +9,7 @@ export default { type: "source", dedupe: "unique", props: { - ironclad: { - type: "app", - app: "ironclad", - }, + ironclad, selectedEvent: { propDefinition: [ ironclad, 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 index b0f2cb7066e30..292d7f901fea9 100644 --- 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 @@ -1,6 +1,5 @@ import ironclad from "../../ironclad.app.mjs"; import crypto from "crypto"; -import { axios } from "@pipedream/platform"; export default { key: "ironclad-new-workflow-document-event-instant", @@ -10,10 +9,7 @@ export default { type: "source", dedupe: "unique", props: { - ironclad: { - type: "app", - app: "ironclad", - }, + ironclad, selectedEvent: { propDefinition: [ "ironclad", 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 index e1b0ed4081e8f..72b11b2e3f944 100644 --- 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 @@ -1,4 +1,3 @@ -import { axios } from "@pipedream/platform"; import ironclad from "../../ironclad.app.mjs"; import crypto from "crypto"; From 23ba51aac229986e741287eaf70924cf701e5501 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Thu, 5 Dec 2024 16:28:39 -0500 Subject: [PATCH 3/7] pnpm-lock.yaml --- pnpm-lock.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3754f16770683..2ce734c598f8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5201,7 +5201,11 @@ importers: components/iqair_airvisual: {} - components/ironclad: {} + components/ironclad: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/iscraper: {} @@ -40848,7 +40852,7 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 5.0.0 + minipass: 7.1.2 path-to-regexp@6.3.0: {} From 2cd9e3eb6cdf7d401edac48ac5b3b0bf2014f86a Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Fri, 6 Dec 2024 16:10:14 -0500 Subject: [PATCH 4/7] new components --- .../actions/create-record/create-record.mjs | 10 +- .../launch-workflow/launch-workflow.mjs | 78 +++++--- .../update-workflow/update-workflow.mjs | 70 +++++-- components/ironclad/ironclad.app.mjs | 179 +++++++++-------- components/ironclad/sources/common/base.mjs | 54 ++++++ components/ironclad/sources/common/events.mjs | 36 ++++ .../new-approval-event-instant.mjs | 103 ++-------- .../new-approval-event-instant/test-event.mjs | 14 ++ .../new-workflow-document-event-instant.mjs | 180 ++---------------- .../test-event.mjs | 14 ++ .../new-workflow-event-instant.mjs | 112 ++--------- .../new-workflow-event-instant/test-event.mjs | 10 + 12 files changed, 382 insertions(+), 478 deletions(-) create mode 100644 components/ironclad/sources/common/base.mjs create mode 100644 components/ironclad/sources/common/events.mjs create mode 100644 components/ironclad/sources/new-approval-event-instant/test-event.mjs create mode 100644 components/ironclad/sources/new-workflow-document-event-instant/test-event.mjs create mode 100644 components/ironclad/sources/new-workflow-event-instant/test-event.mjs diff --git a/components/ironclad/actions/create-record/create-record.mjs b/components/ironclad/actions/create-record/create-record.mjs index 47abe0cf7b095..cf81bbef45da8 100644 --- a/components/ironclad/actions/create-record/create-record.mjs +++ b/components/ironclad/actions/create-record/create-record.mjs @@ -4,7 +4,7 @@ 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.{{ts}}", + version: "0.0.1", type: "action", props: { ironclad, @@ -58,17 +58,19 @@ export default { if (!this.properties?.length) { return props; } - const { properties } = await this.ironclad.getRecordSchema(); + const { properties } = await this.ironclad.getRecordsSchema(); for (const property of this.properties) { props[property] = { - type: "string", + type: properties[property].type === "boolean" + ? "boolean" + : "string", label: properties[property].displayName, }; } return props; }, async run({ $ }) { - const { properties } = await this.ironclad.getRecordSchema(); + const { properties } = await this.ironclad.getRecordsSchema(); const propertiesData = {}; for (const property of this.properties) { propertiesData[property] = { diff --git a/components/ironclad/actions/launch-workflow/launch-workflow.mjs b/components/ironclad/actions/launch-workflow/launch-workflow.mjs index d9fef26694092..dcdb5c9c59c03 100644 --- a/components/ironclad/actions/launch-workflow/launch-workflow.mjs +++ b/components/ironclad/actions/launch-workflow/launch-workflow.mjs @@ -3,40 +3,68 @@ import ironclad from "../../ironclad.app.mjs"; export default { key: "ironclad-launch-workflow", name: "Launch Workflow", - description: "Launches a new workflow in Ironclad. [See the documentation]()", - version: "0.0.{{ts}}", + 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, - workflowDetails: { + templateId: { propDefinition: [ ironclad, - "workflowDetails", + "templateId", ], - }, - attachments: { - propDefinition: [ - ironclad, - "attachments", - ], - optional: true, - }, - user: { - propDefinition: [ - ironclad, - "user", - ], - optional: true, + 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 && value?.type !== "document" && value?.elementType?.type !== "document") { + props[key] = { + type: value.type === "boolean" + ? "boolean" + : value.type === "array" + ? "string[]" + : "string", + label: value.displayName, + optional: !(key === "counterpartyName"), + }; + if (key === "paperSource") { + props[key].options = [ + "Counterparty paper", + "Our paper", + ]; + } + } + } + return props; + }, async run({ $ }) { - const response = await this.ironclad.launchWorkflow( - this.workflowDetails, - this.attachments, - this.user, - ); - const workflowId = response.id || response.workflowId || "unknown"; - $.export("$summary", `Workflow launched successfully with ID ${workflowId}`); + const { + ironclad, + templateId, + ...attributes + } = this; + + const response = await ironclad.launchWorkflow({ + $, + data: { + template: templateId, + attributes: { + ...attributes, + }, + }, + }); + $.export("$summary", `Workflow launched successfully with ID ${response.id}`); return response; }, }; diff --git a/components/ironclad/actions/update-workflow/update-workflow.mjs b/components/ironclad/actions/update-workflow/update-workflow.mjs index 94dfd57269a70..3a3a151b230bc 100644 --- a/components/ironclad/actions/update-workflow/update-workflow.mjs +++ b/components/ironclad/actions/update-workflow/update-workflow.mjs @@ -3,30 +3,74 @@ import ironclad from "../../ironclad.app.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.{{ts}}", + description: "Updates the metadata of an existing workflow. [See the documentation]()", + version: "0.0.1", type: "action", props: { ironclad, workflowId: { propDefinition: [ - "ironclad", + ironclad, "workflowId", ], + reloadProps: true, }, - updatedMetadata: { - propDefinition: [ - "ironclad", - "updatedMetadata", - ], + 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 && value?.type !== "document" && value?.elementType?.type !== "document") { + props[key] = { + type: value.type === "boolean" + ? "boolean" + : value.type === "array" + ? "string[]" + : "string", + label: value.displayName, + optional: true, + }; + } + } + return props; + }, async run({ $ }) { - const response = await this.ironclad.updateWorkflowMetadata( - this.workflowId, - this.updatedMetadata, - ); - $.export("$summary", `Workflow ${this.workflowId} updated successfully`); + const { + ironclad, + workflowId, + comment, + ...attributes + } = this; + const response = await ironclad.updateWorkflowMetadata({ + $, + workflowId: workflowId, + data: { + updates: attributes && Object.entries(attributes).map(([ + key, + value, + ]) => ({ + action: "set", + path: key, + value, + })), + comment: comment, + }, + }); + $.export("$summary", `Workflow ${workflowId} updated successfully`); return response; }, }; diff --git a/components/ironclad/ironclad.app.mjs b/components/ironclad/ironclad.app.mjs index 2bf6fd0efebd4..5f1dd54a5d9dd 100644 --- a/components/ironclad/ironclad.app.mjs +++ b/components/ironclad/ironclad.app.mjs @@ -1,9 +1,9 @@ import { axios } from "@pipedream/platform"; +import events from "./sources/common/events.mjs"; export default { type: "app", app: "ironclad", - version: "0.0.{{ts}}", propDefinitions: { recordType: { type: "string", @@ -11,7 +11,13 @@ export default { description: "The type of the record", async options() { const { recordTypes } = await this.getRecordsSchema(); - return (Object.keys(recordTypes)).map((type) => type); + return Object.entries(recordTypes).map(([ + key, + value, + ]) => ({ + value: key, + label: value.displayName, + })); }, }, recordId: { @@ -38,85 +44,59 @@ export default { label: "Properties", description: "Properties to add to the record", async options() { - const { properties } = await this.getRecordSchema(); - return (Object.keys(properties)).map((property) => property); + 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, + })) || []; }, }, - selectedEvent: { + selectedEvents: { type: "string[]", label: "Selected Events", description: "Select the Ironclad events to emit", async options() { - const events = [ - "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", - "*", - ]; return events.map((event) => ({ label: event.replace(/_/g, " ").toUpperCase(), value: event, })); }, }, - workflowDetails: { - type: "object", - label: "Workflow Details", - description: "Details required to launch a new workflow", - properties: { - templateId: { - type: "string", - label: "Template ID", - description: "ID of the workflow template to use", - }, - attributes: { - type: "object", - label: "Attributes", - description: "Workflow attributes as key-value pairs", - }, - }, - }, - attachments: { - type: "string[]", - label: "Attachments", - description: "Optional attachments to include when launching workflow", - optional: true, - }, - workflowId: { - type: "string", - label: "Workflow ID", - description: "ID of the workflow to update", - }, }, methods: { _baseUrl() { @@ -134,32 +114,67 @@ export default { ...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, }); }, - async launchWorkflow(workflowDetails, attachments, user) { - const data = { - ...workflowDetails, - }; - if (attachments && attachments.length > 0) { - data.attachments = attachments; - } - if (user) { - data.user = user; - } - return await this._makeRequest({ + launchWorkflow(opts = {}) { + return this._makeRequest({ method: "POST", path: "/workflows", - data, + ...opts, }); }, createRecord(opts = {}) { @@ -174,7 +189,7 @@ export default { }) { return this._makeRequest({ method: "PATCH", - path: `workflows/${workflowId}/attributes`, + path: `/workflows/${workflowId}/attributes`, ...opts, }); }, 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 index 437389c490bf9..b0053bfd79597 100644 --- 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 @@ -1,102 +1,21 @@ -import ironclad from "../../ironclad.app.mjs"; -import crypto from "crypto"; +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. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a fresh approval event is generated.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - ironclad, - selectedEvent: { - propDefinition: [ - ironclad, - "selectedEvent", - ], - }, - db: "$.service.db", - http: { - type: "$.interface.http", - customResponse: true, - }, - }, methods: { - async _createWebhook() { - const callbackUrl = this.http.endpoint; - const events = this.selectedEvent; - - const data = { - callback_url: callbackUrl, - events: events, - }; - - const response = await this.ironclad._makeRequest({ - method: "POST", - path: "/webhooks", - data, - }); - - return response.webhookID; - }, - async _deleteWebhook(webhookId) { - await this.ironclad._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookId}`, - }); - }, - computeSignature(rawBody) { - const hmac = crypto.createHmac("sha256", this.ironclad.$auth.secret_key); - return hmac.update(rawBody).digest("hex"); + ...common.methods, + getEvents() { + return [ + "workflow_approval_status_changed", + ]; }, }, - hooks: { - async deploy() { - const workflows = await this.ironclad.listWorkflows(); - const recentWorkflows = workflows.slice(-50); - for (const workflow of recentWorkflows) { - this.$emit(workflow, { - id: workflow.workflowID || workflow.webhookID || workflow.id, - summary: `New approval event: ${workflow.event}`, - ts: Date.parse(workflow.timestamp) || Date.now(), - }); - } - }, - async activate() { - const webhookId = await this._createWebhook(); - await this.db.set("webhookId", webhookId); - }, - async deactivate() { - const webhookId = await this.db.get("webhookId"); - if (webhookId) { - await this._deleteWebhook(webhookId); - await this.db.delete("webhookId"); - } - }, - }, - async run(event) { - const rawBody = event.rawBody; - const receivedSignature = event.headers["X-Ironclad-Signature"]; - const computedSignature = this.computeSignature(rawBody); - - if (computedSignature !== receivedSignature) { - await this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - const payload = JSON.parse(rawBody); - const id = payload.payload.workflowID || payload.webhookID || Date.now(); - const summary = `New approval event: ${payload.payload.event}`; - const ts = Date.parse(payload.timestamp) || Date.now(); - - this.$emit(payload, { - id, - summary, - ts, - }); - }, + 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 index 292d7f901fea9..b1adf0fbdc8fd 100644 --- 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 @@ -1,173 +1,25 @@ -import ironclad from "../../ironclad.app.mjs"; -import crypto from "crypto"; +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. [See the documentation](${docsLink})", - version: "0.0.{{ts}}", + 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", - props: { - ironclad, - selectedEvent: { - propDefinition: [ - "ironclad", - "selectedEvent", - ], - }, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - }, methods: { - async _getWebhookId() { - return this.db.get("webhookId"); - }, - async _setWebhookId(id) { - await this.db.set("webhookId", id); - }, - async _createWebhook() { - const events = this.selectedEvent; - const callbackUrl = this.http.endpoint; - const data = { - callbackUrl, - events, - }; - const response = await this.ironclad._makeRequest({ - method: "POST", - path: "/webhooks", - data, - }); - return response.webhookID || response.id; + ...common.methods, + getEvents() { + return [ + "workflow_documents_added", + "workflow_documents_removed", + "workflow_documents_updated", + "workflow_documents_renamed", + "workflow_document_edited", + ]; }, - async _deleteWebhook(webhookId) { - await this.ironclad._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookId}`, - }); - }, - _validateSignature(event) { - const secret = this.ironclad.$auth.webhook_secret; - if (!secret) { - return true; // No secret available, skip validation - } - const signature = event.headers["x-ironclad-signature"]; - if (!signature) { - return false; - } - const hmac = crypto.createHmac("sha256", secret); - hmac.update(JSON.stringify(event.body)); - const computedSignature = hmac.digest("base64"); - return computedSignature === signature; - }, - }, - hooks: { - async deploy() { - const events = this.selectedEvent; - const fetchEvents = async (event) => { - const historicalEvents = await this.ironclad._makeRequest({ - method: "GET", - path: `/events/${event}`, - params: { - limit: 50, - sort: "desc", - }, - }); - for (const evt of historicalEvents.payload) { - this.$emit(evt, { - id: evt.webhookID || evt.workflowID || evt.documentKey || crypto.randomBytes(16).toString("hex"), - summary: `Historical event: ${evt.event}`, - ts: Date.parse(evt.timestamp) || Date.now(), - }); - } - }; - - if (events.includes("*")) { - const allEvents = [ - "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", - ]; - for (const event of allEvents) { - await fetchEvents(event); - } - } else { - for (const event of events) { - await fetchEvents(event); - } - } - }, - async activate() { - const webhookId = await this._createWebhook(); - await this._setWebhookId(webhookId); - }, - async deactivate() { - const webhookId = await this._getWebhookId(); - if (webhookId) { - await this._deleteWebhook(webhookId); - } - }, - }, - async run(event) { - // Validate webhook signature - const isValid = this._validateSignature(event); - if (!isValid) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - const payload = event.body; - - // Determine a unique ID for deduplication - const uniqueId = payload.webhookID || payload.workflowID || payload.documentKey || crypto.randomBytes(16).toString("hex"); - - // Emit the event - this.$emit(payload, { - id: uniqueId, - summary: `New document event: ${payload.event}`, - ts: Date.parse(payload.timestamp) || Date.now(), - }); - - // Respond to the HTTP request - this.http.respond({ - status: 200, - body: "OK", - }); }, + 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 index 72b11b2e3f944..72587a5e60088 100644 --- 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 @@ -1,112 +1,28 @@ -import ironclad from "../../ironclad.app.mjs"; -import crypto from "crypto"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "ironclad-new-workflow-event-instant", - name: "Ironclad New Workflow Event (Instant)", - description: "Emit new event when a new workflow event is created. [See the documentation](https://developer.ironcladapp.com/reference/webhooks)", - version: "0.0.{{ts}}", + 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: { - ironclad: { - type: "app", - app: "ironclad", - }, - selectedEvent: { + ...common.props, + events: { propDefinition: [ - ironclad, - "selectedEvent", + common.props.ironclad, + "selectedEvents", ], }, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", }, methods: { - async createWebhook() { - const body = { - events: this.selectedEvent.length > 0 - ? this.selectedEvent - : [ - "*", - ], - url: this.http.endpoint, - }; - const response = await this.ironclad._makeRequest({ - method: "POST", - path: "/webhooks", - data: body, - }); - return response.webhookID; - }, - async deleteWebhook() { - const webhookID = await this.db.get("webhookID"); - if (webhookID) { - await this.ironclad._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookID}`, - }); - } + ...common.methods, + getEvents() { + return this.events; }, - async getEvents() { - const events = await this.ironclad._makeRequest({ - method: "GET", - path: "/events?limit=50", - }); - return events; - }, - verifySignature(rawBody, signature) { - const secret = this.ironclad.$auth.webhook_secret; - const computedSignature = crypto.createHmac("sha256", secret).update(rawBody) - .digest("base64"); - return computedSignature === signature; - }, - }, - hooks: { - async deploy() { - const events = await this.getEvents(); - events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); - for (const event of events) { - this.$emit(event.payload, { - id: event.workflowID || event.webhookID, - summary: `New event: ${event.payload.event}`, - ts: new Date(event.timestamp).getTime(), - }); - } - }, - async activate() { - const webhookID = await this.createWebhook(); - await this.db.set("webhookID", webhookID); - }, - async deactivate() { - await this.deleteWebhook(); - await this.db.delete("webhookID"); - }, - }, - async run(event) { - const signature = event.headers["X-Ironclad-Signature"]; - const rawBody = event.rawBody; - if (!this.verifySignature(rawBody, signature)) { - return this.http.respond({ - status: 401, - body: "Unauthorized", - }); - } - - const payload = event.payload; - if (this.selectedEvent.includes(payload.event) || this.selectedEvent.includes("*")) { - this.$emit(payload, { - id: payload.workflowID || payload.webhookID, - summary: `New event: ${payload.event}`, - ts: new Date(payload.timestamp).getTime() || Date.now(), - }); - } - this.http.respond({ - status: 200, - body: "OK", - }); }, + 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 From 666f736a81e104fd1fade0fae8b80bc209fcf6d8 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 10 Dec 2024 14:36:18 -0500 Subject: [PATCH 5/7] updates --- .../actions/create-record/create-record.mjs | 1 + .../launch-workflow/launch-workflow.mjs | 55 +++++++++++---- .../update-workflow/update-workflow.mjs | 68 ++++++++++++++----- components/ironclad/common/utils.mjs | 64 +++++++++++++++++ 4 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 components/ironclad/common/utils.mjs diff --git a/components/ironclad/actions/create-record/create-record.mjs b/components/ironclad/actions/create-record/create-record.mjs index cf81bbef45da8..e6b3a19ba4571 100644 --- a/components/ironclad/actions/create-record/create-record.mjs +++ b/components/ironclad/actions/create-record/create-record.mjs @@ -65,6 +65,7 @@ export default { ? "boolean" : "string", label: properties[property].displayName, + description: properties[property].description ?? `Value of ${properties[property].displayName}`, }; } return props; diff --git a/components/ironclad/actions/launch-workflow/launch-workflow.mjs b/components/ironclad/actions/launch-workflow/launch-workflow.mjs index dcdb5c9c59c03..2b98060d22610 100644 --- a/components/ironclad/actions/launch-workflow/launch-workflow.mjs +++ b/components/ironclad/actions/launch-workflow/launch-workflow.mjs @@ -1,4 +1,8 @@ import ironclad from "../../ironclad.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { + getAttributeDescription, parseValue, +} from "../../common/utils.mjs"; export default { key: "ironclad-launch-workflow", @@ -28,7 +32,7 @@ export default { key, value, ] of Object.entries(schema)) { - if (!value.readOnly && value?.type !== "document" && value?.elementType?.type !== "document") { + if (!value.readOnly) { props[key] = { type: value.type === "boolean" ? "boolean" @@ -36,7 +40,8 @@ export default { ? "string[]" : "string", label: value.displayName, - optional: !(key === "counterpartyName"), + description: getAttributeDescription(value), + optional: (!(key === "counterpartyName") && !value.displayName.toLowerCase().includes("required")), }; if (key === "paperSource") { props[key].options = [ @@ -44,6 +49,11 @@ export default { "Our paper", ]; } + if (key === "recordType") { + const { recordTypes } = await this.ironclad.getRecordsSchema(); + props[key].options = Object.values(recordTypes) + .map((recordType) => recordType.displayName); + } } } return props; @@ -55,16 +65,37 @@ export default { ...attributes } = this; - const response = await ironclad.launchWorkflow({ - $, - data: { - template: templateId, - attributes: { - ...attributes, + const parsedAttributes = {}; + for (const [ + key, + value, + ] of Object.entries(attributes)) { + parsedAttributes[key] = parseValue(value); + } + + try { + const response = await ironclad.launchWorkflow({ + $, + params: { + useDefaultValues: true, }, - }, - }); - $.export("$summary", `Workflow launched successfully with ID ${response.id}`); - return response; + data: { + template: templateId, + attributes: parsedAttributes, + }, + }); + $.export("$summary", `Workflow launched successfully with ID ${response.id}`); + return response; + } catch (error) { + const msg = JSON.parse(error.message); + if (msg.code === "MISSING_PARAM") { + const { schema } = await this.ironclad.getWorkflowSchema({ + templateId: this.templateId, + }); + const paramNames = (JSON.parse(msg.param)).map((p) => `\`${schema[p].displayName}\``); + throw new ConfigurationError(`Please enter or update the following required parameters: ${paramNames.join(", ")}`); + } + 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 index 3a3a151b230bc..8a23298011246 100644 --- a/components/ironclad/actions/update-workflow/update-workflow.mjs +++ b/components/ironclad/actions/update-workflow/update-workflow.mjs @@ -1,4 +1,8 @@ import ironclad from "../../ironclad.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { + getAttributeDescription, parseValue, +} from "../../common/utils.mjs"; export default { key: "ironclad-update-workflow", @@ -34,7 +38,7 @@ export default { key, value, ] of Object.entries(schema)) { - if (!value?.readOnly && value?.type !== "document" && value?.elementType?.type !== "document") { + if (!value?.readOnly) { props[key] = { type: value.type === "boolean" ? "boolean" @@ -42,8 +46,15 @@ export default { ? "string[]" : "string", label: value.displayName, + description: getAttributeDescription(value), optional: true, }; + if (key === "paperSource") { + props[key].options = [ + "Counterparty paper", + "Our paper", + ]; + } } } return props; @@ -55,22 +66,43 @@ export default { comment, ...attributes } = this; - const response = await ironclad.updateWorkflowMetadata({ - $, - workflowId: workflowId, - data: { - updates: attributes && Object.entries(attributes).map(([ - key, - value, - ]) => ({ - action: "set", - path: key, - value, - })), - comment: comment, - }, - }); - $.export("$summary", `Workflow ${workflowId} updated successfully`); - return response; + + 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); + if (msg.code === "MISSING_PARAM") { + const { schema } = await this.ironclad.getWorkflowSchema({ + templateId: this.templateId, + }); + const paramNames = (JSON.parse(msg.param)).map((p) => `\`${schema[p].displayName}\``); + throw new ConfigurationError(`Please enter or update the following required parameters: ${paramNames.join(", ")}`); + } + 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; + } +} From 007d960d4d6c0bd991f7351cd533fba7a2cea42e Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 10 Dec 2024 14:40:24 -0500 Subject: [PATCH 6/7] pnpm-lock.yaml --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b8734259e197..b3db4135b8282 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24484,22 +24484,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} From 643edb5325b08349c3065c4529fd77436f88e468 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Wed, 11 Dec 2024 11:10:09 -0500 Subject: [PATCH 7/7] add invalid_param error handling --- .../actions/launch-workflow/launch-workflow.mjs | 10 +++++++--- .../actions/update-workflow/update-workflow.mjs | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/components/ironclad/actions/launch-workflow/launch-workflow.mjs b/components/ironclad/actions/launch-workflow/launch-workflow.mjs index 2b98060d22610..f8f7c1d8b239b 100644 --- a/components/ironclad/actions/launch-workflow/launch-workflow.mjs +++ b/components/ironclad/actions/launch-workflow/launch-workflow.mjs @@ -88,13 +88,17 @@ export default { return response; } catch (error) { const msg = JSON.parse(error.message); + const { schema } = await ironclad.getWorkflowSchema({ + templateId, + }); if (msg.code === "MISSING_PARAM") { - const { schema } = await this.ironclad.getWorkflowSchema({ - templateId: this.templateId, - }); 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 index 8a23298011246..4c3a222736541 100644 --- a/components/ironclad/actions/update-workflow/update-workflow.mjs +++ b/components/ironclad/actions/update-workflow/update-workflow.mjs @@ -95,13 +95,17 @@ export default { return response; } catch (error) { const msg = JSON.parse(error.message); + const { schema } = await ironclad.getWorkflow({ + workflowId, + }); if (msg.code === "MISSING_PARAM") { - const { schema } = await this.ironclad.getWorkflowSchema({ - templateId: this.templateId, - }); 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); } },