diff --git a/components/specific/actions/create-conversation/create-conversation.mjs b/components/specific/actions/create-conversation/create-conversation.mjs new file mode 100644 index 0000000000000..152ca87f35439 --- /dev/null +++ b/components/specific/actions/create-conversation/create-conversation.mjs @@ -0,0 +1,167 @@ +import specific from "../../specific.app.mjs"; + +export default { + key: "specific-create-conversation", + name: "Create Conversation", + description: "Create a new conversation. [See the documentation](https://public-api.specific.app/docs/mutations/createConversation)", + version: "0.0.1", + type: "action", + props: { + specific, + content: { + type: "string", + label: "Content", + description: "Conversation content as String or ProseMirror document.", + reloadProps: true, + }, + insertedAt: { + propDefinition: [ + specific, + "insertedAt", + ], + optional: true, + }, + assignee: { + type: "string", + label: "Assignee", + description: "The user's email.", + optional: true, + }, + sourceId: { + propDefinition: [ + specific, + "sourceId", + ], + optional: true, + }, + companyId: { + propDefinition: [ + specific, + "companyId", + ], + optional: true, + }, + contactId: { + propDefinition: [ + specific, + "contactId", + ], + optional: true, + }, + sourceUrl: { + type: "string", + label: "Source URL", + description: "Source url where the conversation was gathered.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.content) { + const { data: { customFields } } = await this.specific.query({ + model: "customFields", + where: "{type: {equals: conversation }}", + fields: "name", + }); + for (const { name } of customFields) { + props[`customField-${name}`] = { + type: "string", + label: name, + description: `Custom Field: ${name}`, + optional: true, + }; + } + } + return props; + }, + async run({ $ }) { + const { + specific, + ...data + } = this; + + const customFields = this.specific.parseCustomFields(data); + + const response = await specific.mutation({ + $, + model: "createConversation", + data: `{ + ${this.assignee + ? `assignee: { + connectOrIgnore: { + email: "${this.assignee}" + } + }` + : ""} + ${this.companyId + ? `company: { + connect: { + id: "${this.companyId}" + } + }` + : ""} + ${this.contactId + ? `contact: { + connect: { + id: "${this.contactId}" + } + }` + : ""} + content: "${this.content}" + ${customFields + ? `customFields: ${customFields}` + : ""} + ${this.insertedAt + ? `insertedAt: "${this.insertedAt}"` + : ""} + ${this.sourceId + ? `source: { + connect: { + id: "${this.sourceId}" + } + }` + : ""} + ${this.sourceUrl + ? `sourceUrl: "${this.sourceUrl}"` + : ""} + }`, + fields: ` + customFields + id + insertedAt + name + plainText + sourceUrl + assignee { + email + fullName + id + } + company { + contactsCount + customFields + id + name + visitorId + } + contact { + customFields + email + id + name + visitorId + } + source { + id + name + }`, + on: "Conversation", + }); + + if (response.errors) throw new Error(response.errors[0].message); + + $.export("$summary", `Successfully created conversation for user ID: ${response.data?.createConversation?.id}`); + return response; + }, +}; + diff --git a/components/specific/actions/find-company/find-company.mjs b/components/specific/actions/find-company/find-company.mjs new file mode 100644 index 0000000000000..0aacbc19bce04 --- /dev/null +++ b/components/specific/actions/find-company/find-company.mjs @@ -0,0 +1,34 @@ +import specific from "../../specific.app.mjs"; + +export default { + key: "specific-find-company", + name: "Find Company", + description: "Retrieve details of a specified company. [See the documentation](https://public-api.specific.app/docs/types/Query)", + version: "0.0.1", + type: "action", + props: { + specific, + companyId: { + propDefinition: [ + specific, + "companyId", + ], + }, + }, + async run({ $ }) { + const response = await this.specific.query({ + $, + model: "companies", + where: `{id: {equals: "${this.companyId}"}}`, + fields: `id + name + customFields + visitorId + contactsCount`, + on: "Company", + }); + + $.export("$summary", `Successfully retrieved details for company ID: ${this.companyId}`); + return response; + }, +}; diff --git a/components/specific/actions/update-create-contact/update-create-contact.mjs b/components/specific/actions/update-create-contact/update-create-contact.mjs new file mode 100644 index 0000000000000..78bdce87db262 --- /dev/null +++ b/components/specific/actions/update-create-contact/update-create-contact.mjs @@ -0,0 +1,111 @@ +import specific from "../../specific.app.mjs"; + +export default { + key: "specific-update-create-contact", + name: "Update or Create Contact", + description: "Modify an existing contact's details or create a new one if the specified contact does not exist. [See the documentation](https://public-api.specific.app/docs/types/contact)", + version: "0.0.1", + type: "action", + props: { + specific, + contactEmail: { + propDefinition: [ + specific, + "contactEmail", + ], + reloadProps: true, + }, + companyId: { + propDefinition: [ + specific, + "companyId", + ], + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Contact's name.", + optional: true, + }, + email: { + type: "string", + label: "New Email", + description: "New email to update", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.content) { + const { data: { customFields } } = await this.specific.query({ + model: "customFields", + where: "{type: {equals: contact }}", + fields: "name", + }); + for (const { name } of customFields) { + props[`customField-${name}`] = { + type: "string", + label: name, + description: `Custom Field: ${name}`, + optional: true, + }; + } + } + return props; + }, + async run({ $ }) { + const { + specific, + ...data + } = this; + + const customFields = this.specific.parseCustomFields(data); + + const response = await specific.mutation({ + $, + model: "createOrUpdateContact", + on: "CreatedOrUpdatedContacts", + data: `{ + ${this.companyId + ? `company: { + connect: { + id: "${this.companyId}" + } + }` + : ""} + ${customFields + ? `customFields: ${customFields}` + : ""} + ${this.email + ? `email: "${this.email}"` + : ""} + ${this.name + ? `name: "${this.name}"` + : ""} + }`, + where: `{email: "${this.contactEmail}"}`, + fields: ` + contacts { + id + name + email + visitorId + customFields + company { + contactsCount + customFields + id + name + visitorId + } + }`, + }); + + if (response.errors) throw new Error(response.errors[0].message); + + $.export("$summary", `Successfully updated or created contact with email ${this.contactEmail}`); + return response; + }, +}; + diff --git a/components/specific/common/utils.mjs b/components/specific/common/utils.mjs new file mode 100644 index 0000000000000..d0af679ba89cc --- /dev/null +++ b/components/specific/common/utils.mjs @@ -0,0 +1,8 @@ +export const stringifyObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return JSON.stringify(obj); + } + return obj; +}; diff --git a/components/specific/package.json b/components/specific/package.json index 809e213b677d9..d5a4503190f70 100644 --- a/components/specific/package.json +++ b/components/specific/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/specific", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Specific Components", "main": "specific.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" } -} \ No newline at end of file +} + diff --git a/components/specific/sources/common/base.mjs b/components/specific/sources/common/base.mjs new file mode 100644 index 0000000000000..a50e5e820c6e6 --- /dev/null +++ b/components/specific/sources/common/base.mjs @@ -0,0 +1,82 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { stringifyObject } from "../../common/utils.mjs"; +import specific from "../../specific.app.mjs"; + +export default { + props: { + specific, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + code: { + type: "string", + label: "Code", + description: "Webhook's secret code", + secret: true, + }, + sourceId: { + propDefinition: [ + specific, + "sourceId", + ], + type: "string[]", + optional: true, + }, + }, + hooks: { + async activate() { + const { data } = await this.specific.mutation({ + model: "subscribeWebhook", + data: `{ + code: "${this.code}" + operation: ["${this.getOperation()}"] + ${this.sourceId?.length + ? `sources: ${stringifyObject(this.sourceId)}` + : ""} + url: "${this.http.endpoint}" + }`, + fields: ` + inactive + inactiveReason + operation + url + `, + on: "Webhook", + onValidationError: true, + }); + + if (data?.subscribeWebhook?.fieldErrors?.length) { + throw new ConfigurationError(data.subscribeWebhook?.fieldErrors[0].message); + } + + }, + async deactivate() { + await this.specific.mutation({ + model: "unsubscribeWebhook", + where: `{url: "${this.http.endpoint}"}`, + fields: ` + inactive + inactiveReason + operation + url + `, + on: "Webhook", + }); + + }, + }, + async run({ + headers, body, + }) { + if (headers["x-code"] != this.code) return; + + const ts = Date.parse(body.insertedAt || new Date()); + this.$emit(body, { + id: `${body.id || body.workspaceId}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/specific/sources/new-contact-instant/new-contact-instant.mjs b/components/specific/sources/new-contact-instant/new-contact-instant.mjs new file mode 100644 index 0000000000000..1bfe57c155ca5 --- /dev/null +++ b/components/specific/sources/new-contact-instant/new-contact-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "specific-new-contact-instant", + name: "New Contact Created (Instant)", + description: "Emit new event whenever a new contact is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getOperation() { + return "new-contact"; + }, + getSummary(body) { + return `New contact created: ${body.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/specific/sources/new-contact-instant/test-event.mjs b/components/specific/sources/new-contact-instant/test-event.mjs new file mode 100644 index 0000000000000..d2d4989e32463 --- /dev/null +++ b/components/specific/sources/new-contact-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "id": "specific_cnC1e5fe56v53o", + "email": "user@specific.app", + "name": "User Name", + "custom_fields": {}, + "workspace_id": "12345678-134-134-1234-1234567890" +} \ No newline at end of file diff --git a/components/specific/sources/new-conversation-instant/new-conversation-instant.mjs b/components/specific/sources/new-conversation-instant/new-conversation-instant.mjs new file mode 100644 index 0000000000000..2b002a1816557 --- /dev/null +++ b/components/specific/sources/new-conversation-instant/new-conversation-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "specific-new-conversation-instant", + name: "New Conversation Instant", + description: "Emit new event whenever a new conversation is initiated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getOperation() { + return "new-conversation"; + }, + getSummary(body) { + return `New conversation initiated: ${body.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/specific/sources/new-conversation-instant/test-event.mjs b/components/specific/sources/new-conversation-instant/test-event.mjs new file mode 100644 index 0000000000000..4569ed2d32bde --- /dev/null +++ b/components/specific/sources/new-conversation-instant/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "text": "Conversation Text\n", + "name": "Conversation Name", + "customFields": { + "callId": "1234", + "release": "12" + }, + "assignee": null, + "company": { + "id": "specific_cnC1e5fe56v53o", + "name": "Secret Intelligence Service", + "customFields": {} + }, + "contact": { + "id": "specific_cnC1e5fe56v53o", + "name": "James Bond", + "email": "james@cia.gov", + "customFields": {} + }, + "source": null, + "insertedAt": "2024-07-19T20:28:17", + "sourceUrl": "htpps://urltest.com", + "workspaceId": "12345678-134-134-1234-1234567890" +} \ No newline at end of file diff --git a/components/specific/specific.app.mjs b/components/specific/specific.app.mjs index 44b7e3f4254b7..975bef2ac8c9a 100644 --- a/components/specific/specific.app.mjs +++ b/components/specific/specific.app.mjs @@ -1,11 +1,166 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "specific", - propDefinitions: {}, + propDefinitions: { + customFields: { + type: "string", + label: "customFields", + description: "customFields", + }, + insertedAt: { + type: "string", + label: "Inserted At", + description: "Date and time when the conversation was inserted in format YYYY-MM-DDTHH:mm:ss.sssZ", + default: (new Date).toISOString(), + }, + sourceId: { + type: "string", + label: "Source Id", + description: "The Id of the source associated with the conversation", + async options() { + return await this.listAsyncOptions({ + model: "sources", + }); + }, + }, + contactId: { + type: "string", + label: "Contact Id", + description: "The id of the contact associated with the conversation", + async options() { + return await this.listAsyncOptions({ + model: "contacts", + }); + }, + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email of the contact", + async options() { + return await this.listAsyncOptions({ + model: "contacts", + fields: ` + name + email`, + }); + }, + }, + companyId: { + type: "string", + label: "Company ID", + description: "The ID of the company to retrieve details for", + async options() { + return await this.listAsyncOptions({ + model: "companies", + }); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://public-api.specific.app/graphql"; + }, + _headers() { + return { + Authorization: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + query({ + model, where, fields, + }) { + return this._makeRequest({ + data: { + query: `query { + ${model} ${where + ? `(where: ${where})` + : ""} { + ${fields} + } + }`, + }, + }); + }, + mutation({ + model, data = null, fields, on, where, onValidationError = false, + }) { + return this._makeRequest({ + data: { + query: `mutation { + ${model} ( + ${data + ? `data: ${data}` + : ""} ${where + ? `where: ${where}` + : ""}) { + ...on ${on || model} { + ${fields} + } + ...on DbError { + message + } + ...on GraphQLError { + message + } + ${onValidationError + ? ` + ...on ValidationError { + message + fieldErrors { + message + path + } + }` + : ""} + } + }`, + }, + }); + }, + async listAsyncOptions ({ + model, fields = ` + id + name`, + }) { + const { data } = await this.query({ + model, + fields, + }); + + return data + ? data[model]?.map(({ + id, email, name: label, + }) => ({ + label, + value: id || email, + })) + : []; + }, + parseCustomFields(data) { + let customFields = Object.keys(data) + .filter((key) => key.startsWith("customField-")) + .reduce((res, key) => { + res[key.substring(12)] = data[key]; + return res; + }, {}); + + return JSON.stringify(customFields) + .replace(/"([^"]+)":/g, ` + $1:`) + .replace(/,/g, ` + `); }, }, -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55908b64e245b..f696921f7d9f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8682,7 +8682,10 @@ importers: specifiers: {} components/specific: - specifiers: {} + specifiers: + '@pipedream/platform': ^3.0.0 + dependencies: + '@pipedream/platform': 3.0.0 components/speechace: specifiers: