diff --git a/bun.lock b/bun.lock index 9879aacaecfb..e12f596f80bd 100644 --- a/bun.lock +++ b/bun.lock @@ -3844,7 +3844,7 @@ }, "packages/pieces/community/google-my-business": { "name": "@activepieces/piece-google-my-business", - "version": "0.1.9", + "version": "0.2.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", @@ -6925,7 +6925,7 @@ }, "packages/pieces/community/pipedrive": { "name": "@activepieces/piece-pipedrive", - "version": "0.8.10", + "version": "0.8.11", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", @@ -8012,7 +8012,7 @@ }, "packages/pieces/community/sendinblue": { "name": "@activepieces/piece-sendinblue", - "version": "0.2.9", + "version": "0.3.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", diff --git a/docs/install/reference/breaking-changes.mdx b/docs/install/reference/breaking-changes.mdx index 0a566df3436b..fda3d92d5d9f 100644 --- a/docs/install/reference/breaking-changes.mdx +++ b/docs/install/reference/breaking-changes.mdx @@ -8,6 +8,22 @@ icon: "hammer" ### What has changed? +#### Brevo's Create or Update Contact stops clearing blacklist flags and attributes it was not given + +Four changes to the Brevo (formerly Sendinblue) piece's Create or Update Contact action alter what an existing, unchanged step does. + +The action used to strip every falsy value from the request before sending it, so `Email Blacklisted` and `SMS Blacklisted` could be switched on but never switched back off — the `false` was dropped and the contact stayed blacklisted with no error. Those flags are now sent when you set them, and neither checkbox defaults to off any more, so a checkbox you never touched is omitted from the request instead of quietly un-blacklisting the contact on every run. + +The `Attributes` field used to come pre-filled with nine empty values (`FIRST_NAME`, `LAST_NAME`, `SMS`, `CIV`, `DOB`, `ADDRESS`, `ZIP_CODE`, `CITY`, `AREA`). Because they were sent on every call, running the step overwrote whatever those attributes held in Brevo with empty strings. The default is removed, and only the attributes you list are written. + +`List IDs` was a free-text list of numbers and is now a dropdown of the lists in your account. A stored numeric id is still sent exactly as before, so existing steps keep working unchanged. + +The `SMTP Blacklist Sender` checkbox is replaced by a `Blocked Sender Addresses` list. Brevo expects a list of sender email addresses here, not a true or false, and rejects a boolean outright with *"Invalid smtpBlacklistSender format"* — so ticking that checkbox always failed the step, and leaving it clear did nothing at all. The field never worked. It is now a list of addresses, which Brevo accepts, and any value your step already had is ignored rather than sent. + +#### What you need to do + +Nothing to configure or migrate, and no existing step stops working. Review any flow whose Create or Update Contact step relied on the old behaviour: a step that was silently wiping attributes will now leave them intact, and a step you were relying on to blacklist contacts must have the checkbox explicitly ticked. To block a sender for a contact, list that sender's address in `Blocked Sender Addresses` — it must be an active sender in your Brevo account, or Brevo answers *"One of the sender is invalid or inactive"*. + #### Workers no longer pre-warm the flow cache on startup by default Since v0.86.1 every worker pre-filled its local piece and code cache on startup by resolving and compiling every enabled flow on the platform. That warm-up costs memory and CPU proportional to the number of enabled flows: on instances with many flows it pinned each worker at its CPU limit for the duration and spiked memory enough to OOM-kill small workers, especially during upgrades when all workers restart at once. The warm-up is now opt-in behind the new `AP_PREWARM_CACHE_ON_STARTUP` worker environment variable, which defaults to `false`. When disabled, caches fill lazily on each flow's first run after a worker starts, exactly as they did before v0.86.1. diff --git a/packages/core/execution/src/lib/flows/flow-version.ts b/packages/core/execution/src/lib/flows/flow-version.ts index 2a9f78f6634e..4dae2d9daae3 100755 --- a/packages/core/execution/src/lib/flows/flow-version.ts +++ b/packages/core/execution/src/lib/flows/flow-version.ts @@ -4,7 +4,7 @@ import { UserWithMetaInformation } from '@activepieces/core-piece-types' import { Note } from './note' import { FlowTrigger } from './triggers/trigger' -export const LATEST_FLOW_SCHEMA_VERSION = '23' +export const LATEST_FLOW_SCHEMA_VERSION = '24' export enum FlowVersionState { LOCKED = 'LOCKED', diff --git a/packages/pieces/community/google-my-business/package.json b/packages/pieces/community/google-my-business/package.json index be26e598275f..b0b0dc3a0a17 100644 --- a/packages/pieces/community/google-my-business/package.json +++ b/packages/pieces/community/google-my-business/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-google-my-business", - "version": "0.1.9", + "version": "0.2.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/google-my-business/src/index.ts b/packages/pieces/community/google-my-business/src/index.ts index 4b1ffa59d8f4..f27174a65472 100644 --- a/packages/pieces/community/google-my-business/src/index.ts +++ b/packages/pieces/community/google-my-business/src/index.ts @@ -5,7 +5,12 @@ import { createPiece, } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/pieces-framework'; +import { createPost } from './lib/actions/create-post'; import { createReply } from './lib/actions/create-reply'; +import { deletePost } from './lib/actions/delete-post'; +import { getPost } from './lib/actions/get-post'; +import { listPosts } from './lib/actions/list-posts'; +import { updatePost } from './lib/actions/update-post'; import { newReview } from './lib/triggers/new-review'; export const googleAuth = PieceAuth.OAuth2({ @@ -24,10 +29,15 @@ export const googleBusiness = createPiece({ authors: ["kishanprmr","MoShizzle","abuaboud"], categories: [PieceCategory.MARKETING], actions: [ + createPost, + listPosts, + getPost, + updatePost, + deletePost, createReply, createCustomApiCallAction({ baseUrl: () => { - return 'https://www.googleapis.com/business/v4'; + return 'https://mybusiness.googleapis.com/v4'; }, auth: googleAuth, authMapping: async (auth) => ({ diff --git a/packages/pieces/community/google-my-business/src/lib/actions/create-post.ts b/packages/pieces/community/google-my-business/src/lib/actions/create-post.ts new file mode 100644 index 000000000000..d84154a490a0 --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/actions/create-post.ts @@ -0,0 +1,128 @@ +import { HttpMethod, httpClient, propsValidation } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { googleAuth } from '../..'; +import { createPostActionOutputSchema } from '../output-schemas'; +import { googleBusinessCommon } from '../common/common'; +import { localPostUtils } from '../common/local-post'; + +export const createPost = createAction({ + name: 'create-post', + outputSchema: createPostActionOutputSchema, + classification: 'WRITE', + displayName: 'Create Post', + description: 'Creates a post for a specified location.', + audience: 'both', + aiMetadata: { + description: + 'Publishes a local post to a Google Business Profile location, so it appears on the business listing in Search and Maps. Choose the Post Type: Standard is a plain update, Event and Offer both require a title and a start and end date, and Alert requires an alert type. A call to action button is optional, and needs a URL for every action except Call. Not idempotent: each call publishes a separate post.', + idempotent: false, + }, + auth: googleAuth, + props: { + account: googleBusinessCommon.account, + location: googleBusinessCommon.location, + topicType: Property.StaticDropdown({ + displayName: 'Post Type', + description: 'The kind of post to publish.', + required: true, + defaultValue: 'STANDARD', + options: { disabled: false, options: localPostUtils.topicOptions }, + }), + summary: Property.LongText({ + displayName: 'Summary', + description: 'The body text of the post.', + required: true, + }), + languageCode: Property.ShortText({ + displayName: 'Language Code', + description: 'BCP 47 language code of the post text, for example `en` or `en-GB`.', + required: true, + defaultValue: 'en', + }), + scheduledTime: Property.DateTime({ + displayName: 'Publish At', + description: + 'Leave empty to publish immediately. Set a future time to schedule the post, which keeps it off the listing until then.', + required: false, + }), + mediaSourceUrl: Property.ShortText({ + displayName: 'Photo URL', + description: + 'Publicly accessible URL of a photo to attach. Google fetches the image, so it must not require authentication.', + required: false, + }), + callToActionType: Property.StaticDropdown({ + displayName: 'Call To Action', + description: 'Optional button shown on the post.', + required: false, + options: { disabled: false, options: localPostUtils.callToActionOptions }, + }), + callToActionUrl: Property.ShortText({ + displayName: 'Call To Action URL', + description: + 'Where the button links to. Required for every call to action except Call Now, which uses the location phone number.', + required: false, + }), + eventTitle: Property.ShortText({ + displayName: 'Event / Offer Title', + description: 'Required when the post type is Event or Offer.', + required: false, + }), + eventStartDate: Property.ShortText({ + displayName: 'Start Date', + description: 'Required for Event and Offer posts, as `YYYY-MM-DD`.', + required: false, + }), + eventStartTime: Property.ShortText({ + displayName: 'Start Time', + description: + 'Optional time of day as `HH:mm` in 24-hour form. Interpreted in the location time zone, so no offset is sent.', + required: false, + }), + eventEndDate: Property.ShortText({ + displayName: 'End Date', + description: 'Required for Event and Offer posts, as `YYYY-MM-DD`.', + required: false, + }), + eventEndTime: Property.ShortText({ + displayName: 'End Time', + description: 'Optional time of day as `HH:mm` in 24-hour form.', + required: false, + }), + offerCouponCode: Property.ShortText({ + displayName: 'Coupon Code', + required: false, + }), + offerRedeemOnlineUrl: Property.ShortText({ + displayName: 'Redeem Online URL', + required: false, + }), + offerTermsConditions: Property.LongText({ + displayName: 'Terms And Conditions', + required: false, + }), + alertType: Property.StaticDropdown({ + displayName: 'Alert Type', + description: 'Required when the post type is Alert.', + required: false, + options: { disabled: false, options: localPostUtils.alertTypeOptions }, + }), + }, + async run(ctx) { + const { account, location, ...content } = ctx.propsValue; + + await propsValidation.validateZod(ctx.propsValue, localPostUtils.scheduleValidation); + localPostUtils.assertValid(content); + + const response = await httpClient.sendRequest({ + url: `${localPostUtils.baseUrl}/${account}/${location}/localPosts`, + method: HttpMethod.POST, + headers: { + Authorization: `Bearer ${ctx.auth.access_token}`, + }, + body: localPostUtils.buildContent(content), + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/google-my-business/src/lib/actions/create-reply.ts b/packages/pieces/community/google-my-business/src/lib/actions/create-reply.ts index 038cad0f506b..6b20c219a54a 100644 --- a/packages/pieces/community/google-my-business/src/lib/actions/create-reply.ts +++ b/packages/pieces/community/google-my-business/src/lib/actions/create-reply.ts @@ -34,7 +34,7 @@ export const createReply = createAction({ }); const response = await httpClient.sendRequest({ - url: ` https://mybusiness.googleapis.com/v4/${reviewName}/reply`, + url: `https://mybusiness.googleapis.com/v4/${reviewName}/reply`, method: HttpMethod.PUT, headers: { Authorization: `Bearer ${ctx.auth.access_token}`, diff --git a/packages/pieces/community/google-my-business/src/lib/actions/delete-post.ts b/packages/pieces/community/google-my-business/src/lib/actions/delete-post.ts new file mode 100644 index 000000000000..96abedb49a3e --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/actions/delete-post.ts @@ -0,0 +1,44 @@ +import { HttpMethod, httpClient, propsValidation } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import * as z from 'zod/mini'; +import { googleAuth } from '../..'; +import { localPostUtils } from '../common/local-post'; + +export const deletePost = createAction({ + name: 'delete-post', + classification: 'WRITE', + displayName: 'Delete Post', + description: 'Deletes a post from a specified location.', + audience: 'both', + aiMetadata: { + description: + 'Permanently deletes one local post from a Google Business Profile location, identified by its full resource name. The post stops appearing on the listing and cannot be restored. Idempotent in effect: deleting an already deleted post leaves nothing further to remove, though Google answers with an error.', + idempotent: true, + }, + auth: googleAuth, + props: { + postName: Property.ShortText({ + displayName: 'Post Name', + description: + 'Full resource name of the post, as `accounts/{account}/locations/{location}/localPosts/{post}`.', + required: true, + }), + }, + async run(ctx) { + const { postName } = ctx.propsValue; + + await propsValidation.validateZod(ctx.propsValue, { + postName: z.string().check(z.regex(localPostUtils.postNamePattern)), + }); + + await httpClient.sendRequest({ + url: `${localPostUtils.baseUrl}/${postName}`, + method: HttpMethod.DELETE, + headers: { + Authorization: `Bearer ${ctx.auth.access_token}`, + }, + }); + + return { success: true }; + }, +}); diff --git a/packages/pieces/community/google-my-business/src/lib/actions/get-post.ts b/packages/pieces/community/google-my-business/src/lib/actions/get-post.ts new file mode 100644 index 000000000000..d7a2b78dbb2b --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/actions/get-post.ts @@ -0,0 +1,46 @@ +import { HttpMethod, httpClient, propsValidation } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import * as z from 'zod/mini'; +import { googleAuth } from '../..'; +import { getPostActionOutputSchema } from '../output-schemas'; +import { localPostUtils } from '../common/local-post'; + +export const getPost = createAction({ + name: 'get-post', + outputSchema: getPostActionOutputSchema, + classification: 'READ', + displayName: 'Get Post', + description: 'Retrieves a single post by its resource name.', + audience: 'both', + aiMetadata: { + description: + 'Fetches one local post from a Google Business Profile location by its full resource name, in the form accounts/{account}/locations/{location}/localPosts/{post}. Use List Posts or Create Post to obtain that name. Read-only and safe to repeat.', + idempotent: true, + }, + auth: googleAuth, + props: { + postName: Property.ShortText({ + displayName: 'Post Name', + description: + 'Full resource name of the post, as `accounts/{account}/locations/{location}/localPosts/{post}`. List Posts and Create Post both return it as `name`.', + required: true, + }), + }, + async run(ctx) { + const { postName } = ctx.propsValue; + + await propsValidation.validateZod(ctx.propsValue, { + postName: z.string().check(z.regex(localPostUtils.postNamePattern)), + }); + + const response = await httpClient.sendRequest({ + url: `${localPostUtils.baseUrl}/${postName}`, + method: HttpMethod.GET, + headers: { + Authorization: `Bearer ${ctx.auth.access_token}`, + }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/google-my-business/src/lib/actions/list-posts.ts b/packages/pieces/community/google-my-business/src/lib/actions/list-posts.ts new file mode 100644 index 000000000000..03863146ee05 --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/actions/list-posts.ts @@ -0,0 +1,75 @@ +import { HttpMethod, httpClient, propsValidation } from '@activepieces/pieces-common'; +import { createAction, isNil, Property } from '@activepieces/pieces-framework'; +import * as z from 'zod/mini'; +import { googleAuth } from '../..'; +import { listPostsActionOutputSchema } from '../output-schemas'; +import { googleBusinessCommon } from '../common/common'; +import { localPostUtils } from '../common/local-post'; + +export const listPosts = createAction({ + name: 'list-posts', + outputSchema: listPostsActionOutputSchema, + classification: 'READ', + displayName: 'List Posts', + description: 'Lists the posts of a specified location.', + audience: 'both', + aiMetadata: { + description: + 'Returns the local posts published to a Google Business Profile location, newest first, following pagination up to Maximum Results. Use to read existing posts or to find a post name for Get, Update or Delete Post. Read-only and safe to repeat.', + idempotent: true, + }, + auth: googleAuth, + props: { + account: googleBusinessCommon.account, + location: googleBusinessCommon.location, + maxResults: Property.Number({ + displayName: 'Maximum Results', + description: + 'Stop after this many posts. Google returns at most 100 per request, so larger values are fetched over several requests.', + required: false, + defaultValue: 100, + }), + }, + async run(ctx) { + const { account, location, maxResults } = ctx.propsValue; + + await propsValidation.validateZod(ctx.propsValue, { + maxResults: z.optional(z.number().check(z.gte(1))), + }); + + const limit = maxResults ?? DEFAULT_MAX_RESULTS; + const localPosts: unknown[] = []; + let pageToken: string | undefined; + + do { + const remaining = limit - localPosts.length; + const response = await httpClient.sendRequest({ + url: `${localPostUtils.baseUrl}/${account}/${location}/localPosts`, + method: HttpMethod.GET, + headers: { + Authorization: `Bearer ${ctx.auth.access_token}`, + }, + queryParams: { + pageSize: String(Math.min(remaining, MAX_PAGE_SIZE)), + ...(isNil(pageToken) ? {} : { pageToken }), + }, + }); + + localPosts.push(...(response.body.localPosts ?? [])); + pageToken = response.body.nextPageToken; + } while (!isNil(pageToken) && localPosts.length < limit); + + return { + localPosts, + ...(isNil(pageToken) ? {} : { nextPageToken: pageToken }), + }; + }, +}); + +const DEFAULT_MAX_RESULTS = 100; +const MAX_PAGE_SIZE = 100; + +type ListLocalPostsResponse = { + localPosts?: unknown[]; + nextPageToken?: string; +}; diff --git a/packages/pieces/community/google-my-business/src/lib/actions/update-post.ts b/packages/pieces/community/google-my-business/src/lib/actions/update-post.ts new file mode 100644 index 000000000000..e8f1301305cf --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/actions/update-post.ts @@ -0,0 +1,125 @@ +import { HttpMethod, httpClient, propsValidation } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import * as z from 'zod/mini'; +import { googleAuth } from '../..'; +import { updatePostActionOutputSchema } from '../output-schemas'; +import { localPostUtils } from '../common/local-post'; + +export const updatePost = createAction({ + name: 'update-post', + outputSchema: updatePostActionOutputSchema, + classification: 'WRITE', + displayName: 'Update Post', + description: 'Updates a post for a specified location.', + audience: 'both', + aiMetadata: { + description: + 'Updates an existing local post on a Google Business Profile location, identified by its full resource name. Only the fields you set are sent, and Google replaces each of those fields wholesale. Leave a field empty to keep it as it is. Idempotent: sending the same values again leaves the post in the same state.', + idempotent: true, + }, + auth: googleAuth, + props: { + postName: Property.ShortText({ + displayName: 'Post Name', + description: + 'Full resource name of the post, as `accounts/{account}/locations/{location}/localPosts/{post}`.', + required: true, + }), + summary: Property.LongText({ + displayName: 'Summary', + description: 'New body text. Leave empty to keep the current text.', + required: false, + }), + languageCode: Property.ShortText({ + displayName: 'Language Code', + description: 'BCP 47 language code, for example `en`.', + required: false, + }), + scheduledTime: Property.DateTime({ + displayName: 'Publish At', + description: 'Reschedule the post to a future time.', + required: false, + }), + mediaSourceUrl: Property.ShortText({ + displayName: 'Photo URL', + description: + 'Publicly accessible photo URL. Setting this replaces every photo already on the post.', + required: false, + }), + callToActionType: Property.StaticDropdown({ + displayName: 'Call To Action', + required: false, + options: { disabled: false, options: localPostUtils.callToActionOptions }, + }), + callToActionUrl: Property.ShortText({ + displayName: 'Call To Action URL', + description: 'Required whenever a call to action other than Call Now is set.', + required: false, + }), + eventTitle: Property.ShortText({ + displayName: 'Event / Offer Title', + required: false, + }), + eventStartDate: Property.ShortText({ + displayName: 'Start Date', + description: '`YYYY-MM-DD`. Must be given together with an End Date.', + required: false, + }), + eventStartTime: Property.ShortText({ + displayName: 'Start Time', + description: '`HH:mm` in 24-hour form.', + required: false, + }), + eventEndDate: Property.ShortText({ + displayName: 'End Date', + description: '`YYYY-MM-DD`. Must be given together with a Start Date.', + required: false, + }), + eventEndTime: Property.ShortText({ + displayName: 'End Time', + description: '`HH:mm` in 24-hour form.', + required: false, + }), + offerCouponCode: Property.ShortText({ + displayName: 'Coupon Code', + required: false, + }), + offerRedeemOnlineUrl: Property.ShortText({ + displayName: 'Redeem Online URL', + required: false, + }), + offerTermsConditions: Property.LongText({ + displayName: 'Terms And Conditions', + required: false, + }), + }, + async run(ctx) { + const { postName, ...content } = ctx.propsValue; + + await propsValidation.validateZod(ctx.propsValue, { + postName: z.string().check(z.regex(localPostUtils.postNamePattern)), + ...localPostUtils.scheduleValidation, + }); + localPostUtils.assertValid(content); + + const body = localPostUtils.buildContent(content); + const updateMask = Object.keys(body); + if (updateMask.length === 0) { + throw new Error('Set at least one field to update.'); + } + + const response = await httpClient.sendRequest({ + url: `${localPostUtils.baseUrl}/${postName}`, + method: HttpMethod.PATCH, + headers: { + Authorization: `Bearer ${ctx.auth.access_token}`, + }, + queryParams: { + updateMask: updateMask.join(','), + }, + body, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/google-my-business/src/lib/common/local-post.ts b/packages/pieces/community/google-my-business/src/lib/common/local-post.ts new file mode 100644 index 000000000000..8df39ad94b06 --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/common/local-post.ts @@ -0,0 +1,166 @@ +import { isNil } from '@activepieces/pieces-framework'; +import * as z from 'zod/mini'; + +function buildContent({ + topicType, + summary, + languageCode, + scheduledTime, + mediaSourceUrl, + callToActionType, + callToActionUrl, + eventTitle, + eventStartDate, + eventStartTime, + eventEndDate, + eventEndTime, + offerCouponCode, + offerRedeemOnlineUrl, + offerTermsConditions, + alertType, +}: LocalPostContent): Record { + const body: Record = {}; + + if (!isNil(topicType)) body['topicType'] = topicType; + if (!isNil(summary)) body['summary'] = summary; + if (!isNil(languageCode)) body['languageCode'] = languageCode; + if (!isNil(scheduledTime)) body['scheduledTime'] = scheduledTime; + + if (!isNil(callToActionType)) { + body['callToAction'] = + callToActionType === 'CALL' + ? { actionType: callToActionType } + : { actionType: callToActionType, url: callToActionUrl }; + } + + if (!isNil(mediaSourceUrl)) { + body['media'] = [{ mediaFormat: 'PHOTO', sourceUrl: mediaSourceUrl }]; + } + + const hasSchedule = !isNil(eventStartDate) && !isNil(eventEndDate); + if (!isNil(eventTitle) || hasSchedule) { + body['event'] = { + ...(isNil(eventTitle) ? {} : { title: eventTitle }), + ...(hasSchedule + ? { + schedule: { + startDate: toGoogleDate(eventStartDate), + ...(isNil(eventStartTime) ? {} : { startTime: toGoogleTime(eventStartTime) }), + endDate: toGoogleDate(eventEndDate), + ...(isNil(eventEndTime) ? {} : { endTime: toGoogleTime(eventEndTime) }), + }, + } + : {}), + }; + } + + const offer = { + ...(isNil(offerCouponCode) ? {} : { couponCode: offerCouponCode }), + ...(isNil(offerRedeemOnlineUrl) ? {} : { redeemOnlineUrl: offerRedeemOnlineUrl }), + ...(isNil(offerTermsConditions) ? {} : { termsConditions: offerTermsConditions }), + }; + if (Object.keys(offer).length > 0) { + body['offer'] = offer; + } + + if (!isNil(alertType)) body['alertType'] = alertType; + + return body; +} + +function assertValid({ + topicType, + eventTitle, + eventStartDate, + eventEndDate, + callToActionType, + callToActionUrl, + alertType, +}: LocalPostContent): void { + const needsSchedule = topicType === 'EVENT' || topicType === 'OFFER'; + if (needsSchedule && (isNil(eventTitle) || isNil(eventStartDate) || isNil(eventEndDate))) { + throw new Error( + 'Event and Offer posts require an Event / Offer Title, a Start Date and an End Date.', + ); + } + if (topicType === 'ALERT' && isNil(alertType)) { + throw new Error('Alert posts require an Alert Type.'); + } + if (!isNil(callToActionType) && callToActionType !== 'CALL' && isNil(callToActionUrl)) { + throw new Error('A Call To Action URL is required for every call to action except Call Now.'); + } + if (!isNil(eventStartDate) !== !isNil(eventEndDate)) { + throw new Error('A Start Date and an End Date must be given together.'); + } +} + +function toGoogleDate(value: string): GoogleDate { + const [year, month, day] = value.split('-').map(Number); + return { year, month, day }; +} + +function toGoogleTime(value: string): GoogleTimeOfDay { + const [hours, minutes] = value.split(':').map(Number); + return { hours, minutes }; +} + +const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; +const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/; + +export const localPostUtils = { + buildContent, + assertValid, + postNamePattern: /^accounts\/[^/]+\/locations\/[^/]+\/localPosts\/[^/]+$/, + scheduleValidation: { + eventStartDate: z.optional(z.string().check(z.regex(DATE_PATTERN))), + eventEndDate: z.optional(z.string().check(z.regex(DATE_PATTERN))), + eventStartTime: z.optional(z.string().check(z.regex(TIME_PATTERN))), + eventEndTime: z.optional(z.string().check(z.regex(TIME_PATTERN))), + }, + topicOptions: [ + { label: 'Standard', value: 'STANDARD' }, + { label: 'Event', value: 'EVENT' }, + { label: 'Offer', value: 'OFFER' }, + { label: 'Alert', value: 'ALERT' }, + ], + callToActionOptions: [ + { label: 'Book', value: 'BOOK' }, + { label: 'Order Online', value: 'ORDER' }, + { label: 'Shop', value: 'SHOP' }, + { label: 'Learn More', value: 'LEARN_MORE' }, + { label: 'Sign Up', value: 'SIGN_UP' }, + { label: 'Call Now', value: 'CALL' }, + ], + alertTypeOptions: [{ label: 'COVID-19', value: 'COVID_19' }], + baseUrl: 'https://mybusiness.googleapis.com/v4', +}; + +export type LocalPostContent = { + topicType?: string; + summary?: string; + languageCode?: string; + scheduledTime?: string; + mediaSourceUrl?: string; + callToActionType?: string; + callToActionUrl?: string; + eventTitle?: string; + eventStartDate?: string; + eventStartTime?: string; + eventEndDate?: string; + eventEndTime?: string; + offerCouponCode?: string; + offerRedeemOnlineUrl?: string; + offerTermsConditions?: string; + alertType?: string; +}; + +type GoogleDate = { + year: number; + month: number; + day: number; +}; + +type GoogleTimeOfDay = { + hours: number; + minutes: number; +}; diff --git a/packages/pieces/community/google-my-business/src/lib/output-schemas.ts b/packages/pieces/community/google-my-business/src/lib/output-schemas.ts new file mode 100644 index 000000000000..bb18579d60f0 --- /dev/null +++ b/packages/pieces/community/google-my-business/src/lib/output-schemas.ts @@ -0,0 +1,69 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +const googleDateFields: OutputSchema['fields'] = [ + { key: 'year', label: 'Year', format: 'number' }, + { key: 'month', label: 'Month', format: 'number' }, + { key: 'day', label: 'Day', format: 'number' }, +]; + +const googleTimeFields: OutputSchema['fields'] = [ + { key: 'hours', label: 'Hours', format: 'number' }, + { key: 'minutes', label: 'Minutes', format: 'number' }, +]; + +const localPostFields: OutputSchema['fields'] = [ + { key: 'name', label: 'Post Name' }, + { key: 'summary', label: 'Summary' }, + { key: 'topicType', label: 'Post Type' }, + { key: 'alertType', label: 'Alert Type' }, + { key: 'state', label: 'State' }, + { key: 'languageCode', label: 'Language Code' }, + { key: 'scheduledTime', label: 'Publish At', format: 'datetime' }, + { key: 'createTime', label: 'Created At', format: 'datetime' }, + { key: 'updateTime', label: 'Updated At', format: 'datetime' }, + { + key: 'callToAction', + label: 'Call To Action', + children: [ + { key: 'actionType', label: 'Action Type' }, + { key: 'url', label: 'URL', format: 'url' }, + ], + }, + { + key: 'event', + label: 'Event', + children: [ + { key: 'title', label: 'Title' }, + { + key: 'schedule', + label: 'Schedule', + children: [ + { key: 'startDate', label: 'Start Date', children: googleDateFields }, + { key: 'startTime', label: 'Start Time', children: googleTimeFields }, + { key: 'endDate', label: 'End Date', children: googleDateFields }, + { key: 'endTime', label: 'End Time', children: googleTimeFields }, + ], + }, + ], + }, + { + key: 'media', + label: 'Media', + labelKey: 'name', + listItems: [ + { key: 'name', label: 'Media Name' }, + { key: 'mediaFormat', label: 'Media Format' }, + { key: 'googleUrl', label: 'Google URL', format: 'url' }, + ], + }, +]; + +export const createPostActionOutputSchema: OutputSchema = { fields: localPostFields }; +export const getPostActionOutputSchema: OutputSchema = { fields: localPostFields }; +export const updatePostActionOutputSchema: OutputSchema = { fields: localPostFields }; +export const listPostsActionOutputSchema: OutputSchema = { + fields: [ + { key: 'localPosts', label: 'Posts', labelKey: 'summary', listItems: localPostFields }, + { key: 'nextPageToken', label: 'Next Page Token' }, + ], +}; diff --git a/packages/pieces/community/google-my-business/src/lib/triggers/new-review.ts b/packages/pieces/community/google-my-business/src/lib/triggers/new-review.ts index d0d322022954..fe027c12791a 100644 --- a/packages/pieces/community/google-my-business/src/lib/triggers/new-review.ts +++ b/packages/pieces/community/google-my-business/src/lib/triggers/new-review.ts @@ -78,7 +78,7 @@ const getResponse = async ( const response = await httpClient.sendRequest<{ reviews: { createTime: string }[]; }>({ - url: ` https://mybusiness.googleapis.com/v4/${account}/${location}/reviews`, + url: `https://mybusiness.googleapis.com/v4/${account}/${location}/reviews`, method: HttpMethod.GET, headers: { Authorization: `Bearer ${authentication.access_token}`, diff --git a/packages/pieces/community/pipedrive/package.json b/packages/pieces/community/pipedrive/package.json index 53e208e3de2a..d669f60377d2 100644 --- a/packages/pieces/community/pipedrive/package.json +++ b/packages/pieces/community/pipedrive/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-pipedrive", - "version": "0.8.10", + "version": "0.8.11", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/pipedrive/src/lib/actions/add-follower.ts b/packages/pieces/community/pipedrive/src/lib/actions/add-follower.ts index aec5fd90694d..ae4031cd3e96 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/add-follower.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/add-follower.ts @@ -3,10 +3,12 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { ownerIdProp } from '../common/props'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; +import { addFollowerActionOutputSchema } from '../output-schemas'; export const addFollowerAction = createAction({ auth: pipedriveAuth, name: 'add-follower', + outputSchema: addFollowerActionOutputSchema, classification: 'WRITE', displayName: 'Add Follower', description: 'Adds a follower to a deal, person, organization or product.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/add-label-to-person.ts b/packages/pieces/community/pipedrive/src/lib/actions/add-label-to-person.ts index 2cdfea509d96..ce3954a7fe14 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/add-label-to-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/add-label-to-person.ts @@ -8,10 +8,12 @@ import { } from '../common'; import { GetField, GetPersonResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; +import { addLabelsToPersonActionOutputSchema } from '../output-schemas'; export const addLabelToPersonAction = createAction({ auth: pipedriveAuth, name: 'add-labels-to-person', + outputSchema: addLabelsToPersonActionOutputSchema, classification: 'WRITE', displayName: 'Add Labels to Person', description: 'Adds existing labels to an existing person.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/add-product-to-deal.ts b/packages/pieces/community/pipedrive/src/lib/actions/add-product-to-deal.ts index 8817a166e2e2..ea35870592a8 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/add-product-to-deal.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/add-product-to-deal.ts @@ -3,10 +3,12 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { dealIdProp, productIdProp } from '../common/props'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; +import { addProductToDealActionOutputSchema } from '../output-schemas'; export const addProductToDealAction = createAction({ auth: pipedriveAuth, name: 'add-product-to-deal', + outputSchema: addProductToDealActionOutputSchema, classification: 'WRITE', displayName: 'Add Product to Deal', description: 'Adds a product to a deal.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/attach-file.ts b/packages/pieces/community/pipedrive/src/lib/actions/attach-file.ts index e8a0985bbf73..d3927975a139 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/attach-file.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/attach-file.ts @@ -3,10 +3,12 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { dealIdProp, organizationIdProp, personIdProp, productIdProp } from '../common/props'; import FormData from 'form-data'; import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { attachFileActionOutputSchema } from '../output-schemas'; export const attachFileAction = createAction({ auth: pipedriveAuth, name: 'attach-file', + outputSchema: attachFileActionOutputSchema, classification: 'WRITE', displayName: 'Attach File', description: 'Uploads a file and attaches it to a deal,person,organization,activity or product.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-activity.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-activity.ts index d3bb60911551..e6f7c13d253b 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-activity.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-activity.ts @@ -4,10 +4,12 @@ import { activityCommonProps } from '../common/props'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import dayjs from 'dayjs'; +import { createActivityActionOutputSchema } from '../output-schemas'; export const createActivityAction = createAction({ auth: pipedriveAuth, name: 'create-activity', + outputSchema: createActivityActionOutputSchema, classification: 'WRITE', displayName: 'Create Activity', description: 'Creates a new activity.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-deal.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-deal.ts index 48f17e4f20ac..cbf209572b38 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-deal.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-deal.ts @@ -11,10 +11,12 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { GetField, GetDealResponse } from '../common/types'; import dayjs from 'dayjs'; import { isEmpty } from '@activepieces/pieces-framework'; +import { createDealActionOutputSchema } from '../output-schemas'; export const createDealAction = createAction({ auth: pipedriveAuth, name: 'create-deal', + outputSchema: createDealActionOutputSchema, classification: 'WRITE', displayName: 'Create Deal', description: 'Creates a new deal.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-lead.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-lead.ts index 07791fc5b687..6d437fec6be8 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-lead.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-lead.ts @@ -10,10 +10,12 @@ import { import { HttpMethod } from '@activepieces/pieces-common'; import { GetField, GetLeadResponse } from '../common/types'; import dayjs from 'dayjs'; +import { createLeadActionOutputSchema } from '../output-schemas'; export const createLeadAction = createAction({ auth: pipedriveAuth, name: 'create-lead', + outputSchema: createLeadActionOutputSchema, classification: 'WRITE', displayName: 'Create Lead', description: 'Creates a new lead.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-note.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-note.ts index ffd733094ac0..aa55add580f6 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-note.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-note.ts @@ -3,10 +3,12 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { dealIdProp, leadIdProp, organizationIdProp, personIdProp } from '../common/props'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; +import { createNoteActionOutputSchema } from '../output-schemas'; export const createNoteAction = createAction({ auth: pipedriveAuth, name: 'create-note', + outputSchema: createNoteActionOutputSchema, classification: 'WRITE', displayName: 'Create Note', description: 'Creates a new note.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-organization.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-organization.ts index 33a798a0f340..3f3185258f9e 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-organization.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-organization.ts @@ -10,10 +10,12 @@ import { import { GetField, GetOrganizationResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; import { isEmpty } from '@activepieces/pieces-framework'; +import { createOrganizationActionOutputSchema } from '../output-schemas'; export const createOrganizationAction = createAction({ auth: pipedriveAuth, name: 'create-organization', + outputSchema: createOrganizationActionOutputSchema, classification: 'WRITE', displayName: 'Create Organization', description: 'Creates a new organization.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-person.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-person.ts index f1345671e75f..f418f86a43a3 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-person.ts @@ -10,10 +10,12 @@ import { import { GetField, GetPersonResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; import { isEmpty } from '@activepieces/pieces-framework'; +import { createPersonActionOutputSchema } from '../output-schemas'; export const createPersonAction = createAction({ auth: pipedriveAuth, name: 'create-person', + outputSchema: createPersonActionOutputSchema, classification: 'WRITE', displayName: 'Create Person', description: 'Creates a new person.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/create-product.ts b/packages/pieces/community/pipedrive/src/lib/actions/create-product.ts index 39084a348fc0..3fd1b0875c32 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/create-product.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/create-product.ts @@ -10,10 +10,12 @@ import { import { GetField, GetProductResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; import { isEmpty } from '@activepieces/pieces-framework'; +import { createProductActionOutputSchema } from '../output-schemas'; export const createProductAction = createAction({ auth: pipedriveAuth, name: 'create-product', + outputSchema: createProductActionOutputSchema, classification: 'WRITE', displayName: 'Create Product', description: 'Creates a new product.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-activity.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-activity.ts index d0b22337dcc3..62c326ce4714 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-activity.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-activity.ts @@ -4,10 +4,12 @@ import { activityTypeIdProp, filterIdProp, ownerIdProp } from '../common/props'; import { pipedrivePaginatedV2ApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; +import { findActivityActionOutputSchema } from '../output-schemas'; export const findActivityAction = createAction({ auth: pipedriveAuth, name: 'find-activity', + outputSchema: findActivityActionOutputSchema, classification: 'SEARCH', displayName: 'Find Activity', description: 'Finds an activity by subject.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-deal.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-deal.ts index e64ef7d01cd7..a49005060184 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-deal.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-deal.ts @@ -10,10 +10,12 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; import { searchFieldProp, searchFieldValueProp } from '../common/props'; import { DEAL_OPTIONAL_FIELDS } from '../common/constants'; +import { findDealActionOutputSchema } from '../output-schemas'; export const findDealAction = createAction({ auth: pipedriveAuth, name: 'find-deal', + outputSchema: findDealActionOutputSchema, classification: 'SEARCH', displayName: 'Find Deal', description: 'Finds a deal by any field.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-deals-associated-with-person.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-deals-associated-with-person.ts index f6137ecc2a7d..31bcdf8e31cb 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-deals-associated-with-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-deals-associated-with-person.ts @@ -10,10 +10,12 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; import { GetField } from '../common/types'; import { DEAL_OPTIONAL_FIELDS } from '../common/constants'; +import { findDealsAssociatedWithPersonActionOutputSchema } from '../output-schemas'; export const findDealsAssociatedWithPersonAction = createAction({ auth: pipedriveAuth, name: 'find-deals-associated-with-person', + outputSchema: findDealsAssociatedWithPersonActionOutputSchema, classification: 'SEARCH', displayName: 'Find Deals Associated With Person', description: 'Finds multiple deals related to a specific person.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-leads.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-leads.ts index 8827900d88d3..5665f16d3b6e 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-leads.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-leads.ts @@ -3,10 +3,12 @@ import { isNil } from '@activepieces/pieces-framework'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import { pipedriveAuth } from '../auth'; +import { findLeadActionOutputSchema } from '../output-schemas'; export const findLeadAction = createAction({ auth: pipedriveAuth, name: 'find-lead', + outputSchema: findLeadActionOutputSchema, classification: 'SEARCH', displayName: 'Find Lead', description: 'Finds leads by title.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-notes.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-notes.ts index 960d2e6ed02b..d6b48071c7a7 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-notes.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-notes.ts @@ -3,10 +3,12 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { pipedrivePaginatedV1ApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; +import { findNotesActionOutputSchema } from '../output-schemas'; export const findNotesAction = createAction({ auth: pipedriveAuth, name: 'find-notes', + outputSchema: findNotesActionOutputSchema, classification: 'SEARCH', displayName: 'Find Notes', description: 'Finds notes by Deal, Lead, Person, or Organization ID.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-organization.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-organization.ts index 15273681e348..75c6d74b7bce 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-organization.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-organization.ts @@ -10,10 +10,12 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; import { searchFieldProp, searchFieldValueProp } from '../common/props'; import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants'; +import { findOrganizationActionOutputSchema } from '../output-schemas'; export const findOrganizationAction = createAction({ auth: pipedriveAuth, name: 'find-organization', + outputSchema: findOrganizationActionOutputSchema, classification: 'SEARCH', displayName: 'Find Organization', description: 'Finds an organization.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-person.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-person.ts index 45c962e8b394..a6236b97a7f0 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-person.ts @@ -10,10 +10,12 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; import { searchFieldProp, searchFieldValueProp } from '../common/props'; import { PERSON_OPTIONAL_FIELDS } from '../common/constants'; +import { findPersonActionOutputSchema } from '../output-schemas'; export const findPersonAction = createAction({ auth: pipedriveAuth, name: 'find-person', + outputSchema: findPersonActionOutputSchema, classification: 'SEARCH', displayName: 'Find Person', description: 'Finds a person.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-product.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-product.ts index 728a31ab6ce1..e5b9574a78c6 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-product.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-product.ts @@ -8,10 +8,12 @@ import { import { HttpMethod } from '@activepieces/pieces-common'; import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; +import { findProductActionOutputSchema } from '../output-schemas'; export const findProductAction = createAction({ auth: pipedriveAuth, name: 'find-product', + outputSchema: findProductActionOutputSchema, classification: 'SEARCH', displayName: 'Find Product', description: 'Finds a product by name ', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-products.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-products.ts index 0f39ecdf2cc0..f9ff71f4c70d 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-products.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-products.ts @@ -9,10 +9,12 @@ import { import { HttpMethod, QueryParams } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; import { GetField } from '../common/types'; +import { findProductsActionOutputSchema } from '../output-schemas'; export const findProductsAction = createAction({ auth: pipedriveAuth, name: 'find-products', + outputSchema: findProductsActionOutputSchema, classification: 'SEARCH', displayName: 'Find Products', description: 'Finds a product or products by name or product code.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/find-user.ts b/packages/pieces/community/pipedrive/src/lib/actions/find-user.ts index 86c2ff1b168a..e4065d8a088f 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/find-user.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/find-user.ts @@ -2,10 +2,12 @@ import { pipedriveAuth } from '../auth'; import { createAction, Property } from '@activepieces/pieces-framework'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; +import { findUserActionOutputSchema } from '../output-schemas'; export const findUserAction = createAction({ auth: pipedriveAuth, name: 'find-user', + outputSchema: findUserActionOutputSchema, classification: 'SEARCH', displayName: 'Find User', description: 'Finds a user by name or email.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/get-note.ts b/packages/pieces/community/pipedrive/src/lib/actions/get-note.ts index f4e93ba330ef..34a09235c185 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/get-note.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/get-note.ts @@ -2,10 +2,12 @@ import { pipedriveAuth } from '../auth'; import { createAction, Property } from '@activepieces/pieces-framework'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; +import { getNoteActionOutputSchema } from '../output-schemas'; export const getNoteAction = createAction({ auth: pipedriveAuth, name: 'get-note', + outputSchema: getNoteActionOutputSchema, classification: 'READ', displayName: 'Retrieve a Note', description: 'Finds a note by ID.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/get-product.ts b/packages/pieces/community/pipedrive/src/lib/actions/get-product.ts index 1364ac6c2226..c1bebaf00dc7 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/get-product.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/get-product.ts @@ -7,10 +7,12 @@ import { } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import { GetField } from '../common/types'; +import { getProductActionOutputSchema } from '../output-schemas'; export const getProductAction = createAction({ auth: pipedriveAuth, name: 'get-product', + outputSchema: getProductActionOutputSchema, classification: 'READ', displayName: 'Retrieve a Product', description: 'Finds a product by ID.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/update-activity.ts b/packages/pieces/community/pipedrive/src/lib/actions/update-activity.ts index 76ace07f1317..530f5d4a4b0c 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/update-activity.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/update-activity.ts @@ -4,10 +4,12 @@ import { activityCommonProps } from '../common/props'; import { pipedriveApiCall } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import dayjs from 'dayjs'; +import { updateActivityActionOutputSchema } from '../output-schemas'; export const updateActivityAction = createAction({ auth: pipedriveAuth, name: 'update-activity', + outputSchema: updateActivityActionOutputSchema, classification: 'WRITE', displayName: 'Update Activity', description: 'Updates an existing activity.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/update-deal.ts b/packages/pieces/community/pipedrive/src/lib/actions/update-deal.ts index 9f67077bef5a..060a628005b9 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/update-deal.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/update-deal.ts @@ -11,10 +11,12 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { GetField, GetDealResponse } from '../common/types'; import dayjs from 'dayjs'; import { isEmpty } from '@activepieces/pieces-framework'; +import { updateDealActionOutputSchema } from '../output-schemas'; export const updateDealAction = createAction({ auth: pipedriveAuth, name: 'update-deal', + outputSchema: updateDealActionOutputSchema, classification: 'WRITE', displayName: 'Update Deal', description: 'Updates an existing deal.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/update-lead.ts b/packages/pieces/community/pipedrive/src/lib/actions/update-lead.ts index 88dcd4c05420..6174b376b464 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/update-lead.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/update-lead.ts @@ -9,10 +9,12 @@ import { import { HttpMethod } from '@activepieces/pieces-common'; import { GetField, GetLeadResponse } from '../common/types'; import dayjs from 'dayjs'; +import { updateLeadActionOutputSchema } from '../output-schemas'; export const updateLeadAction = createAction({ auth: pipedriveAuth, name: 'update-lead', + outputSchema: updateLeadActionOutputSchema, classification: 'WRITE', displayName: 'Update Lead', description: 'Updates an existing lead.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/update-organization.ts b/packages/pieces/community/pipedrive/src/lib/actions/update-organization.ts index d6a178e9ef9c..03bb7c0c1a7d 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/update-organization.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/update-organization.ts @@ -10,10 +10,12 @@ import { import { GetField, GetOrganizationResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; import { isEmpty } from '@activepieces/pieces-framework'; +import { updateOrganizationActionOutputSchema } from '../output-schemas'; export const updateOrganizationAction = createAction({ auth: pipedriveAuth, name: 'update-organization', + outputSchema: updateOrganizationActionOutputSchema, classification: 'WRITE', displayName: 'Update Organization', description: 'Updates an existing organization.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/update-person.ts b/packages/pieces/community/pipedrive/src/lib/actions/update-person.ts index 9ca952165e41..34d142a9f993 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/update-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/update-person.ts @@ -10,10 +10,12 @@ import { import { GetField, GetPersonResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; import { isEmpty } from '@activepieces/pieces-framework'; +import { updatePersonActionOutputSchema } from '../output-schemas'; export const updatePersonAction = createAction({ auth: pipedriveAuth, name: 'update-person', + outputSchema: updatePersonActionOutputSchema, classification: 'WRITE', displayName: 'Update Person', description: 'Updates an existing person.', diff --git a/packages/pieces/community/pipedrive/src/lib/actions/update-product.ts b/packages/pieces/community/pipedrive/src/lib/actions/update-product.ts index c8f9945fa7da..e4474fb5af35 100644 --- a/packages/pieces/community/pipedrive/src/lib/actions/update-product.ts +++ b/packages/pieces/community/pipedrive/src/lib/actions/update-product.ts @@ -10,10 +10,12 @@ import { import { GetField, GetProductResponse } from '../common/types'; import { HttpMethod } from '@activepieces/pieces-common'; import { isEmpty } from '@activepieces/pieces-framework'; +import { updateProductActionOutputSchema } from '../output-schemas'; export const updateProductAction = createAction({ auth: pipedriveAuth, name: 'update-product', + outputSchema: updateProductActionOutputSchema, classification: 'WRITE', displayName: 'Update Product', description: 'Updates an existing product.', diff --git a/packages/pieces/community/pipedrive/src/lib/output-schemas.ts b/packages/pieces/community/pipedrive/src/lib/output-schemas.ts new file mode 100644 index 000000000000..aaa35ebdf2b6 --- /dev/null +++ b/packages/pieces/community/pipedrive/src/lib/output-schemas.ts @@ -0,0 +1,447 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +const contactChannelFields: OutputSchema['fields'] = [ + { key: 'value', label: 'Value' }, + { key: 'label', label: 'Label' }, + { key: 'primary', label: 'Primary', format: 'boolean' }, +]; + +const personCoreFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Person ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'first_name', label: 'First Name' }, + { key: 'last_name', label: 'Last Name' }, + { key: 'emails', label: 'Emails', labelKey: 'value', listItems: contactChannelFields }, + { key: 'phones', label: 'Phones', labelKey: 'value', listItems: contactChannelFields }, + { key: 'org_id', label: 'Organization ID', format: 'number' }, + { key: 'owner_id', label: 'Owner ID', format: 'number' }, + { key: 'label_ids', label: 'Label IDs' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'is_deleted', label: 'Deleted', format: 'boolean' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const personStatsFields: OutputSchema['fields'] = [ + { key: 'open_deals_count', label: 'Open Deals', format: 'number' }, + { key: 'won_deals_count', label: 'Won Deals', format: 'number' }, + { key: 'lost_deals_count', label: 'Lost Deals', format: 'number' }, + { key: 'closed_deals_count', label: 'Closed Deals', format: 'number' }, + { key: 'activities_count', label: 'Activities', format: 'number' }, + { key: 'done_activities_count', label: 'Done Activities', format: 'number' }, + { key: 'undone_activities_count', label: 'Undone Activities', format: 'number' }, + { key: 'notes_count', label: 'Notes', format: 'number' }, + { key: 'files_count', label: 'Files', format: 'number' }, + { key: 'followers_count', label: 'Followers', format: 'number' }, + { key: 'last_activity_id', label: 'Last Activity ID', format: 'number' }, + { key: 'next_activity_id', label: 'Next Activity ID', format: 'number' }, + { key: 'last_incoming_mail_time', label: 'Last Incoming Mail', format: 'datetime' }, + { key: 'last_outgoing_mail_time', label: 'Last Outgoing Mail', format: 'datetime' }, +]; + +const personDetailFields: OutputSchema['fields'] = [...personCoreFields, ...personStatsFields]; + +const dealCoreFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Deal ID', format: 'number' }, + { key: 'title', label: 'Title' }, + { key: 'value', label: 'Value', format: 'number' }, + { key: 'currency', label: 'Currency' }, + { key: 'status', label: 'Status' }, + { key: 'probability', label: 'Probability', format: 'number' }, + { key: 'stage_id', label: 'Stage ID', format: 'number' }, + { key: 'pipeline_id', label: 'Pipeline ID', format: 'number' }, + { key: 'person_id', label: 'Person ID', format: 'number' }, + { key: 'org_id', label: 'Organization ID', format: 'number' }, + { key: 'owner_id', label: 'Owner ID', format: 'number' }, + { key: 'creator_user_id', label: 'Creator User ID', format: 'number' }, + { key: 'label_ids', label: 'Label IDs' }, + { key: 'expected_close_date', label: 'Expected Close Date', format: 'date' }, + { key: 'close_time', label: 'Closed At', format: 'datetime' }, + { key: 'won_time', label: 'Won At', format: 'datetime' }, + { key: 'lost_time', label: 'Lost At', format: 'datetime' }, + { key: 'lost_reason', label: 'Lost Reason' }, + { key: 'stage_change_time', label: 'Stage Changed At', format: 'datetime' }, + { key: 'mrr', label: 'MRR', format: 'number' }, + { key: 'arr', label: 'ARR', format: 'number' }, + { key: 'acv', label: 'ACV', format: 'number' }, + { key: 'is_archived', label: 'Archived', format: 'boolean' }, + { key: 'is_deleted', label: 'Deleted', format: 'boolean' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const dealStatsFields: OutputSchema['fields'] = [ + { key: 'activities_count', label: 'Activities', format: 'number' }, + { key: 'done_activities_count', label: 'Done Activities', format: 'number' }, + { key: 'undone_activities_count', label: 'Undone Activities', format: 'number' }, + { key: 'notes_count', label: 'Notes', format: 'number' }, + { key: 'files_count', label: 'Files', format: 'number' }, + { key: 'followers_count', label: 'Followers', format: 'number' }, + { key: 'participants_count', label: 'Participants', format: 'number' }, + { key: 'products_count', label: 'Products', format: 'number' }, + { key: 'last_activity_id', label: 'Last Activity ID', format: 'number' }, + { key: 'next_activity_id', label: 'Next Activity ID', format: 'number' }, + { key: 'smart_bcc_email', label: 'Smart BCC Email', format: 'email' }, +]; + +const dealDetailFields: OutputSchema['fields'] = [...dealCoreFields, ...dealStatsFields]; + +const organizationCoreFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Organization ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'address', label: 'Address' }, + { key: 'website', label: 'Website', format: 'url' }, + { key: 'linkedin', label: 'LinkedIn', format: 'url' }, + { key: 'industry', label: 'Industry' }, + { key: 'employee_count', label: 'Employees', format: 'number' }, + { key: 'annual_revenue', label: 'Annual Revenue', format: 'number' }, + { key: 'owner_id', label: 'Owner ID', format: 'number' }, + { key: 'label_ids', label: 'Label IDs' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'is_deleted', label: 'Deleted', format: 'boolean' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const organizationStatsFields: OutputSchema['fields'] = [ + { key: 'people_count', label: 'People', format: 'number' }, + { key: 'open_deals_count', label: 'Open Deals', format: 'number' }, + { key: 'won_deals_count', label: 'Won Deals', format: 'number' }, + { key: 'lost_deals_count', label: 'Lost Deals', format: 'number' }, + { key: 'closed_deals_count', label: 'Closed Deals', format: 'number' }, + { key: 'activities_count', label: 'Activities', format: 'number' }, + { key: 'notes_count', label: 'Notes', format: 'number' }, + { key: 'files_count', label: 'Files', format: 'number' }, + { key: 'followers_count', label: 'Followers', format: 'number' }, + { key: 'last_activity_id', label: 'Last Activity ID', format: 'number' }, + { key: 'next_activity_id', label: 'Next Activity ID', format: 'number' }, +]; + +const organizationDetailFields: OutputSchema['fields'] = [ + ...organizationCoreFields, + ...organizationStatsFields, +]; + +const activityFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Activity ID', format: 'number' }, + { key: 'subject', label: 'Subject' }, + { key: 'type', label: 'Type' }, + { key: 'done', label: 'Done', format: 'boolean' }, + { key: 'due_date', label: 'Due Date', format: 'date' }, + { key: 'due_time', label: 'Due Time' }, + { key: 'duration', label: 'Duration', format: 'duration' }, + { key: 'priority', label: 'Priority' }, + { key: 'note', label: 'Note', format: 'html' }, + { key: 'public_description', label: 'Public Description' }, + { key: 'location', label: 'Location' }, + { key: 'outcome', label: 'Outcome' }, + { key: 'busy', label: 'Busy', format: 'boolean' }, + { key: 'deal_id', label: 'Deal ID', format: 'number' }, + { key: 'person_id', label: 'Person ID', format: 'number' }, + { key: 'org_id', label: 'Organization ID', format: 'number' }, + { key: 'lead_id', label: 'Lead ID' }, + { key: 'owner_id', label: 'Owner ID', format: 'number' }, + { key: 'creator_user_id', label: 'Creator User ID', format: 'number' }, + { key: 'participants', label: 'Participants' }, + { key: 'conference_meeting_url', label: 'Meeting URL', format: 'url' }, + { key: 'marked_as_done_time', label: 'Marked Done At', format: 'datetime' }, + { key: 'is_deleted', label: 'Deleted', format: 'boolean' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const productPriceFields: OutputSchema['fields'] = [ + { key: 'price', label: 'Price', format: 'number' }, + { key: 'currency', label: 'Currency' }, + { key: 'cost', label: 'Cost', format: 'number' }, + { key: 'direct_cost', label: 'Direct Cost', format: 'number' }, + { key: 'notes', label: 'Notes' }, +]; + +const productFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Product ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'code', label: 'Code' }, + { key: 'description', label: 'Description' }, + { key: 'unit', label: 'Unit' }, + { key: 'tax', label: 'Tax', format: 'number' }, + { key: 'category', label: 'Category' }, + { key: 'prices', label: 'Prices', labelKey: 'currency', listItems: productPriceFields }, + { key: 'billing_frequency', label: 'Billing Frequency' }, + { key: 'billing_frequency_cycles', label: 'Billing Cycles', format: 'number' }, + { key: 'is_linkable', label: 'Linkable', format: 'boolean' }, + { key: 'is_deleted', label: 'Deleted', format: 'boolean' }, + { key: 'owner_id', label: 'Owner ID', format: 'number' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const productSearchFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Product ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'code', label: 'Code' }, + { key: 'tax', label: 'Tax', format: 'number' }, + { key: 'type', label: 'Type' }, + { key: 'owner', label: 'Owner' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'custom_fields', label: 'Custom Fields', dynamicKey: true }, +]; + +const noteFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Note ID', format: 'number' }, + { key: 'content', label: 'Content', format: 'html' }, + { key: 'deal_id', label: 'Deal ID', format: 'number' }, + { key: 'person_id', label: 'Person ID', format: 'number' }, + { key: 'org_id', label: 'Organization ID', format: 'number' }, + { key: 'lead_id', label: 'Lead ID' }, + { + key: 'deal', + label: 'Deal', + children: [{ key: 'title', label: 'Title' }], + }, + { + key: 'user', + label: 'Author', + children: [ + { key: 'name', label: 'Name' }, + { key: 'email', label: 'Email', format: 'email' }, + ], + }, + { key: 'user_id', label: 'Author ID', format: 'number' }, + { key: 'last_update_user_id', label: 'Last Updated By', format: 'number' }, + { key: 'pinned_to_deal_flag', label: 'Pinned to Deal', format: 'boolean' }, + { key: 'pinned_to_person_flag', label: 'Pinned to Person', format: 'boolean' }, + { key: 'pinned_to_organization_flag', label: 'Pinned to Organization', format: 'boolean' }, + { key: 'active_flag', label: 'Active', format: 'boolean' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const leadFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Lead ID' }, + { key: 'title', label: 'Title' }, + { key: 'value', label: 'Value' }, + { key: 'owner_id', label: 'Owner ID', format: 'number' }, + { key: 'creator_id', label: 'Creator ID', format: 'number' }, + { key: 'person_id', label: 'Person ID', format: 'number' }, + { key: 'organization_id', label: 'Organization ID', format: 'number' }, + { key: 'label_ids', label: 'Label IDs' }, + { key: 'expected_close_date', label: 'Expected Close Date', format: 'date' }, + { key: 'source_name', label: 'Source' }, + { key: 'channel', label: 'Channel' }, + { key: 'cc_email', label: 'CC Email', format: 'email' }, + { key: 'was_seen', label: 'Seen', format: 'boolean' }, + { key: 'is_archived', label: 'Archived', format: 'boolean' }, + { key: 'next_activity_id', label: 'Next Activity ID', format: 'number' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const leadSearchFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Lead ID' }, + { key: 'title', label: 'Title' }, + { key: 'type', label: 'Type' }, + { key: 'value', label: 'Value', format: 'number' }, + { key: 'currency', label: 'Currency' }, + { key: 'person', label: 'Person' }, + { key: 'organization', label: 'Organization' }, + { key: 'owner', label: 'Owner' }, + { key: 'emails', label: 'Emails' }, + { key: 'phones', label: 'Phones' }, + { key: 'notes', label: 'Notes' }, + { key: 'is_archived', label: 'Archived', format: 'boolean' }, + { key: 'visible_to', label: 'Visible To' }, + { key: 'custom_fields', label: 'Custom Fields', dynamicKey: true }, +]; + +const userFields: OutputSchema['fields'] = [ + { key: 'id', label: 'User ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'email', label: 'Email', format: 'email' }, +]; + +const dealProductFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Deal Product ID', format: 'number' }, + { key: 'deal_id', label: 'Deal ID', format: 'number' }, + { key: 'product_id', label: 'Product ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'item_price', label: 'Item Price', format: 'number' }, + { key: 'quantity', label: 'Quantity', format: 'number' }, + { key: 'sum', label: 'Sum', format: 'number' }, + { key: 'currency', label: 'Currency' }, + { key: 'discount', label: 'Discount', format: 'number' }, + { key: 'discount_type', label: 'Discount Type' }, + { key: 'tax', label: 'Tax', format: 'number' }, + { key: 'tax_method', label: 'Tax Method' }, + { key: 'comments', label: 'Comments' }, + { key: 'order_nr', label: 'Order Number', format: 'number' }, + { key: 'is_enabled', label: 'Enabled', format: 'boolean' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +const followerFields: OutputSchema['fields'] = [ + { key: 'user_id', label: 'User ID', format: 'number' }, + { key: 'add_time', label: 'Followed At', format: 'datetime' }, +]; + +const fileFields: OutputSchema['fields'] = [ + { key: 'id', label: 'File ID', format: 'number' }, + { key: 'name', label: 'Name' }, + { key: 'file_name', label: 'Stored File Name' }, + { key: 'file_type', label: 'File Type' }, + { key: 'file_size', label: 'File Size', format: 'number' }, + { key: 'url', label: 'Download URL', format: 'url' }, + { key: 'description', label: 'Description' }, + { key: 'user_id', label: 'User ID', format: 'number' }, + { key: 'active_flag', label: 'Active', format: 'boolean' }, + { key: 'inline_flag', label: 'Inline', format: 'boolean' }, + { key: 'remote_location', label: 'Remote Location' }, + { key: 'remote_id', label: 'Remote ID' }, + { key: 'deal_id', label: 'Deal ID', format: 'number' }, + { key: 'deal_name', label: 'Deal Name' }, + { key: 'lead_id', label: 'Lead ID' }, + { key: 'lead_name', label: 'Lead Name' }, + { key: 'person_id', label: 'Person ID', format: 'number' }, + { key: 'person_name', label: 'Person Name' }, + { key: 'org_id', label: 'Organization ID', format: 'number' }, + { key: 'org_name', label: 'Organization Name' }, + { key: 'product_id', label: 'Product ID', format: 'number' }, + { key: 'product_name', label: 'Product Name' }, + { key: 'activity_id', label: 'Activity ID', format: 'number' }, + { key: 'log_id', label: 'Log ID', format: 'number' }, + { key: 'mail_message_id', label: 'Mail Message ID', format: 'number' }, + { key: 'mail_template_id', label: 'Mail Template ID', format: 'number' }, + { key: 'cid', label: 'Content ID' }, + { key: 'add_time', label: 'Created At', format: 'datetime' }, + { key: 'update_time', label: 'Updated At', format: 'datetime' }, +]; + +function envelope(label: string, fields: OutputSchema['fields']): OutputSchema { + return { + fields: [ + { key: 'success', label: 'Success', format: 'boolean' }, + { key: 'data', label, children: fields }, + ], + }; +} + +function search({ + label, + fields, + labelKey, +}: { + label: string; + fields: OutputSchema['fields']; + labelKey: string; +}): OutputSchema { + return { + fields: [ + { key: 'found', label: 'Found', format: 'boolean' }, + { key: 'data', label, labelKey, listItems: fields }, + ], + }; +} + +export const createPersonActionOutputSchema = envelope('Person', personCoreFields); +export const updatePersonActionOutputSchema = envelope('Person', personCoreFields); +export const addLabelsToPersonActionOutputSchema = envelope('Person', personCoreFields); +export const createDealActionOutputSchema = envelope('Deal', dealCoreFields); +export const updateDealActionOutputSchema = envelope('Deal', dealCoreFields); +export const createOrganizationActionOutputSchema = envelope('Organization', organizationCoreFields); +export const updateOrganizationActionOutputSchema = envelope('Organization', organizationCoreFields); +export const createActivityActionOutputSchema = envelope('Activity', activityFields); +export const updateActivityActionOutputSchema = envelope('Activity', activityFields); +export const createProductActionOutputSchema = envelope('Product', productFields); +export const updateProductActionOutputSchema = envelope('Product', productFields); +export const addProductToDealActionOutputSchema = envelope('Deal Product', dealProductFields); +export const createNoteActionOutputSchema = envelope('Note', noteFields); +export const getNoteActionOutputSchema = envelope('Note', noteFields); +export const createLeadActionOutputSchema = envelope('Lead', leadFields); +export const updateLeadActionOutputSchema = envelope('Lead', leadFields); + +export const findPersonActionOutputSchema = search({ + label: 'People', + fields: personDetailFields, + labelKey: 'name', +}); +export const findDealActionOutputSchema = search({ + label: 'Deals', + fields: dealDetailFields, + labelKey: 'title', +}); +export const findDealsAssociatedWithPersonActionOutputSchema = search({ + label: 'Deals', + fields: dealDetailFields, + labelKey: 'title', +}); +export const findOrganizationActionOutputSchema = search({ + label: 'Organizations', + fields: organizationDetailFields, + labelKey: 'name', +}); +export const findActivityActionOutputSchema = search({ + label: 'Activities', + fields: activityFields, + labelKey: 'subject', +}); +export const findNotesActionOutputSchema = search({ + label: 'Notes', + fields: noteFields, + labelKey: 'content', +}); +export const findLeadActionOutputSchema = search({ + label: 'Leads', + fields: leadSearchFields, + labelKey: 'title', +}); +export const findUserActionOutputSchema = search({ + label: 'Users', + fields: userFields, + labelKey: 'name', +}); +export const getProductActionOutputSchema = search({ + label: 'Products', + fields: productFields, + labelKey: 'name', +}); +export const findProductActionOutputSchema = search({ + label: 'Products', + fields: productFields, + labelKey: 'name', +}); +export const findProductsActionOutputSchema = search({ + label: 'Products', + fields: productSearchFields, + labelKey: 'name', +}); +export const addFollowerActionOutputSchema = envelope('Follower', followerFields); +export const attachFileActionOutputSchema = envelope('File', fileFields); + +export const newPersonTriggerOutputSchema: OutputSchema = { fields: personDetailFields }; +export const updatedPersonTriggerOutputSchema: OutputSchema = { fields: personDetailFields }; +export const newDealTriggerOutputSchema: OutputSchema = { fields: dealDetailFields }; +export const updatedDealTriggerOutputSchema: OutputSchema = { fields: dealDetailFields }; +export const updatedDealStageTriggerOutputSchema: OutputSchema = { fields: dealDetailFields }; +export const newOrganizationTriggerOutputSchema: OutputSchema = { + fields: organizationDetailFields, +}; +export const updatedOrganizationTriggerOutputSchema: OutputSchema = { + fields: organizationDetailFields, +}; +export const newActivityTriggerOutputSchema: OutputSchema = { fields: activityFields }; +export const newNoteTriggerOutputSchema: OutputSchema = { fields: noteFields }; +export const newLeadTriggerOutputSchema: OutputSchema = { fields: leadFields }; +export const personMatchingFilterTriggerOutputSchema: OutputSchema = { + fields: personDetailFields, +}; +export const dealMatchingFilterTriggerOutputSchema: OutputSchema = { fields: dealDetailFields }; +export const organizationMatchingFilterTriggerOutputSchema: OutputSchema = { + fields: organizationDetailFields, +}; +export const activityMatchingFilterTriggerOutputSchema: OutputSchema = { + fields: activityFields, +}; diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/activity-matching-filter.ts b/packages/pieces/community/pipedrive/src/lib/trigger/activity-matching-filter.ts index 3b1e8b2cac53..646f683e9b6c 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/activity-matching-filter.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/activity-matching-filter.ts @@ -5,10 +5,12 @@ import { filterIdProp } from '../common/props'; import { pipedriveApiCall, pipedrivePaginatedV2ApiCall } from '../common'; import { LeadListResponse } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; +import { activityMatchingFilterTriggerOutputSchema } from '../output-schemas'; export const activityMatchingFilterTrigger = createTrigger({ auth: pipedriveAuth, name: 'activity-matching-filter', + outputSchema: activityMatchingFilterTriggerOutputSchema, classification: 'READ', displayName: 'Activity Matching Filter', description: 'Trigges when an activity newly matches a Pipedrive filter for the first time.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/deal-matching-filter.ts b/packages/pieces/community/pipedrive/src/lib/trigger/deal-matching-filter.ts index ba3e77298aff..5dba3b564803 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/deal-matching-filter.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/deal-matching-filter.ts @@ -11,10 +11,12 @@ import { import { GetField, LeadListResponse } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { DEAL_OPTIONAL_FIELDS } from '../common/constants'; +import { dealMatchingFilterTriggerOutputSchema } from '../output-schemas'; export const dealMatchingFilterTrigger = createTrigger({ auth: pipedriveAuth, name: 'deal-matching-filter', + outputSchema: dealMatchingFilterTriggerOutputSchema, classification: 'READ', displayName: 'Deal Matching Filter', description: 'Trigges when a deal newly matches a Pipedrive filter for the first time.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/new-activity.ts b/packages/pieces/community/pipedrive/src/lib/trigger/new-activity.ts index 9e6ed1c238ca..27f297b380d8 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/new-activity.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/new-activity.ts @@ -5,6 +5,7 @@ import { pipedriveAuth } from '../auth'; import { HttpMethod } from '@activepieces/pieces-common'; import { LeadListResponse } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; +import { newActivityTriggerOutputSchema } from '../output-schemas'; interface PipedriveActivityV2 { id: number; @@ -66,6 +67,7 @@ interface ListActivitiesResponse { export const newActivity = createTrigger({ auth: pipedriveAuth, name: 'new_activity', + outputSchema: newActivityTriggerOutputSchema, classification: 'READ', displayName: 'New Activity', description: 'Triggers when a new activity is added', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/new-deal.ts b/packages/pieces/community/pipedrive/src/lib/trigger/new-deal.ts index e73e6b053b9a..982dadb5f929 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/new-deal.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/new-deal.ts @@ -11,6 +11,7 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { DEAL_OPTIONAL_FIELDS } from '../common/constants'; +import { newDealTriggerOutputSchema } from '../output-schemas'; interface PipedriveDealV2 { id: number; @@ -70,6 +71,7 @@ interface GetDealResponseV2 { export const newDeal = createTrigger({ auth: pipedriveAuth, name: 'new_deal', + outputSchema: newDealTriggerOutputSchema, classification: 'READ', displayName: 'New Deal', description: 'Triggers when a new deal is created.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/new-lead.ts b/packages/pieces/community/pipedrive/src/lib/trigger/new-lead.ts index d859a8ea138b..3d56d328b015 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/new-lead.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/new-lead.ts @@ -9,6 +9,7 @@ import { } from '../common'; import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; +import { newLeadTriggerOutputSchema } from '../output-schemas'; interface PipedriveLeadV2 { id: string; @@ -53,6 +54,7 @@ interface GetLeadResponseV2 { export const newLeadTrigger = createTrigger({ auth: pipedriveAuth, name: 'new-lead', + outputSchema: newLeadTriggerOutputSchema, classification: 'READ', displayName: 'New Lead', description: 'Triggers when a new lead is created.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/new-note.ts b/packages/pieces/community/pipedrive/src/lib/trigger/new-note.ts index 08263b8804ec..7b83fde59408 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/new-note.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/new-note.ts @@ -6,6 +6,7 @@ import { pipedriveCommon, } from '../common'; import { isNil } from '@activepieces/pieces-framework'; +import { newNoteTriggerOutputSchema } from '../output-schemas'; interface PipedriveNoteV2 { id: number; @@ -40,6 +41,7 @@ interface GetNoteResponseV2 { export const newNoteTrigger = createTrigger({ auth: pipedriveAuth, name: 'new-note', + outputSchema: newNoteTriggerOutputSchema, classification: 'READ', displayName: 'New Note', description: 'Triggers when a new note is created.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/new-organization.ts b/packages/pieces/community/pipedrive/src/lib/trigger/new-organization.ts index 9f74ba419910..50274dc15409 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/new-organization.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/new-organization.ts @@ -10,6 +10,7 @@ import { import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants'; +import { newOrganizationTriggerOutputSchema } from '../output-schemas'; interface PipedriveOrganizationV2 { id: number; @@ -78,6 +79,7 @@ interface GetOrganizationResponseV2 { export const newOrganizationTrigger = createTrigger({ auth: pipedriveAuth, name: 'new-organization', + outputSchema: newOrganizationTriggerOutputSchema, classification: 'READ', displayName: 'New Organization', description: 'Triggers when a new organization is created.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/new-person.ts b/packages/pieces/community/pipedrive/src/lib/trigger/new-person.ts index 42b51c27a124..b4a7e1d0d96f 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/new-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/new-person.ts @@ -10,6 +10,7 @@ import { import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { PERSON_OPTIONAL_FIELDS } from '../common/constants'; +import { newPersonTriggerOutputSchema } from '../output-schemas'; interface PipedrivePersonV2 { id: number; @@ -79,6 +80,7 @@ interface GetPersonResponseV2 { export const newPerson = createTrigger({ auth: pipedriveAuth, name: 'new_person', + outputSchema: newPersonTriggerOutputSchema, classification: 'READ', displayName: 'New Person', description: 'Triggers when a new person is created', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/organization-matching-filter.ts b/packages/pieces/community/pipedrive/src/lib/trigger/organization-matching-filter.ts index 89b88d69df3f..adfe36f4a7a2 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/organization-matching-filter.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/organization-matching-filter.ts @@ -11,6 +11,7 @@ import { import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants'; +import { organizationMatchingFilterTriggerOutputSchema } from '../output-schemas'; interface PipedriveOrganizationV2 { id: number; @@ -75,6 +76,7 @@ interface OrganizationListResponseV2 { export const organizationMatchingFilterTrigger = createTrigger({ auth: pipedriveAuth, name: 'organization-matching-filter', + outputSchema: organizationMatchingFilterTriggerOutputSchema, classification: 'READ', displayName: 'Organization Matching Filter', description: 'Triggers when an organization newly matches a Pipedrive filter for the first time.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/person-matching-filter.ts b/packages/pieces/community/pipedrive/src/lib/trigger/person-matching-filter.ts index 9250fecdced4..d09c3ff30f17 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/person-matching-filter.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/person-matching-filter.ts @@ -11,6 +11,7 @@ import { import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { PERSON_OPTIONAL_FIELDS } from '../common/constants'; +import { personMatchingFilterTriggerOutputSchema } from '../output-schemas'; interface PipedrivePersonV2 { id: number; @@ -80,6 +81,7 @@ interface GetPersonResponseV2 { export const personMatchingFilterTrigger = createTrigger({ auth: pipedriveAuth, name: 'person-matching-filter', + outputSchema: personMatchingFilterTriggerOutputSchema, classification: 'READ', displayName: 'Person Matching Filter', description: 'Triggers when a person newly matches a Pipedrive filter for the first time.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal-stage.ts b/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal-stage.ts index 0d011243d5c1..eb482bc2df20 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal-stage.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal-stage.ts @@ -16,6 +16,7 @@ import { GetField, RequestParams, WebhookCreateResponse } from '../common/types' import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; import { DEAL_OPTIONAL_FIELDS } from '../common/constants'; +import { updatedDealStageTriggerOutputSchema } from '../output-schemas'; interface PipedriveDealV2 { id: number; @@ -93,6 +94,7 @@ interface GetDealResponseV2 { export const updatedDealStageTrigger = createTrigger({ auth: pipedriveAuth, name: 'updated-deal-stage', + outputSchema: updatedDealStageTriggerOutputSchema, classification: 'READ', displayName: 'Updated Deal Stage', description: "Triggers when a deal's stage is updated.", diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal.ts b/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal.ts index 7133bbba8131..55f275c0dcbf 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/updated-deal.ts @@ -17,6 +17,7 @@ import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces import { FieldsResponse, GetField, RequestParams } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { DEAL_OPTIONAL_FIELDS } from '../common/constants'; +import { updatedDealTriggerOutputSchema } from '../output-schemas'; interface PipedriveDealV2 { id: number; @@ -94,6 +95,7 @@ interface GetDealResponseV2 { export const updatedDeal = createTrigger({ auth: pipedriveAuth, name: 'updated_deal', + outputSchema: updatedDealTriggerOutputSchema, classification: 'READ', displayName: 'Updated Deal', description: 'Triggers when a deal is updated.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/updated-organization.ts b/packages/pieces/community/pipedrive/src/lib/trigger/updated-organization.ts index 998fb15a481c..1014cd8a71c1 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/updated-organization.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/updated-organization.ts @@ -10,6 +10,7 @@ import { import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants'; +import { updatedOrganizationTriggerOutputSchema } from '../output-schemas'; interface PipedriveOrganizationV2 { id: number; @@ -78,6 +79,7 @@ interface GetOrganizationResponseV2 { export const updatedOrganizationTrigger = createTrigger({ auth: pipedriveAuth, name: 'updated-organization', + outputSchema: updatedOrganizationTriggerOutputSchema, classification: 'READ', displayName: 'Updated Organization', description: 'Triggers when an existing organization is updated.', diff --git a/packages/pieces/community/pipedrive/src/lib/trigger/updated-person.ts b/packages/pieces/community/pipedrive/src/lib/trigger/updated-person.ts index d2a8e8231795..badf8eb35a23 100644 --- a/packages/pieces/community/pipedrive/src/lib/trigger/updated-person.ts +++ b/packages/pieces/community/pipedrive/src/lib/trigger/updated-person.ts @@ -10,6 +10,7 @@ import { import { GetField } from '../common/types'; import { isNil } from '@activepieces/pieces-framework'; import { PERSON_OPTIONAL_FIELDS } from '../common/constants'; +import { updatedPersonTriggerOutputSchema } from '../output-schemas'; interface PipedrivePersonV2 { id: number; @@ -79,6 +80,7 @@ interface GetPersonResponseV2 { export const updatedPerson = createTrigger({ auth: pipedriveAuth, name: 'updated_person', + outputSchema: updatedPersonTriggerOutputSchema, classification: 'READ', displayName: 'Updated Person', description: 'Triggers when a person is updated.', diff --git a/packages/pieces/community/sendinblue/package.json b/packages/pieces/community/sendinblue/package.json index 1b5b258b0339..f8bd47a2ecd7 100644 --- a/packages/pieces/community/sendinblue/package.json +++ b/packages/pieces/community/sendinblue/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-sendinblue", - "version": "0.2.9", + "version": "0.3.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/sendinblue/src/index.ts b/packages/pieces/community/sendinblue/src/index.ts index c8c5276563ac..4e1b7fa393ae 100644 --- a/packages/pieces/community/sendinblue/src/index.ts +++ b/packages/pieces/community/sendinblue/src/index.ts @@ -1,13 +1,22 @@ import { createCustomApiCallAction } from '@activepieces/pieces-common'; -import { PieceAuth, createPiece } from '@activepieces/pieces-framework'; +import { createPiece } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/pieces-framework'; +import { createEvent } from './lib/actions/create-event'; import { createOrUpdateContact } from './lib/actions/create-or-update-contact'; - -export const sendinblueAuth = PieceAuth.SecretText({ - displayName: 'Project API key', - description: 'Your project API key', - required: true, -}); +import { findContact } from './lib/actions/find-contact'; +import { sendTransactionalEmail } from './lib/actions/send-transactional-email'; +import { sendTransactionalSms } from './lib/actions/send-transactional-sms'; +import { unsubscribeContact } from './lib/actions/unsubscribe-contact'; +import { sendinblueAuth } from './lib/auth'; +import { BREVO_API_URL } from './lib/common'; +import { contactAddedToList } from './lib/triggers/contact-added-to-list'; +import { contactDeleted } from './lib/triggers/contact-deleted'; +import { contactUnsubscribed } from './lib/triggers/contact-unsubscribed'; +import { contactUpdated } from './lib/triggers/contact-updated'; +import { emailBounced } from './lib/triggers/email-bounced'; +import { emailClicked } from './lib/triggers/email-clicked'; +import { emailDelivered } from './lib/triggers/email-delivered'; +import { emailOpened } from './lib/triggers/email-opened'; export const sendinblue = createPiece({ displayName: 'Brevo', @@ -20,13 +29,29 @@ export const sendinblue = createPiece({ auth: sendinblueAuth, actions: [ createOrUpdateContact, + findContact, + unsubscribeContact, + sendTransactionalEmail, + sendTransactionalSms, + createEvent, createCustomApiCallAction({ - baseUrl: () => 'https://api.sendinblue.com/v3', + baseUrl: () => BREVO_API_URL, auth: sendinblueAuth, authMapping: async (auth) => ({ 'api-key': auth.secret_text, }), }), ], - triggers: [], + triggers: [ + contactAddedToList, + contactUpdated, + contactDeleted, + contactUnsubscribed, + emailDelivered, + emailOpened, + emailClicked, + emailBounced, + ], }); + +export { sendinblueAuth }; diff --git a/packages/pieces/community/sendinblue/src/lib/actions/create-event.ts b/packages/pieces/community/sendinblue/src/lib/actions/create-event.ts new file mode 100644 index 000000000000..f1a8aa618f97 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/actions/create-event.ts @@ -0,0 +1,70 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; + +export const createEvent = createAction({ + auth: sendinblueAuth, + name: 'create_event', + classification: 'WRITE', + displayName: 'Create Event', + description: 'Track a custom event for a contact.', + audience: 'both', + aiMetadata: { + description: + 'Records a custom event against a Brevo contact so automations and segments can react to it. Identify the contact by email, contact id or external id, and attach arbitrary event properties. Brevo answers with an empty body, so this returns a success flag rather than the stored event. Not idempotent — each call records another occurrence.', + idempotent: false, + }, + props: { + event_name: Property.ShortText({ + displayName: 'Event Name', + description: 'How the event is identified in Brevo, for example order_completed.', + required: true, + }), + email: Property.ShortText({ + displayName: 'Contact Email', + description: 'Email of the contact the event belongs to.', + required: true, + }), + event_date: Property.DateTime({ + displayName: 'Event Date', + description: 'When the event occurred. Defaults to now when left empty.', + required: false, + }), + event_properties: Property.Object({ + displayName: 'Event Properties', + description: 'Details of the event, for example {"order_id": 42, "total": 99.5}.', + required: false, + }), + contact_properties: Property.Object({ + displayName: 'Contact Properties', + description: + 'Contact attributes to update alongside the event, for example {"FIRSTNAME": "Elly"}.', + required: false, + }), + }, + async run(context) { + const { event_name, email, event_date, event_properties, contact_properties } = + context.propsValue; + + const body = { + event_name, + identifiers: { email_id: email }, + event_date: event_date ?? undefined, + event_properties: brevoCommon.isEmptyObject(event_properties) ? undefined : event_properties, + contact_properties: brevoCommon.isEmptyObject(contact_properties) + ? undefined + : contact_properties, + }; + + await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.POST, + resourceUri: '/events', + body, + }); + + return { success: true }; + }, +}); + diff --git a/packages/pieces/community/sendinblue/src/lib/actions/create-or-update-contact.ts b/packages/pieces/community/sendinblue/src/lib/actions/create-or-update-contact.ts index c8168fa57b48..9758c2a8e05e 100644 --- a/packages/pieces/community/sendinblue/src/lib/actions/create-or-update-contact.ts +++ b/packages/pieces/community/sendinblue/src/lib/actions/create-or-update-contact.ts @@ -1,112 +1,102 @@ +import { HttpMethod } from '@activepieces/pieces-common'; import { createAction, Property } from '@activepieces/pieces-framework'; -import { httpClient, HttpMethod } from '@activepieces/pieces-common'; -import { sendinblueAuth } from '../..'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; +import { brevoProps } from '../common/props'; +import { createOrUpdateContactActionOutputSchema } from '../output-schemas'; export const createOrUpdateContact = createAction({ - auth: sendinblueAuth, - name: 'create_or_update_contact', - classification: 'WRITE', - displayName: 'Create or Update Contact', - description: 'Create or update an existing contact', - audience: 'both', - aiMetadata: { - description: - 'Upserts a Brevo (formerly Sendinblue) contact keyed on its email address, setting attributes, list membership, and email/SMS blacklist flags. Use to add a new subscriber or sync changes to an existing one; because it is keyed on email with update enabled, re-running with the same input is idempotent and does not create duplicates. Email is required, and any attributes referenced must already exist in the Brevo account.', - idempotent: true, - }, - props: { - email: Property.ShortText({ - displayName: 'Email', - description: `Email address of the user. Mandatory if "SMS" field is not passed in "attributes" parameter. Mobile Number in SMS field should be passed with proper country code. For example: {"SMS":"+91xxxxxxxxxx"} or {"SMS":"0091xxxxxxxxxx"}`, - required: true, - }), - ext_id: Property.ShortText({ - displayName: 'External ID', - description: `Pass your own Id to create a contact.`, - required: false, - }), - attributes: Property.Object({ - displayName: 'Attributes', - description: `Pass the set of attributes and their values. The attribute's parameter should be passed in capital letter while creating a contact. These attributes must be present in your SendinBlue account. For eg: - {"FNAME":"Elly", "LNAME":"Roger"}`, - required: false, - defaultValue: { - FIRST_NAME: '', - LAST_NAME: '', - SMS: '', - CIV: '', - DOB: '', - ADDRESS: '', - ZIP_CODE: '', - CITY: '', - AREA: '', - }, - }), - email_blacklisted: Property.Checkbox({ - displayName: 'Email Blacklisted?', - description: `Set this field to blacklist the contact for emails (emailBlacklisted = true)`, - required: false, - defaultValue: false, - }), - sms_blacklisted: Property.Checkbox({ - displayName: 'SMS Blacklisted?', - description: `Set this field to blacklist the contact for SMS (smsBlacklisted = true)`, - required: false, - defaultValue: false, - }), - list_ids: Property.Array({ - displayName: 'List IDs', - description: `Ids of the lists to add the contact to.`, - required: false, - defaultValue: [], - }), - smtp_blacklist_sender: Property.Checkbox({ - displayName: 'SMTP Blacklist Sender', - description: `transactional email forbidden sender for contact. Use only for email Contact ( only available if updateEnabled = true )`, - required: false, - defaultValue: false, - }), - }, - async run(context) { - let listIds: number[] = []; - if (context.propsValue.list_ids) { - listIds = context.propsValue.list_ids.map((listId) => { - return parseInt(listId as unknown as string); - }); - } - const contact = { - email: context.propsValue.email, - ext_id: context.propsValue.ext_id, - attributes: context.propsValue.attributes, - emailBlacklisted: context.propsValue.email_blacklisted, - smsBlacklisted: context.propsValue.sms_blacklisted, - listIds: listIds, - smtpBlacklistSender: context.propsValue.smtp_blacklist_sender, - updateEnabled: true, - }; - const identifier = context.propsValue.email; + auth: sendinblueAuth, + name: 'create_or_update_contact', + outputSchema: createOrUpdateContactActionOutputSchema, + classification: 'WRITE', + displayName: 'Create or Update Contact', + description: 'Create or update an existing contact', + audience: 'both', + aiMetadata: { + description: + 'Upserts a Brevo (formerly Sendinblue) contact keyed on its email address, setting attributes, list membership, and email/SMS blacklist flags. Use to add a new subscriber or sync changes to an existing one; because it is keyed on email with update enabled, re-running with the same input is idempotent and does not create duplicates. Email is required, and any attributes referenced must already exist in the Brevo account. Blacklist flags are only sent when explicitly set, so leaving them untouched preserves the contact current subscription state.', + idempotent: true, + }, + props: { + email: Property.ShortText({ + displayName: 'Email', + description: `Email address of the user. Mandatory if "SMS" field is not passed in "attributes" parameter. Mobile Number in SMS field should be passed with proper country code. For example: {"SMS":"+91xxxxxxxxxx"} or {"SMS":"0091xxxxxxxxxx"}`, + required: true, + }), + ext_id: Property.ShortText({ + displayName: 'External ID', + description: `Pass your own Id to create a contact.`, + required: false, + }), + attributes: Property.Object({ + displayName: 'Attributes', + description: `Pass the set of attributes and their values. The attribute's parameter should be passed in capital letter while creating a contact. These attributes must be present in your Brevo account. For eg: + {"FNAME":"Elly", "LNAME":"Roger"}. Only the attributes you list are changed; the rest are left as they are.`, + required: false, + }), + email_blacklisted: Property.Checkbox({ + displayName: 'Email Blacklisted?', + description: `Set this field to blacklist the contact for emails (emailBlacklisted = true). Leave untouched to keep the contact's current setting.`, + required: false, + }), + sms_blacklisted: Property.Checkbox({ + displayName: 'SMS Blacklisted?', + description: `Set this field to blacklist the contact for SMS (smsBlacklisted = true). Leave untouched to keep the contact's current setting.`, + required: false, + }), + list_ids: brevoProps.listIds({ + displayName: 'Lists', + description: 'Lists to add the contact to.', + }), + blocked_sender_addresses: Property.Array({ + displayName: 'Blocked Sender Addresses', + description: `Sender email addresses this contact must not receive transactional email from. Leave empty to change nothing.`, + required: false, + }), + }, + async run(context) { + const { + email, + ext_id, + attributes, + email_blacklisted, + sms_blacklisted, + list_ids, + blocked_sender_addresses, + } = context.propsValue; - // filter out undefined values - const body = Object.fromEntries( - Object.entries(contact).filter(([_, value]) => Boolean(value)) - ); + const listIds = (list_ids ?? []) + .map((listId) => Number(listId)) + .filter((listId) => Number.isFinite(listId)); - await httpClient.sendRequest({ - method: HttpMethod.POST, - url: `https://api.sendinblue.com/v3/contacts`, - body, - headers: { - 'api-key': context.auth.secret_text, - }, - }); + const blockedSenders = (Array.isArray(blocked_sender_addresses) ? blocked_sender_addresses : []) + .map((sender) => String(sender).trim()) + .filter((sender) => sender.length > 0); - const contactREsponse = await httpClient.sendRequest({ - method: HttpMethod.GET, - url: `https://api.sendinblue.com/v3/contacts/${encodeURI(identifier)}`, - headers: { - 'api-key': context.auth.secret_text, - }, - }); - return contactREsponse.body; - }, + const contact = { + email, + ext_id, + attributes: brevoCommon.isEmptyObject(attributes) ? undefined : attributes, + emailBlacklisted: email_blacklisted, + smsBlacklisted: sms_blacklisted, + listIds: listIds.length > 0 ? listIds : undefined, + smtpBlacklistSender: blockedSenders.length > 0 ? blockedSenders : undefined, + updateEnabled: true, + }; + + await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.POST, + resourceUri: '/contacts', + body: contact, + }); + + return await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.GET, + resourceUri: `/contacts/${encodeURIComponent(email)}`, + }); + }, }); + diff --git a/packages/pieces/community/sendinblue/src/lib/actions/find-contact.ts b/packages/pieces/community/sendinblue/src/lib/actions/find-contact.ts new file mode 100644 index 000000000000..b10a16442a9c --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/actions/find-contact.ts @@ -0,0 +1,62 @@ +import { HttpError, HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; +import { findContactActionOutputSchema } from '../output-schemas'; + +export const findContact = createAction({ + auth: sendinblueAuth, + name: 'find_contact', + outputSchema: findContactActionOutputSchema, + classification: 'READ', + displayName: 'Find Contact', + description: 'Check whether a contact exists in Brevo and fetch its details.', + audience: 'both', + aiMetadata: { + description: + 'Looks up a single Brevo contact by email, phone number, contact id, external id, WhatsApp id or landline number, and returns its attributes, list membership and blacklist flags. Returns found:false instead of failing when no contact matches, so it is safe to branch on. Read-only and idempotent.', + idempotent: true, + }, + props: { + identifier: Property.ShortText({ + displayName: 'Identifier', + description: 'The value to look the contact up by, for example an email address.', + required: true, + }), + identifier_type: Property.StaticDropdown({ + displayName: 'Identifier Type', + description: 'How the identifier above should be interpreted.', + required: false, + defaultValue: 'email_id', + options: { + options: [ + { label: 'Email', value: 'email_id' }, + { label: 'Phone (SMS)', value: 'phone_id' }, + { label: 'Contact ID', value: 'contact_id' }, + { label: 'External ID', value: 'ext_id' }, + { label: 'WhatsApp', value: 'whatsapp_id' }, + { label: 'Landline Number', value: 'landline_number_id' }, + ], + }, + }), + }, + async run(context) { + const { identifier, identifier_type } = context.propsValue; + + try { + const contact = await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.GET, + resourceUri: `/contacts/${encodeURIComponent(identifier)}`, + query: { identifierType: identifier_type }, + }); + + return { found: true, data: contact }; + } catch (error) { + if (error instanceof HttpError && error.response.status === 404) { + return { found: false, data: {} }; + } + throw error; + } + }, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/actions/send-transactional-email.ts b/packages/pieces/community/sendinblue/src/lib/actions/send-transactional-email.ts new file mode 100644 index 000000000000..a47d56cd323d --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/actions/send-transactional-email.ts @@ -0,0 +1,226 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property, isNil } from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; +import { brevoProps } from '../common/props'; +import { sendTransactionalEmailActionOutputSchema } from '../output-schemas'; + +export const sendTransactionalEmail = createAction({ + auth: sendinblueAuth, + name: 'send_transactional_email', + outputSchema: sendTransactionalEmailActionOutputSchema, + classification: 'WRITE', + displayName: 'Send Transactional Email', + description: 'Send an email from your Brevo account with HTML or plain text content.', + audience: 'both', + aiMetadata: { + description: + 'Sends a one-off transactional email through Brevo, either by supplying subject and HTML content directly or by selecting a saved template and passing its variables. Use for receipts, password resets, notifications and other per-recipient mail; not for bulk marketing campaigns. Requires a verified sender unless a template with its own sender is used. Not idempotent — each call sends a new message.', + idempotent: false, + }, + props: { + to: Property.Array({ + displayName: 'To', + description: 'Recipients of the email.', + required: true, + properties: { + email: Property.ShortText({ displayName: 'Email', required: true }), + name: Property.ShortText({ displayName: 'Name', required: false }), + }, + }), + sender_email: brevoProps.senderEmail, + sender_name: Property.ShortText({ + displayName: 'Sender Name', + description: 'Overrides the display name of the selected sender.', + required: false, + }), + template_id: brevoProps.emailTemplateId, + subject: Property.ShortText({ + displayName: 'Subject', + description: 'Required unless a template is selected.', + required: false, + }), + html_content: Property.LongText({ + displayName: 'HTML Content', + description: 'Required unless a template is selected.', + required: false, + }), + text_content: Property.LongText({ + displayName: 'Text Content', + description: 'Plain text alternative shown when HTML cannot be rendered.', + required: false, + }), + params: Property.Object({ + displayName: 'Template Parameters', + description: 'Values substituted into the template placeholders.', + required: false, + }), + cc: Property.Array({ + displayName: 'CC', + required: false, + properties: { + email: Property.ShortText({ displayName: 'Email', required: true }), + name: Property.ShortText({ displayName: 'Name', required: false }), + }, + }), + bcc: Property.Array({ + displayName: 'BCC', + required: false, + properties: { + email: Property.ShortText({ displayName: 'Email', required: true }), + name: Property.ShortText({ displayName: 'Name', required: false }), + }, + }), + reply_to_email: Property.ShortText({ + displayName: 'Reply To', + required: false, + }), + attachments: Property.Array({ + displayName: 'Attachments', + description: 'Files attached by public URL. Brevo downloads each URL when sending.', + required: false, + properties: { + url: Property.ShortText({ displayName: 'URL', required: true }), + name: Property.ShortText({ displayName: 'File Name', required: true }), + }, + }), + tags: Property.Array({ + displayName: 'Tags', + description: 'Labels used to filter this message in Brevo reporting.', + required: false, + }), + scheduled_at: Property.DateTime({ + displayName: 'Scheduled At', + description: 'Send the email at this UTC time instead of immediately.', + required: false, + }), + sandbox: Property.Checkbox({ + displayName: 'Sandbox Mode', + description: + 'Validate the request without delivering anything. Brevo checks the payload, sender and credentials, returns a message id, and drops the message: nothing reaches the recipient and no event is logged.', + required: false, + }), + }, + async run(context) { + const { + to, + sender_email, + sender_name, + template_id, + subject, + html_content, + text_content, + params, + cc, + bcc, + reply_to_email, + attachments, + tags, + scheduled_at, + sandbox, + } = context.propsValue; + + const recipients = toRecipients(to); + if (recipients.length === 0) { + throw new Error('At least one valid recipient email is required in "To".'); + } + + if (isNil(template_id) && (isNil(subject) || isNil(html_content))) { + throw new Error( + 'Provide a Template, or supply both Subject and HTML Content.', + ); + } + + if (isNil(template_id) && isNil(sender_email)) { + throw new Error('A Sender is required when no Template is selected.'); + } + + const body = { + to: recipients, + sender: isNil(sender_email) + ? undefined + : { email: sender_email, name: sender_name ?? undefined }, + templateId: template_id ?? undefined, + subject: subject ?? undefined, + htmlContent: html_content ?? undefined, + textContent: text_content ?? undefined, + params: brevoCommon.isEmptyObject(params) ? undefined : params, + cc: emptyToUndefined(toRecipients(cc)), + bcc: emptyToUndefined(toRecipients(bcc)), + replyTo: isNil(reply_to_email) ? undefined : { email: reply_to_email }, + attachment: emptyToUndefined(toAttachments(attachments)), + tags: emptyToUndefined(toStrings(tags)), + scheduledAt: scheduled_at ?? undefined, + headers: sandbox ? { 'X-Sib-Sandbox': 'drop' } : undefined, + }; + + return await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.POST, + resourceUri: '/smtp/email', + body, + }); + }, +}); + +function toRecipients(value: unknown): BrevoRecipient[] { + if (!Array.isArray(value)) { + return []; + } + + return value.flatMap((entry) => { + if (!isRecord(entry)) { + return []; + } + const email = entry['email']; + if (typeof email !== 'string' || email.length === 0) { + return []; + } + const name = entry['name']; + return [typeof name === 'string' && name.length > 0 ? { email, name } : { email }]; + }); +} + +function toAttachments(value: unknown): BrevoAttachment[] { + if (!Array.isArray(value)) { + return []; + } + + return value.flatMap((entry) => { + if (!isRecord(entry)) { + return []; + } + const url = entry['url']; + const name = entry['name']; + if (typeof url !== 'string' || typeof name !== 'string') { + return []; + } + return [{ url, name }]; + }); +} + +function toStrings(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value.filter((entry): entry is string => typeof entry === 'string'); +} + +function emptyToUndefined(value: T[]): T[] | undefined { + return value.length > 0 ? value : undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + + +export type BrevoRecipient = { + email: string; + name?: string; +}; + +export type BrevoAttachment = { + url: string; + name: string; +}; diff --git a/packages/pieces/community/sendinblue/src/lib/actions/send-transactional-sms.ts b/packages/pieces/community/sendinblue/src/lib/actions/send-transactional-sms.ts new file mode 100644 index 000000000000..91e2073ea458 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/actions/send-transactional-sms.ts @@ -0,0 +1,102 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; +import { sendTransactionalSmsActionOutputSchema } from '../output-schemas'; + +export const sendTransactionalSms = createAction({ + auth: sendinblueAuth, + name: 'send_transactional_sms', + outputSchema: sendTransactionalSmsActionOutputSchema, + classification: 'WRITE', + displayName: 'Send Transactional SMS', + description: 'Send a transactional SMS from your Brevo account.', + audience: 'both', + aiMetadata: { + description: + 'Sends a one-off transactional SMS through Brevo to a single mobile number. The recipient must include the country code without a leading plus or zeros, and the sender is limited to 11 alphanumeric or 15 numeric characters. Content longer than 160 characters is split into multiple messages and billed accordingly. Not idempotent — each call sends a new message.', + idempotent: false, + }, + props: { + sender: Property.ShortText({ + displayName: 'Sender', + description: + 'Name or number shown as the sender. Limited to 11 alphanumeric characters or 15 numeric characters.', + required: true, + }), + recipient: Property.ShortText({ + displayName: 'Recipient', + description: + 'Mobile number with the country code and no leading plus or zeros, for example 33680005003.', + required: true, + }), + content: Property.LongText({ + displayName: 'Content', + description: + 'Message body. Longer than 160 characters is sent as multiple messages.', + required: true, + }), + type: Property.StaticDropdown({ + displayName: 'Type', + required: false, + defaultValue: 'transactional', + options: { + options: [ + { label: 'Transactional', value: 'transactional' }, + { label: 'Marketing', value: 'marketing' }, + ], + }, + }), + tag: Property.ShortText({ + displayName: 'Tag', + description: 'Label used to filter this message in Brevo reporting.', + required: false, + }), + web_url: Property.ShortText({ + displayName: 'Webhook URL', + description: 'URL Brevo posts delivery reports for this message to.', + required: false, + }), + unicode_enabled: Property.Checkbox({ + displayName: 'Unicode Enabled', + description: + 'Send the content as unicode. Unicode messages are limited to 70 characters per part.', + required: false, + }), + organisation_prefix: Property.ShortText({ + displayName: 'Organisation Prefix', + description: 'Brand name prepended to the message content.', + required: false, + }), + }, + async run(context) { + const { + sender, + recipient, + content, + type, + tag, + web_url, + unicode_enabled, + organisation_prefix, + } = context.propsValue; + + const body = { + sender, + recipient, + content, + type, + tag, + webUrl: web_url, + unicodeEnabled: unicode_enabled, + organisationPrefix: organisation_prefix, + }; + + return await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.POST, + resourceUri: '/transactionalSMS/send', + body, + }); + }, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/actions/unsubscribe-contact.ts b/packages/pieces/community/sendinblue/src/lib/actions/unsubscribe-contact.ts new file mode 100644 index 000000000000..c122fcafeae6 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/actions/unsubscribe-contact.ts @@ -0,0 +1,61 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; +import { brevoProps } from '../common/props'; + +export const unsubscribeContact = createAction({ + auth: sendinblueAuth, + name: 'unsubscribe_contact', + classification: 'WRITE', + displayName: 'Unsubscribe Contact', + description: 'Blacklist a contact so it stops receiving email or SMS.', + audience: 'both', + aiMetadata: { + description: + 'Opts a Brevo contact out by setting its email and optionally SMS blacklist flags, and can additionally remove it from specific lists. Use this to honour an unsubscribe request received elsewhere. Brevo answers with an empty body, so this returns a success flag. Idempotent — unsubscribing an already unsubscribed contact changes nothing.', + idempotent: true, + }, + props: { + email: Property.ShortText({ + displayName: 'Contact Email', + required: true, + }), + unsubscribe_email: Property.Checkbox({ + displayName: 'Unsubscribe From Email', + required: false, + defaultValue: true, + }), + unsubscribe_sms: Property.Checkbox({ + displayName: 'Unsubscribe From SMS', + required: false, + }), + unlink_list_ids: brevoProps.listIds({ + displayName: 'Remove From Lists', + description: 'Lists to remove the contact from as part of unsubscribing.', + }), + }, + async run(context) { + const { email, unsubscribe_email, unsubscribe_sms, unlink_list_ids } = + context.propsValue; + + const unlinkListIds = (unlink_list_ids ?? []) + .map((listId) => Number(listId)) + .filter((listId) => Number.isFinite(listId)); + + const body = { + emailBlacklisted: unsubscribe_email, + smsBlacklisted: unsubscribe_sms, + unlinkListIds: unlinkListIds.length > 0 ? unlinkListIds : undefined, + }; + + await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.PUT, + resourceUri: `/contacts/${encodeURIComponent(email)}`, + body, + }); + + return { success: true }; + }, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/auth.ts b/packages/pieces/community/sendinblue/src/lib/auth.ts new file mode 100644 index 000000000000..0af85813e8b1 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/auth.ts @@ -0,0 +1,7 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; + +export const sendinblueAuth = PieceAuth.SecretText({ + displayName: 'Project API key', + description: 'Your project API key', + required: true, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/common/index.ts b/packages/pieces/community/sendinblue/src/lib/common/index.ts new file mode 100644 index 000000000000..fae822285d42 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/common/index.ts @@ -0,0 +1,66 @@ +import { + HttpMessageBody, + HttpMethod, + QueryParams, + httpClient, +} from '@activepieces/pieces-common'; +import { isNil } from '@activepieces/pieces-framework'; + +async function apiCall({ + apiKey, + method, + resourceUri, + query, + body, +}: BrevoApiCallParams): Promise { + const queryParams: QueryParams = {}; + if (query) { + for (const [key, value] of Object.entries(query)) { + if (!isNil(value)) { + queryParams[key] = String(value); + } + } + } + + const response = await httpClient.sendRequest({ + method, + url: `${BREVO_API_URL}${resourceUri}`, + headers: { + 'api-key': apiKey, + accept: 'application/json', + }, + queryParams, + body: compactBody(body), + }); + + return response.body; +} + +function compactBody(body: unknown): unknown { + if (!isPlainObject(body)) { + return body; + } + return Object.fromEntries( + Object.entries(body).filter(([, value]) => !isNil(value)), + ); +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isEmptyObject(value: Record | undefined): boolean { + return isNil(value) || Object.keys(value).length === 0; +} + +export const brevoCommon = { apiCall, isEmptyObject }; + +export const BREVO_API_URL = 'https://api.brevo.com/v3'; + +export type BrevoApiCallParams = { + apiKey: string; + method: HttpMethod; + resourceUri: string; + query?: Record; + body?: unknown; +}; diff --git a/packages/pieces/community/sendinblue/src/lib/common/props.ts b/packages/pieces/community/sendinblue/src/lib/common/props.ts new file mode 100644 index 000000000000..720ffc235ce0 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/common/props.ts @@ -0,0 +1,168 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { Property } from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '.'; + +const PAGE_SIZE = 50; + +async function fetchAllLists(apiKey: string): Promise { + const lists: BrevoList[] = []; + let offset = 0; + let hasMore = true; + + while (hasMore) { + const response = await brevoCommon.apiCall({ + apiKey, + method: HttpMethod.GET, + resourceUri: '/contacts/lists', + query: { limit: PAGE_SIZE, offset }, + }); + + const page = response.lists ?? []; + lists.push(...page); + offset += PAGE_SIZE; + hasMore = page.length === PAGE_SIZE; + } + + return lists; +} + +async function fetchAllTemplates(apiKey: string): Promise { + const templates: BrevoTemplate[] = []; + let offset = 0; + let hasMore = true; + + while (hasMore) { + const response = await brevoCommon.apiCall({ + apiKey, + method: HttpMethod.GET, + resourceUri: '/smtp/templates', + query: { limit: PAGE_SIZE, offset }, + }); + + const page = response.templates ?? []; + templates.push(...page); + offset += PAGE_SIZE; + hasMore = page.length === PAGE_SIZE; + } + + return templates; +} + +function connectFirst() { + return { + disabled: true, + placeholder: 'Connect your Brevo account first.', + options: [], + }; +} + +export const brevoProps = { + listIds: ({ displayName, description }: ListIdsPropParams) => + Property.MultiSelectDropdown({ + displayName, + description, + required: false, + auth: sendinblueAuth, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return connectFirst(); + } + + const lists = await fetchAllLists(auth.secret_text); + + return { + disabled: false, + options: lists.map((list) => ({ + label: list.name, + value: String(list.id), + })), + }; + }, + }), + senderEmail: Property.Dropdown({ + displayName: 'Sender', + description: 'The verified sender the email is sent from.', + required: false, + auth: sendinblueAuth, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return connectFirst(); + } + + const response = await brevoCommon.apiCall({ + apiKey: auth.secret_text, + method: HttpMethod.GET, + resourceUri: '/senders', + }); + + return { + disabled: false, + options: (response.senders ?? []).map((sender) => ({ + label: sender.name ? `${sender.name} <${sender.email}>` : sender.email, + value: sender.email, + })), + }; + }, + }), + emailTemplateId: Property.Dropdown({ + displayName: 'Template', + description: + 'Send a saved Brevo template instead of supplying subject and content.', + required: false, + auth: sendinblueAuth, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return connectFirst(); + } + + const templates = await fetchAllTemplates(auth.secret_text); + + return { + disabled: false, + options: templates.map((template) => ({ + label: template.name, + value: template.id, + })), + }; + }, + }), +}; + +export type BrevoList = { + id: number; + name: string; +}; + +export type BrevoListsResponse = { + lists: BrevoList[]; + count: number; +}; + +export type BrevoTemplate = { + id: number; + name: string; +}; + +export type BrevoTemplatesResponse = { + templates: BrevoTemplate[]; + count: number; +}; + +export type BrevoSender = { + id: number; + name?: string; + email: string; +}; + +export type BrevoSendersResponse = { + senders: BrevoSender[]; +}; + +export type ListIdsPropParams = { + displayName: string; + description: string; +}; diff --git a/packages/pieces/community/sendinblue/src/lib/output-schemas.ts b/packages/pieces/community/sendinblue/src/lib/output-schemas.ts new file mode 100644 index 000000000000..9519cf322145 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/output-schemas.ts @@ -0,0 +1,179 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +const contactFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Contact ID', format: 'number' }, + { key: 'email', label: 'Email', format: 'email' }, + { + key: 'attributes', + label: 'Attributes', + description: 'Account specific contact attributes such as FIRSTNAME or SMS.', + dynamicKey: true, + }, + { key: 'listIds', label: 'List IDs', description: 'Lists the contact belongs to.' }, + { key: 'emailBlacklisted', label: 'Email Blacklisted', format: 'boolean' }, + { key: 'smsBlacklisted', label: 'SMS Blacklisted', format: 'boolean' }, + { key: 'whatsappBlacklisted', label: 'WhatsApp Blacklisted', format: 'boolean' }, + { key: 'createdAt', label: 'Created At', format: 'datetime' }, + { key: 'modifiedAt', label: 'Modified At', format: 'datetime' }, + { + key: 'statistics', + label: 'Statistics', + description: 'Engagement counters, populated once the contact has campaign activity.', + }, +]; + +const marketingEventFields: OutputSchema['fields'] = [ + { key: 'event', label: 'Event' }, + { key: 'email', label: 'Email', format: 'email' }, + { + key: 'id', + label: 'Webhook ID', + description: 'The Brevo webhook that delivered this event, not the contact id.', + format: 'number', + }, + { key: 'date', label: 'Date', format: 'datetime' }, + { key: 'ts', label: 'Timestamp (seconds)', format: 'number' }, +]; + +const transactionalEventFields: OutputSchema['fields'] = [ + { key: 'event', label: 'Event' }, + { key: 'email', label: 'Recipient', format: 'email' }, + { key: 'subject', label: 'Subject' }, + { key: 'message-id', label: 'Message ID' }, + { key: 'uuid', label: 'Event UUID' }, + { key: 'sender_email', label: 'Sender', format: 'email' }, + { key: 'tags', label: 'Tags' }, + { key: 'sending_ip', label: 'Sending IP' }, + { key: 'date', label: 'Date', format: 'datetime' }, + { key: 'ts_event', label: 'Event Timestamp (seconds)', format: 'number' }, + { key: 'ts_epoch', label: 'Event Timestamp', format: 'datetime' }, + { + key: 'id', + label: 'Webhook ID', + description: 'The Brevo webhook that delivered this event, not the message id.', + format: 'number', + }, +]; + +const deliveryEventFields: OutputSchema['fields'] = [ + ...transactionalEventFields, + { + key: 'reason', + label: 'Reason', + description: 'Why the message reached this state, for example sent or an MX lookup failure.', + }, +]; + +const engagementEventFields: OutputSchema['fields'] = [ + ...transactionalEventFields, + { key: 'user_agent', label: 'User Agent' }, + { key: 'device_used', label: 'Device Used' }, + { + key: 'contact_id', + label: 'Contact ID', + description: 'The Brevo contact that engaged. Unlike the top level id, this is a real contact id.', + format: 'number', + }, +]; + +export const createOrUpdateContactActionOutputSchema: OutputSchema = { + fields: contactFields, +}; + +export const findContactActionOutputSchema: OutputSchema = { + fields: [ + { key: 'found', label: 'Found', format: 'boolean' }, + { key: 'data', label: 'Contact', children: contactFields }, + ], +}; + +export const sendTransactionalEmailActionOutputSchema: OutputSchema = { + fields: [ + { + key: 'messageId', + label: 'Message ID', + description: 'Angle bracketed SMTP message id, matching the message-id on email events.', + }, + ], +}; + +export const sendTransactionalSmsActionOutputSchema: OutputSchema = { + fields: [ + { + key: 'messageId', + label: 'Message ID', + description: + 'Brevo accepts the SMS asynchronously, so a message id here does not confirm delivery.', + format: 'number', + }, + ], +}; + +export const contactAddedToListTriggerOutputSchema: OutputSchema = { + fields: [ + ...marketingEventFields, + { key: 'list_id', label: 'List IDs', description: 'Lists the contact was added to.' }, + ], +}; + +export const contactUpdatedTriggerOutputSchema: OutputSchema = { + fields: [ + ...marketingEventFields, + { + key: 'content', + label: 'Changed Fields', + labelKey: 'email', + listItems: [ + { key: 'email', label: 'Email', format: 'email' }, + { key: 'attributes', label: 'Attributes', dynamicKey: true }, + ], + }, + ], +}; + +export const contactDeletedTriggerOutputSchema: OutputSchema = { + fields: [ + { key: 'event', label: 'Event' }, + { + key: 'email', + label: 'Emails', + description: 'Array of deleted addresses, unlike the single address other contact events send.', + }, + { + key: 'id', + label: 'Webhook ID', + description: 'The Brevo webhook that delivered this event, not the contact id.', + format: 'number', + }, + { key: 'date', label: 'Date', format: 'datetime' }, + { key: 'ts', label: 'Timestamp (seconds)', format: 'number' }, + ], +}; + +export const contactUnsubscribedTriggerOutputSchema: OutputSchema = { + fields: [ + ...marketingEventFields, + { key: 'camp_id', label: 'Campaign ID', format: 'number' }, + { key: 'campaign_name', label: 'Campaign Name' }, + { key: 'list_id', label: 'List IDs' }, + ], +}; + +export const emailDeliveredTriggerOutputSchema: OutputSchema = { + fields: deliveryEventFields, +}; + +export const emailBouncedTriggerOutputSchema: OutputSchema = { + fields: deliveryEventFields, +}; + +export const emailOpenedTriggerOutputSchema: OutputSchema = { + fields: engagementEventFields, +}; + +export const emailClickedTriggerOutputSchema: OutputSchema = { + fields: [ + ...engagementEventFields, + { key: 'link', label: 'Clicked Link', format: 'url' }, + ], +}; diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/contact-added-to-list.ts b/packages/pieces/community/sendinblue/src/lib/triggers/contact-added-to-list.ts new file mode 100644 index 000000000000..8cfdf0079875 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/contact-added-to-list.ts @@ -0,0 +1,22 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE } from './samples'; +import { contactAddedToListTriggerOutputSchema } from '../output-schemas'; + +export const contactAddedToList = brevoRegisterTrigger({ + name: 'contact_added_to_list', + displayName: 'Contact Added to List', + description: 'Triggers when a contact is added to one of your lists.', + aiDescription: + `Fires when a contact is added to any Brevo list. The payload carries the contact email and a list_id array of the lists it was added to. Brevo subscribes at account level, so this fires for every list; filter on list_id downstream when only one list matters. ${WEBHOOK_ID_NOTE}`, + type: 'marketing', + events: ['listAddition'], + sampleData: { + id: 2152070, + event: 'list_addition', + email: 'contact@example.com', + list_id: [2], + date: '2026-08-25 11:02:27', + ts: 1787655747, + }, + outputSchema: contactAddedToListTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/contact-deleted.ts b/packages/pieces/community/sendinblue/src/lib/triggers/contact-deleted.ts new file mode 100644 index 000000000000..2be320d6d9d3 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/contact-deleted.ts @@ -0,0 +1,21 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE } from './samples'; +import { contactDeletedTriggerOutputSchema } from '../output-schemas'; + +export const contactDeleted = brevoRegisterTrigger({ + name: 'contact_deleted', + displayName: 'Contact Deleted', + description: 'Triggers when a contact is deleted.', + aiDescription: + `Fires when a Brevo contact is deleted, so downstream systems can drop the record. Unlike the other contact events the email field is an ARRAY of addresses, and date is an ISO 8601 timestamp rather than the space separated format the other events use. ${WEBHOOK_ID_NOTE}`, + type: 'marketing', + events: ['contactDeleted'], + sampleData: { + id: 2152073, + event: 'contact_deleted', + email: ['contact@example.com'], + date: '2026-08-25T11:04:17.82511Z', + ts: 1787655857, + }, + outputSchema: contactDeletedTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/contact-unsubscribed.ts b/packages/pieces/community/sendinblue/src/lib/triggers/contact-unsubscribed.ts new file mode 100644 index 000000000000..11bcc591a26e --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/contact-unsubscribed.ts @@ -0,0 +1,24 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE } from './samples'; +import { contactUnsubscribedTriggerOutputSchema } from '../output-schemas'; + +export const contactUnsubscribed = brevoRegisterTrigger({ + name: 'contact_unsubscribed', + displayName: 'Contact Unsubscribed', + description: 'Triggers when a contact unsubscribes from a campaign.', + aiDescription: + `Fires when a contact unsubscribes, so opt-outs can be honoured in other systems. The payload carries the contact email and the campaign and list identifiers behind the unsubscribe. ${WEBHOOK_ID_NOTE}`, + type: 'marketing', + events: ['unsubscribed'], + sampleData: { + id: 2152074, + event: 'unsubscribe', + email: 'contact@example.com', + camp_id: 12, + campaign_name: 'My First Campaign', + list_id: [3, 42], + date: '2026-08-25 11:05:00', + ts: 1787655900, + }, + outputSchema: contactUnsubscribedTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/contact-updated.ts b/packages/pieces/community/sendinblue/src/lib/triggers/contact-updated.ts new file mode 100644 index 000000000000..b4140455c487 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/contact-updated.ts @@ -0,0 +1,27 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE } from './samples'; +import { contactUpdatedTriggerOutputSchema } from '../output-schemas'; + +export const contactUpdated = brevoRegisterTrigger({ + name: 'contact_updated', + displayName: 'Contact Updated', + description: 'Triggers when the attributes of a contact are updated.', + aiDescription: + `Fires when an existing Brevo contact is modified. The payload carries the contact email plus a content array whose single entry holds the contact email and an attributes object of the fields that changed. ${WEBHOOK_ID_NOTE}`, + type: 'marketing', + events: ['contactUpdated'], + sampleData: { + id: 2152072, + event: 'contact_updated', + email: 'contact@example.com', + date: '2026-08-25 11:04:09', + ts: 1787655849, + content: [ + { + email: 'contact@example.com', + attributes: { FIRSTNAME: 'Elly' }, + }, + ], + }, + outputSchema: contactUpdatedTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/email-bounced.ts b/packages/pieces/community/sendinblue/src/lib/triggers/email-bounced.ts new file mode 100644 index 000000000000..5871f00dba2c --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/email-bounced.ts @@ -0,0 +1,20 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE, brevoSamples } from './samples'; +import { emailBouncedTriggerOutputSchema } from '../output-schemas'; + +export const emailBounced = brevoRegisterTrigger({ + name: 'email_bounced', + displayName: 'Transactional Email Bounced', + description: 'Triggers when a transactional email hard or soft bounces.', + aiDescription: + `Fires when a transactional email sent from this Brevo account bounces. Covers both hard and soft bounces: read the event field to tell them apart, where hard_bounce means permanently undeliverable and soft_bounce a temporary failure, and read reason for the mail server explanation. ${WEBHOOK_ID_NOTE}`, + type: 'transactional', + events: ['hardBounce', 'softBounce'], + sampleData: { + ...brevoSamples.transactionalEmail, + email: 'invalid@example.invalid', + event: 'soft_bounce', + reason: 'Unable to find MX of domain example.invalid', + }, + outputSchema: emailBouncedTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/email-clicked.ts b/packages/pieces/community/sendinblue/src/lib/triggers/email-clicked.ts new file mode 100644 index 000000000000..297c1ce5b09f --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/email-clicked.ts @@ -0,0 +1,19 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE, brevoSamples } from './samples'; +import { emailClickedTriggerOutputSchema } from '../output-schemas'; + +export const emailClicked = brevoRegisterTrigger({ + name: 'email_clicked', + displayName: 'Transactional Email Link Clicked', + description: 'Triggers when a recipient clicks a link in a transactional email.', + aiDescription: + `Fires when a recipient clicks a link inside a transactional email sent from this Brevo account. The payload adds the clicked link URL to the usual message and device fields, so use it to score intent or route follow ups. ${WEBHOOK_ID_NOTE}`, + type: 'transactional', + events: ['click'], + sampleData: { + ...brevoSamples.engagement, + event: 'click', + link: 'https://example.com/product', + }, + outputSchema: emailClickedTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/email-delivered.ts b/packages/pieces/community/sendinblue/src/lib/triggers/email-delivered.ts new file mode 100644 index 000000000000..462ca93ec2a8 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/email-delivered.ts @@ -0,0 +1,19 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE, brevoSamples } from './samples'; +import { emailDeliveredTriggerOutputSchema } from '../output-schemas'; + +export const emailDelivered = brevoRegisterTrigger({ + name: 'email_delivered', + displayName: 'Transactional Email Delivered', + description: 'Triggers when a transactional email is delivered to the recipient.', + aiDescription: + `Fires when a transactional email sent from this Brevo account reaches the recipient mail server. The payload carries the recipient email, message-id, subject, sender_email and uuid. Fires for every transactional email on the account, so filter on tags or subject to narrow it. Note tags is an array while tag is that same array JSON encoded as a string. ${WEBHOOK_ID_NOTE}`, + type: 'transactional', + events: ['delivered'], + sampleData: { + ...brevoSamples.transactionalEmail, + event: 'delivered', + reason: 'sent', + }, + outputSchema: emailDeliveredTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/email-opened.ts b/packages/pieces/community/sendinblue/src/lib/triggers/email-opened.ts new file mode 100644 index 000000000000..8337f1f59247 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/email-opened.ts @@ -0,0 +1,15 @@ +import { brevoRegisterTrigger } from './register-webhook'; +import { WEBHOOK_ID_NOTE, brevoSamples } from './samples'; +import { emailOpenedTriggerOutputSchema } from '../output-schemas'; + +export const emailOpened = brevoRegisterTrigger({ + name: 'email_opened', + displayName: 'Transactional Email Opened', + description: 'Triggers when a recipient opens a transactional email.', + aiDescription: + `Fires when a recipient opens a transactional email sent from this Brevo account. Alongside the message-id and subject the payload carries user_agent and device_used, so use it to react to engagement. ${WEBHOOK_ID_NOTE}`, + type: 'transactional', + events: ['opened'], + sampleData: { ...brevoSamples.engagement, event: 'opened' }, + outputSchema: emailOpenedTriggerOutputSchema, +}); diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/register-webhook.ts b/packages/pieces/community/sendinblue/src/lib/triggers/register-webhook.ts new file mode 100644 index 000000000000..cca32e27a5e1 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/register-webhook.ts @@ -0,0 +1,94 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { + OutputSchema, + TriggerStrategy, + createTrigger, + isNil, +} from '@activepieces/pieces-framework'; +import { sendinblueAuth } from '../auth'; +import { brevoCommon } from '../common'; + +export const brevoRegisterTrigger = ({ + name, + displayName, + description, + aiDescription, + type, + events, + sampleData, + outputSchema, +}: BrevoTriggerParams) => + createTrigger({ + auth: sendinblueAuth, + name, + displayName, + description, + classification: 'READ', + aiMetadata: { description: aiDescription }, + type: TriggerStrategy.WEBHOOK, + props: {}, + sampleData, + outputSchema, + async onEnable(context) { + const webhook = await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.POST, + resourceUri: '/webhooks', + body: { + url: context.webhookUrl, + description: `Activepieces - ${displayName}`, + events, + type, + }, + }); + + await context.store.put(storeKey(name), { + webhookId: webhook.id, + }); + }, + async onDisable(context) { + const information = await context.store.get( + storeKey(name), + ); + + if (isNil(information)) { + return; + } + + await brevoCommon.apiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.DELETE, + resourceUri: `/webhooks/${information.webhookId}`, + }); + + await context.store.delete(storeKey(name)); + }, + async run(context) { + return [context.payload.body]; + }, + }); + +function storeKey(name: string): string { + return `brevo_webhook_${name}`; +} + +export type BrevoWebhookType = 'marketing' | 'transactional'; + +export type BrevoTriggerParams = { + name: string; + displayName: string; + description: string; + aiDescription: string; + type: BrevoWebhookType; + events: string[]; + sampleData: unknown; + outputSchema: OutputSchema; +}; + +export type BrevoWebhookResponse = { + id: number; +}; + +export type BrevoWebhookInformation = { + webhookId: number; +}; diff --git a/packages/pieces/community/sendinblue/src/lib/triggers/samples.ts b/packages/pieces/community/sendinblue/src/lib/triggers/samples.ts new file mode 100644 index 000000000000..36c669ebe143 --- /dev/null +++ b/packages/pieces/community/sendinblue/src/lib/triggers/samples.ts @@ -0,0 +1,26 @@ +const transactionalEmail = { + id: 2152078, + email: 'contact@example.com', + 'message-id': '<202608251105.84807994032@smtp-relay.mailin.fr>', + date: '2026-08-25 14:05:36', + tags: ['welcome_series'], + tag: '["welcome_series"]', + subject: 'Your receipt', + sending_ip: '77.32.148.25', + ts_event: 1787655936, + ts: 1787655936, + ts_epoch: 1787655936000, + sender_email: 'sender@example.com', + uuid: 'daf7778b-13f8-4db8-9f05-a65c1795ae21', +}; + +const engagement = { + ...transactionalEmail, + user_agent: 'Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0', + device_used: 'DESKTOP', +}; + +export const brevoSamples = { transactionalEmail, engagement }; + +export const WEBHOOK_ID_NOTE = + 'Note that the payload id field is the Brevo webhook id, not the contact id — identify the contact by email.'; diff --git a/packages/server/api/src/app/flows/flow-version/flow-version-migration.service.ts b/packages/server/api/src/app/flows/flow-version/flow-version-migration.service.ts index afe46a6685e6..d46ad005f601 100644 --- a/packages/server/api/src/app/flows/flow-version/flow-version-migration.service.ts +++ b/packages/server/api/src/app/flows/flow-version/flow-version-migration.service.ts @@ -4,12 +4,13 @@ import { FlowVersion, LATEST_FLOW_SCHEMA_VERSION } from '@activepieces/shared' import { FastifyBaseLogger } from 'fastify' import { system } from '../../helper/system/system' import { AppSystemProp } from '../../helper/system/system-props' +import { projectService } from '../../project/project-service' import { flowVersionBackupService } from './flow-version-backup.service' import { flowVersionRepo } from './flow-version.service' import { flowMigrations } from './migrations' export const flowVersionMigrationService = (log: FastifyBaseLogger) => ({ - async migrate(flowVersion: FlowVersion, projectId?: ProjectId): Promise { + async migrate(flowVersion: FlowVersion, projectId?: ProjectId, platformId?: string): Promise { // Early exit if already at latest version if (flowVersion.schemaVersion === LATEST_FLOW_SCHEMA_VERSION) { return flowVersion @@ -17,12 +18,8 @@ export const flowVersionMigrationService = (log: FastifyBaseLogger) => ({ log.info('Starting flow version migration') - const backupFiles = flowVersion.backupFiles ?? {} - if (!isNil(flowVersion.schemaVersion)) { - backupFiles[flowVersion.schemaVersion] = await flowVersionBackupService(log).store(flowVersion) - } - - const { data: migratedFlowVersion, error: migrationError } = await tryCatch(() => flowMigrations.apply(flowVersion, { log, projectId })) + const resolvedPlatformId = platformId ?? (isNil(projectId) ? undefined : await projectService(log).getPlatformId(projectId)) + const { data: migratedFlowVersion, error: migrationError } = await tryCatch(() => flowMigrations.apply(flowVersion, { log, projectId, platformId: resolvedPlatformId })) if (migrationError) { log.error({ migrationError }, '[flowVersionMigration] Failed to migrate flow version') onCallService(log, system.get(AppSystemProp.PAGE_ONCALL_WEBHOOK)).page({ @@ -35,6 +32,15 @@ export const flowVersionMigrationService = (log: FastifyBaseLogger) => ({ throw migrationError } + if (migratedFlowVersion.schemaVersion === flowVersion.schemaVersion) { + return migratedFlowVersion + } + + const backupFiles = flowVersion.backupFiles ?? {} + if (!isNil(flowVersion.schemaVersion)) { + backupFiles[flowVersion.schemaVersion] = await flowVersionBackupService(log).store(flowVersion) + } + await flowVersionRepo().update(flowVersion.id, { schemaVersion: migratedFlowVersion.schemaVersion, ...spreadIfDefined('trigger', migratedFlowVersion.trigger), diff --git a/packages/server/api/src/app/flows/flow-version/flow-version.service.ts b/packages/server/api/src/app/flows/flow-version/flow-version.service.ts index fd9c6bc1bed6..70afbcb67f8e 100644 --- a/packages/server/api/src/app/flows/flow-version/flow-version.service.ts +++ b/packages/server/api/src/app/flows/flow-version/flow-version.service.ts @@ -6,6 +6,7 @@ import { EntityManager, FindOneOptions } from 'typeorm' import { repoFactory } from '../../core/db/repo-factory' import { buildPaginator } from '../../helper/pagination/build-paginator' import { paginationHelper } from '../../helper/pagination/pagination-utils' +import { projectService } from '../../project/project-service' import { userService } from '../../user/user-service' import { sampleDataService } from '../step-run/sample-data.service' import { FlowVersionEntity } from './flow-version-entity' @@ -147,9 +148,10 @@ export const flowVersionService = (log: FastifyBaseLogger) => ({ .orderBy('fv.flowId') .addOrderBy('fv.created', 'DESC') .getMany() + const platformId = isNil(projectId) ? undefined : await projectService(log).getPlatformId(projectId) const migratedEntries = await Promise.all( latestVersions.map(async (version) => { - const migrated = await flowVersionMigrationService(log).migrate(version, projectId) + const migrated = await flowVersionMigrationService(log).migrate(version, projectId, platformId) return [version.flowId, migrated] as const }), ) @@ -225,6 +227,7 @@ export const flowVersionService = (log: FastifyBaseLogger) => ({ removeSampleData = false, entityManager, projectId, + platformId, }: GetFlowVersionOrThrowParams): Promise { const flowVersion: FlowVersion | null = await findOne(log, { where: { @@ -235,7 +238,7 @@ export const flowVersionService = (log: FastifyBaseLogger) => ({ order: { created: 'DESC', }, - }, entityManager, projectId) + }, entityManager, projectId, platformId) if (isNil(flowVersion)) { throw new ActivepiecesError({ @@ -307,12 +310,12 @@ export const flowVersionService = (log: FastifyBaseLogger) => ({ -async function findOne(log: FastifyBaseLogger, options: FindOneOptions, entityManager?: EntityManager, projectId?: ProjectId): Promise { +async function findOne(log: FastifyBaseLogger, options: FindOneOptions, entityManager?: EntityManager, projectId?: ProjectId, platformId?: string): Promise { const flowVersion = await flowVersionRepo(entityManager).findOne(options) if (isNil(flowVersion)) { return null } - return flowVersionMigrationService(log).migrate(flowVersion, projectId) + return flowVersionMigrationService(log).migrate(flowVersion, projectId, platformId) } @@ -369,6 +372,7 @@ type GetFlowVersionOrThrowParams = { removeSampleData?: boolean entityManager?: EntityManager projectId?: ProjectId + platformId?: string } type NewFlowVersion = Omit diff --git a/packages/server/api/src/app/flows/flow-version/migrations/index.ts b/packages/server/api/src/app/flows/flow-version/migrations/index.ts index f9a28cf7bc8d..aaeec414196e 100644 --- a/packages/server/api/src/app/flows/flow-version/migrations/index.ts +++ b/packages/server/api/src/app/flows/flow-version/migrations/index.ts @@ -17,6 +17,7 @@ import { migrateAgentPieceV2 } from './migrate-v2-agent-piece' import { migrateV20GoogleModelPrefix } from './migrate-v20-google-model-prefix' import { migrateV21StepOutputNesting } from './migrate-v21-step-output-nesting' import { migrateV22AgentStepToThinClient } from './migrate-v22-agent-step-to-thin-client' +import { migrateV23UpgradePieceVersions } from './migrate-v23-upgrade-piece-versions' import { migrateAgentPieceV3 } from './migrate-v3-agent-piece' import { migrateAgentPieceV4 } from './migrate-v4-agent-piece' import { migrateHttpToWebhookV5 } from './migrate-v5-http-to-webhook' @@ -28,6 +29,7 @@ import { migrateV9AiPieces } from './migrate-v9-ai-pieces' export type MigrationContext = { log: FastifyBaseLogger projectId?: ProjectId + platformId?: string } export type Migration = { @@ -59,6 +61,7 @@ const migrations: Migration[] = [ migrateV20GoogleModelPrefix, migrateV21StepOutputNesting, migrateV22AgentStepToThinClient, + migrateV23UpgradePieceVersions, ] as const export const flowMigrations = { diff --git a/packages/server/api/src/app/flows/flow-version/migrations/migrate-v23-upgrade-piece-versions.ts b/packages/server/api/src/app/flows/flow-version/migrations/migrate-v23-upgrade-piece-versions.ts new file mode 100644 index 000000000000..4cd4e0fc59ba --- /dev/null +++ b/packages/server/api/src/app/flows/flow-version/migrations/migrate-v23-upgrade-piece-versions.ts @@ -0,0 +1,19 @@ +import { FlowVersion } from '@activepieces/shared' +import { system } from '../../../helper/system/system' +import { pieceUpgradeService } from '../piece-upgrade.service' +import type { Migration, MigrationContext } from '.' + +export const migrateV23UpgradePieceVersions: Migration = { + targetSchemaVersion: '23', + migrate: async (flowVersion: FlowVersion, context?: MigrationContext): Promise => { + const log = context?.log ?? system.globalLogger() + const result = await pieceUpgradeService(log).migrateFlowVersion({ flowVersion, projectId: context?.projectId, platformId: context?.platformId }) + if (!result.migrated) { + return flowVersion + } + return { + ...result.flowVersion, + schemaVersion: '24', + } + }, +} diff --git a/packages/server/api/src/app/flows/flow-version/piece-upgrade.service.ts b/packages/server/api/src/app/flows/flow-version/piece-upgrade.service.ts index f606798713a4..24c9932adb6a 100644 --- a/packages/server/api/src/app/flows/flow-version/piece-upgrade.service.ts +++ b/packages/server/api/src/app/flows/flow-version/piece-upgrade.service.ts @@ -2,11 +2,12 @@ import { isNil, spreadIfDefined, unique } from '@activepieces/core-utils' import { ApplicationEventName, Flow, FlowAction, FlowActionType, FlowPiecesUpgradedEvent, flowStructureUtil, FlowTrigger, FlowTriggerType, FlowVersion } from '@activepieces/shared' import { FastifyBaseLogger } from 'fastify' import { repoFactory } from '../../core/db/repo-factory' +import { redisConnections } from '../../database/redis-connections' import { AuditEventEntity } from '../../ee/audit-logs/audit-event-entity' import { applicationEvents } from '../../helper/application-events' import { projectService } from '../../project/project-service' import { flowRepo } from '../flow/flow.repo' -import { flowVersionRepo } from './flow-version.service' +import { FlowVersionEntity } from './flow-version-entity' import { pieceUpgradeRegister } from './piece-upgrade-register' export const pieceUpgradeService = (log: FastifyBaseLogger) => ({ @@ -16,9 +17,33 @@ export const pieceUpgradeService = (log: FastifyBaseLogger) => ({ async revertFlows({ flowIds }: RevertFlowsParams): Promise { return Promise.all(unique(flowIds).map((flowId) => revertFlow({ flowId, log }))) }, + async migrateFlowVersion({ flowVersion, projectId, platformId }: MigrateFlowVersionParams): Promise { + if (isNil(projectId) || isNil(platformId)) { + return { migrated: false, flowVersion } + } + if (!await isPlatformMigrationEnabled(platformId)) { + return { migrated: false, flowVersion } + } + const { newFlowVersion, decisions } = await resolveFlowVersionUpgrades({ flowVersion, log }) + if (decisions.length > 0) { + await sendUpgradeAuditEvent({ platformId, projectId, flowId: flowVersion.flowId, flowVersionId: flowVersion.id, decisions, log }) + } + return { migrated: true, flowVersion: newFlowVersion } + }, }) const auditEventRepo = repoFactory(AuditEventEntity) +const flowVersionRepo = repoFactory(FlowVersionEntity) +const PIECE_UPGRADE_ENABLED_PLATFORMS_KEY = 'piece_upgrade_enabled_platforms' + +async function isPlatformMigrationEnabled(platformId: string): Promise { + const redis = await redisConnections.useExisting() + const gateExists = await redis.exists(PIECE_UPGRADE_ENABLED_PLATFORMS_KEY) + if (gateExists === 0) { + return true + } + return await redis.sismember(PIECE_UPGRADE_ENABLED_PLATFORMS_KEY, platformId) === 1 +} async function revertFlow({ flowId, log }: RevertFlowParams): Promise { const flow = await flowRepo().findOneBy({ id: flowId }) @@ -125,6 +150,7 @@ async function upgradeFlow({ flowId, projectId, log }: UpgradeFlowParams): Promi if (isNil(flow)) { return { flowId, found: false, upgradedSteps: [] } } + const platformId = await projectService(log).getPlatformId(flow.projectId) const latestVersion = await flowVersionRepo().findOne({ where: { flowId }, order: { created: 'DESC' } }) const versions = [latestVersion] if (!isNil(flow.publishedVersionId) && flow.publishedVersionId !== latestVersion?.id) { @@ -135,41 +161,18 @@ async function upgradeFlow({ flowId, projectId, log }: UpgradeFlowParams): Promi if (isNil(version)) { continue } - upgradedSteps.push(...await upgradeFlowVersion({ flow, flowVersion: version, log })) + upgradedSteps.push(...await upgradeFlowVersion({ flow, platformId, flowVersion: version, log })) } return { flowId, found: true, upgradedSteps } } -async function upgradeFlowVersion({ flow, flowVersion, log }: UpgradeFlowVersionParams): Promise { - const steps = flowStructureUtil.getAllSteps(flowVersion.trigger) - - const decisions: StepUpgradeDecision[] = [] - for (const step of steps) { - const decision = await resolveStepDecision({ step, flowVersion, log }) - if (!isNil(decision)) { - decisions.push(decision) - } - } +async function upgradeFlowVersion({ flow, platformId, flowVersion, log }: UpgradeFlowVersionParams): Promise { + const { newFlowVersion, decisions, upgraded } = await resolveFlowVersionUpgrades({ flowVersion, log }) if (decisions.length === 0) { return [] } - const upgraded = decisions.filter((decision): decision is UpgradedStepDecision => decision.decision === 'UPGRADED') if (upgraded.length > 0) { - const stepNameToNewVersion = Object.fromEntries(upgraded.map((decision) => [decision.stepName, decision.newVersion])) - const newFlowVersion = flowStructureUtil.transferFlow(flowVersion, (step) => { - const newVersion = stepNameToNewVersion[step.name] - if (isNil(newVersion)) { - return step - } - return { - ...step, - settings: { - ...step.settings, - pieceVersion: newVersion, - }, - } - }) const updated = await updateTriggerIfUnchanged({ flowVersion, newTrigger: newFlowVersion.trigger }) if (!updated) { log.warn({ flowVersion: { id: flowVersion.id } }, '[pieceUpgradeService] flow version changed concurrently, skipping upgrade') @@ -177,15 +180,7 @@ async function upgradeFlowVersion({ flow, flowVersion, log }: UpgradeFlowVersion } } - const platformId = await projectService(log).getPlatformId(flow.projectId) - applicationEvents(log).sendUserEvent({ platformId, projectId: flow.projectId }, { - action: ApplicationEventName.FLOW_PIECES_UPGRADED, - data: { - flowId: flow.id, - flowVersionId: flowVersion.id, - steps: decisions.map(toLogStep), - }, - }) + await sendUpgradeAuditEvent({ platformId, projectId: flow.projectId, flowId: flow.id, flowVersionId: flowVersion.id, decisions, log }) return upgraded.map((decision) => ({ flowVersionId: flowVersion.id, @@ -196,6 +191,50 @@ async function upgradeFlowVersion({ flow, flowVersion, log }: UpgradeFlowVersion })) } +async function resolveFlowVersionUpgrades({ flowVersion, log }: ResolveFlowVersionUpgradesParams): Promise { + const steps = flowStructureUtil.getAllSteps(flowVersion.trigger) + + const decisions: StepUpgradeDecision[] = [] + for (const step of steps) { + const decision = await resolveStepDecision({ step, flowVersion, log }) + if (!isNil(decision)) { + decisions.push(decision) + } + } + + const upgraded = decisions.filter((decision): decision is UpgradedStepDecision => decision.decision === 'UPGRADED') + if (upgraded.length === 0) { + return { newFlowVersion: flowVersion, decisions, upgraded } + } + + const stepNameToNewVersion = Object.fromEntries(upgraded.map((decision) => [decision.stepName, decision.newVersion])) + const newFlowVersion = flowStructureUtil.transferFlow(flowVersion, (step) => { + const newVersion = stepNameToNewVersion[step.name] + if (isNil(newVersion)) { + return step + } + return { + ...step, + settings: { + ...step.settings, + pieceVersion: newVersion, + }, + } + }) + return { newFlowVersion, decisions, upgraded } +} + +async function sendUpgradeAuditEvent({ platformId, projectId, flowId, flowVersionId, decisions, log }: SendUpgradeAuditEventParams): Promise { + applicationEvents(log).sendUserEvent({ platformId, projectId }, { + action: ApplicationEventName.FLOW_PIECES_UPGRADED, + data: { + flowId, + flowVersionId, + steps: decisions.map(toLogStep), + }, + }) +} + function toLogStep(decision: StepUpgradeDecision): PieceUpgradeAuditStep { return { stepName: decision.stepName, @@ -324,6 +363,37 @@ type StepRevert = { newVersion: string } +type MigrateFlowVersionParams = { + flowVersion: FlowVersion + projectId?: string + platformId?: string +} + +type FlowVersionMigrationResult = { + migrated: boolean + flowVersion: FlowVersion +} + +type ResolveFlowVersionUpgradesParams = { + flowVersion: FlowVersion + log: FastifyBaseLogger +} + +type FlowVersionUpgrades = { + newFlowVersion: FlowVersion + decisions: StepUpgradeDecision[] + upgraded: UpgradedStepDecision[] +} + +type SendUpgradeAuditEventParams = { + platformId: string + projectId: string + flowId: string + flowVersionId: string + decisions: StepUpgradeDecision[] + log: FastifyBaseLogger +} + type UpdateTriggerIfUnchangedParams = { flowVersion: FlowVersion newTrigger: FlowVersion['trigger'] @@ -345,6 +415,7 @@ type UpgradeFlowParams = { type UpgradeFlowVersionParams = { flow: Flow + platformId: string flowVersion: FlowVersion log: FastifyBaseLogger } diff --git a/packages/server/api/src/app/flows/flow/flow.service.ts b/packages/server/api/src/app/flows/flow/flow.service.ts index e650239b65aa..9cde186ab2a3 100644 --- a/packages/server/api/src/app/flows/flow/flow.service.ts +++ b/packages/server/api/src/app/flows/flow/flow.service.ts @@ -202,7 +202,7 @@ export const flowService = (log: FastifyBaseLogger) => ({ }, }) } - const migratedVersion = await flowVersionMigrationService(log).migrate(flow.version, flow.projectId) + const migratedVersion = await flowVersionMigrationService(log).migrate(flow.version, flow.projectId, platformId) return { ...flow, version: migratedVersion, diff --git a/packages/server/engine/src/lib/core/piece/piece-protocol.ts b/packages/server/engine/src/lib/core/piece/piece-protocol.ts index 07d0143382c6..24705a29d175 100644 --- a/packages/server/engine/src/lib/core/piece/piece-protocol.ts +++ b/packages/server/engine/src/lib/core/piece/piece-protocol.ts @@ -1,3 +1,4 @@ +import { inspect } from 'node:util' import { isNil, isObject } from '@activepieces/core-utils' import { ContextVersion, PieceMetadata } from '@activepieces/pieces-framework' import { ExecutionError, ExecutionErrorType, ExecutionType, PropertySettings, ResumePayload, ScheduleOptions } from '@activepieces/shared' @@ -24,19 +25,18 @@ export const pieceProtocol = { if (!(error instanceof Error)) { return { message: String(error) } } - const details: Record = {} - for (const key of [...Object.keys(error), ...ERROR_DETAIL_KEYS]) { - const { data } = readJsonSafe(() => Reflect.get(error, key)) - if (data !== undefined) { - details[key] = data - } - } + const details = Object.fromEntries( + [...Object.keys(error), ...ERROR_DETAIL_KEYS] + .map((key) => [key, readJsonSafe(() => Reflect.get(error, key)).data] as const) + .filter(([, data]) => data !== undefined), + ) return { ...details, message: error.message, name: error.name === 'Error' ? error.constructor.name : error.name, stack: error.stack, type: error instanceof ExecutionError ? error.type : undefined, + cause: isNil(error.cause) ? undefined : inspect(error.cause), } }, diff --git a/packages/server/engine/test/core/piece/piece-protocol-error-cause.test.ts b/packages/server/engine/test/core/piece/piece-protocol-error-cause.test.ts new file mode 100644 index 000000000000..ecf3360192e3 --- /dev/null +++ b/packages/server/engine/test/core/piece/piece-protocol-error-cause.test.ts @@ -0,0 +1,25 @@ +import { inspect } from 'node:util' +import { formatPieceError } from '@activepieces/core-utils' +import { describe, expect, it } from 'vitest' +import { pieceProtocol } from '../../../src/lib/core/piece/piece-protocol' + +function acrossBoundary(error: unknown): Error { + return pieceProtocol.deserializeError(JSON.parse(JSON.stringify(pieceProtocol.serializeError(error)))) +} + +describe('piece protocol error cause', () => { + it('carries an undici cause across the RPC boundary into the raw payload', async () => { + const original = await fetch('http://127.0.0.1:9/nope').catch((error: Error) => error) + expect(original.message).toBe('fetch failed') + + const revived = acrossBoundary(original) + const { raw, message } = formatPieceError(revived, { raw: inspect(revived) }) + + expect(message).toBe('fetch failed') + expect(raw).toContain((original.cause as Error).message) + }) + + it('leaves errors without a cause untouched', () => { + expect(acrossBoundary(new Error('plain')).cause).toBeUndefined() + }) +})