Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions docs/install/reference/breaking-changes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/execution/src/lib/flows/flow-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/pieces/community/google-my-business/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
12 changes: 11 additions & 1 deletion packages/pieces/community/google-my-business/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
},
});
Original file line number Diff line number Diff line change
@@ -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;
},
});
Loading
Loading