Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,73 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, objectDropdown, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const cloneObject = createAction({
auth: googleCloudStorageAuth,
name: 'clone_object',
displayName: 'Clone Object',
description: 'Copy / clone an object (file) to a new location (same or different bucket), optionally overriding metadata. Perfect for duplicating files as backup or version.',
props: {
projectId: projectIdProperty,
sourceBucket: bucketDropdown,
sourceObject: objectDropdown('sourceBucket'),
destBucket: bucketDropdown,
destObject: Property.ShortText({
displayName: 'Destination Object Name',
description: 'Name for the copied object (must be valid object name)',
required: true,
}),
metadataOverrides: Property.Object({
displayName: 'Metadata Overrides',
description: 'Optional metadata and properties to override in the copied object (contentType, cacheControl, contentDisposition, etc.)',
required: false,
}),
},
async run(context) {
const { sourceBucket, sourceObject, destBucket, destObject, metadataOverrides } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const body = metadataOverrides || undefined;
try {
const response = await gcsCommon.makeRequest(
HttpMethod.POST,
`/b/${sourceBucket}/o/${encodeURIComponent(sourceObject as string)}/copyTo/b/${destBucket}/o/${encodeURIComponent(destObject as string)}`,
auth.access_token,
body
);
return {
success: true,
source: {
bucket: sourceBucket,
object: sourceObject,
},
destination: {
bucket: destBucket,
object: destObject,
},
object: response,
};
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(`Source object "${sourceObject}" not found in bucket "${sourceBucket}".`);
}
if (error.response?.status === 403) {
throw new Error('Access denied. Check permissions for source and destination buckets.');
}
if (error.response?.status === 412) {
throw new Error('Precondition failed. Source object may have been modified.');
}
if (error.response?.status === 409) {
throw new Error(`Destination object "${destObject}" already exists in bucket "${destBucket}".`);
}
if (error.response?.status === 400) {
throw new Error('Invalid request. Check object names and bucket configurations.');
}
throw new Error(`Failed to clone object: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,56 @@
import { createAction, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, aclEntityProperty, bucketAclRoleProperty, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const createBucketAcl = createAction({
auth: googleCloudStorageAuth,
name: 'create_bucket_acl',
displayName: 'Create Bucket ACL',
description: 'Add an ACL entry at bucket level. Perfect for granting permission to manage the bucket.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
entity: aclEntityProperty,
role: bucketAclRoleProperty,
},
async run(context) {
const { bucket, entity, role } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const aclEntry = {
entity,
role,
};
try {
const response = await gcsCommon.makeRequest(
HttpMethod.POST,
`/b/${bucket}/acl`,
auth.access_token,
aclEntry
);
return {
success: true,
bucket,
acl: response,
};
} catch (error: any) {
if (error.response?.status === 400) {
throw new Error('Bad request. This bucket may have uniform bucket-level access enabled, which doesn\'t support bucket ACLs. Use IAM policies instead.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.buckets.update permission to modify bucket ACLs.');
}
if (error.response?.status === 404) {
throw new Error(`Bucket "${bucket}" not found.`);
}
if (error.response?.status === 409) {
throw new Error(`ACL entry for entity "${entity}" already exists on bucket "${bucket}".`);
}
throw new Error(`Failed to create bucket ACL: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,56 @@
import { createAction, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, aclEntityProperty, objectAclRoleProperty, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const createBucketDefaultObjectAcl = createAction({
auth: googleCloudStorageAuth,
name: 'create_bucket_default_object_acl',
displayName: 'Create Bucket Default Object ACL',
description: 'Set default ACLs for new objects added to a bucket. Perfect for automatically assigning permissions to new uploads.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
entity: aclEntityProperty,
role: objectAclRoleProperty,
},
async run(context) {
const { bucket, entity, role } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const aclEntry = {
entity,
role,
};
try {
const response = await gcsCommon.makeRequest(
HttpMethod.POST,
`/b/${bucket}/defaultObjectAcl`,
auth.access_token,
aclEntry
);
return {
success: true,
bucket,
acl: response,
};
} catch (error: any) {
if (error.response?.status === 400) {
throw new Error('Bad request. This bucket may have uniform bucket-level access enabled, which doesn\'t support default object ACLs. Use IAM policies instead.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.buckets.update permission to modify bucket default object ACLs.');
}
if (error.response?.status === 404) {
throw new Error(`Bucket "${bucket}" not found.`);
}
if (error.response?.status === 409) {
throw new Error(`Default object ACL entry for entity "${entity}" already exists on bucket "${bucket}".`);
}
throw new Error(`Failed to create bucket default object ACL: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,73 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { projectIdProperty, bucketNameProperty, locationProperty, storageClassProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const createBucket = createAction({
auth: googleCloudStorageAuth,
name: 'create_bucket',
displayName: 'Create Bucket',
description: 'Create a new bucket in a specified location/configuration. Perfect for automating storage provisioning for new projects.',
props: {
projectId: projectIdProperty,
name: bucketNameProperty,
location: locationProperty,
storageClass: storageClassProperty,
versioning: Property.Checkbox({
displayName: 'Enable Versioning',
required: false,
}),
uniformBucketLevelAccess: Property.Checkbox({
displayName: 'Uniform Bucket Level Access',
required: false,
}),
labels: Property.Object({
displayName: 'Labels',
description: 'Key-value pairs for bucket labels',
required: false,
}),
},
async run(context) {
const { projectId, name, location, storageClass, versioning, uniformBucketLevelAccess, labels } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const bucketConfig: any = {
name,
location: location || 'US',
storageClass: storageClass || 'STANDARD',
};
if (versioning) {
bucketConfig.versioning = { enabled: true };
}
if (uniformBucketLevelAccess !== undefined) {
bucketConfig.iamConfiguration = {
uniformBucketLevelAccess: {
enabled: uniformBucketLevelAccess,
},
};
}
if (labels) {
bucketConfig.labels = labels;
}
try {
const response = await gcsCommon.makeRequest(HttpMethod.POST, `/b?project=${projectId}`, auth.access_token, bucketConfig);
return response;
} catch (error: any) {
if (error.response?.status === 409) {
throw new Error(`Bucket "${name}" already exists. Bucket names must be globally unique.`);
}
if (error.response?.status === 403) {
throw new Error('Access denied. Check your permissions for the selected project.');
}
if (error.response?.status === 400) {
throw new Error('Invalid bucket configuration. Check bucket name format and project settings.');
}
throw new Error(`Failed to create bucket: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,79 @@
import {
createAction,
Property,
OAuth2PropertyValue,
} from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import {
bucketDropdown,
objectDropdown,
aclEntityProperty,
objectAclRoleProperty,
projectIdProperty,
} from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const createObjectAcl = createAction({
auth: googleCloudStorageAuth,
name: 'create_object_acl',
displayName: 'Create Object ACL',
description: 'Add an ACL entry to an object (grant a permission). Perfect for granting read/write access to a user or group.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
object: objectDropdown('bucket'),
entity: aclEntityProperty,
role: objectAclRoleProperty,
generation: Property.Number({
displayName: 'Generation',
description: 'Optional generation number for versioned objects',
required: false,
}),
},
async run(context) {
const { bucket, object, entity, role, generation } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const aclEntry = {
entity,
role,
};
let path = `/b/${bucket}/o/${encodeURIComponent(object as string)}/acl`;
if (generation) {
path += `?generation=${generation}`;
}
try {
const response = await gcsCommon.makeRequest(
HttpMethod.POST,
path,
auth.access_token,
aclEntry
);
return {
success: true,
bucket,
object,
generation: generation || 'latest',
acl: response,
};
} catch (error: any) {
if (error.response?.status === 400) {
throw new Error('Bad request. This bucket may have uniform bucket-level access enabled, which doesn\'t support object ACLs. Use IAM policies instead.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.objects.setIamPolicy permission or OWNER ACL permission on the object.');
}
if (error.response?.status === 404) {
throw new Error(`Object "${object}" not found in bucket "${bucket}".`);
}
if (error.response?.status === 409) {
throw new Error(`ACL entry for entity "${entity}" already exists on this object.`);
}
throw new Error(`Failed to create object ACL: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,50 @@
import { createAction, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, aclEntityProperty, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const deleteBucketAcl = createAction({
auth: googleCloudStorageAuth,
name: 'delete_bucket_acl',
displayName: 'Delete Bucket ACL',
description: 'Remove an ACL entry from a bucket. Perfect for revoking access for a user or group.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
entity: aclEntityProperty,
},
async run(context) {
const { bucket, entity } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
try {
await gcsCommon.makeRequest(
HttpMethod.DELETE,
`/b/${bucket}/acl/${encodeURIComponent(entity)}`,
auth.access_token
);
return {
success: true,
bucket,
entity,
message: `ACL entry for entity "${entity}" removed successfully from bucket "${bucket}"`,
};
} catch (error: any) {
if (error.response?.status === 400) {
throw new Error('Bad request. This bucket may have uniform bucket-level access enabled, which doesn\'t support bucket ACLs. Use IAM policies instead.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.buckets.update permission to modify bucket ACLs.');
}
if (error.response?.status === 404) {
if (error.response?.data?.error?.message?.includes('ACL')) {
throw new Error(`ACL entry for entity "${entity}" not found on bucket "${bucket}".`);
}
throw new Error(`Bucket "${bucket}" not found.`);
}
throw new Error(`Failed to delete bucket ACL: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,50 @@
import { createAction, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, aclEntityProperty, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const deleteBucketDefaultObjectAcl = createAction({
auth: googleCloudStorageAuth,
name: 'delete_bucket_default_object_acl',
displayName: 'Delete Bucket Default Object ACL',
description: 'Remove default ACL settings from a bucket. Perfect for reverting to default behavior.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
entity: aclEntityProperty,
},
async run(context) {
const { bucket, entity } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
try {
await gcsCommon.makeRequest(
HttpMethod.DELETE,
`/b/${bucket}/defaultObjectAcl/${encodeURIComponent(entity)}`,
auth.access_token
);
return {
success: true,
bucket,
entity,
message: `Default object ACL entry for entity "${entity}" removed successfully from bucket "${bucket}"`,
};
} catch (error: any) {
if (error.response?.status === 400) {
throw new Error('Bad request. This bucket may have uniform bucket-level access enabled, which doesn\'t support default object ACLs. Use IAM policies instead.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.buckets.update permission to modify bucket default object ACLs.');
}
if (error.response?.status === 404) {
if (error.response?.data?.error?.message?.includes('ACL')) {
throw new Error(`Default object ACL entry for entity "${entity}" not found on bucket "${bucket}".`);
}
throw new Error(`Bucket "${bucket}" not found.`);
}
throw new Error(`Failed to delete bucket default object ACL: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,40 @@
import { createAction, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const deleteEmptyBucket = createAction({
auth: googleCloudStorageAuth,
name: 'delete_empty_bucket',
displayName: 'Delete Empty Bucket',
description: 'Clean up unused buckets by deleting them if they contain no live objects.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
},
async run(context) {
const { bucket } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
try {
await gcsCommon.makeRequest(HttpMethod.DELETE, `/b/${bucket}`, auth.access_token);
return {
success: true,
message: `Bucket "${bucket}" deleted successfully`,
};
} catch (error: any) {
if (error.response?.status === 409) {
throw new Error(`Bucket "${bucket}" contains live or noncurrent objects and cannot be deleted. Remove all objects first.`);
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.buckets.delete permission to delete this bucket.');
}
if (error.response?.status === 404) {
throw new Error(`Bucket "${bucket}" not found. It may have already been deleted.`);
}
throw new Error(`Failed to delete bucket: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,62 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, objectDropdown, aclEntityProperty, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const deleteObjectAcl = createAction({
auth: googleCloudStorageAuth,
name: 'delete_object_acl',
displayName: 'Delete Object ACL',
description: 'Remove an ACL entry from an object. Perfect for revoking access for a user.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
object: objectDropdown('bucket'),
entity: aclEntityProperty,
generation: Property.Number({
displayName: 'Generation',
description: 'Optional generation number for versioned objects',
required: false,
}),
},
async run(context) {
const { bucket, object, entity, generation } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
let path = `/b/${bucket}/o/${encodeURIComponent(object as string)}/acl/${encodeURIComponent(entity as string)}`;
if (generation) {
path += `?generation=${generation}`;
}
try {
await gcsCommon.makeRequest(HttpMethod.DELETE, path, auth.access_token);
return {
success: true,
bucket,
object,
entity,
generation: generation || 'latest',
message: `ACL entry for entity "${entity}" removed successfully from object "${object}"`,
};
} catch (error: any) {
if (error.response?.status === 400) {
if (entity.includes('user-') && entity.includes('@')) {
throw new Error('Cannot remove OWNER access from the object owner. The only way to remove owner access is to delete or replace the object.');
}
throw new Error('Bad request. This bucket may have uniform bucket-level access enabled, which doesn\'t support object ACLs. Use IAM policies instead.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.objects.setIamPolicy permission or OWNER ACL permission on the object.');
}
if (error.response?.status === 404) {
if (error.response?.data?.error?.message?.includes('ACL')) {
throw new Error(`ACL entry for entity "${entity}" not found on object "${object}".`);
}
throw new Error(`Object "${object}" not found in bucket "${bucket}".`);
}
throw new Error(`Failed to delete object ACL: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,57 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, objectDropdown, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const deleteObject = createAction({
auth: googleCloudStorageAuth,
name: 'delete_object',
displayName: 'Delete Object',
description: 'Permanently delete a specific object. Perfect for removing obsolete files.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
object: objectDropdown('bucket'),
generation: Property.Number({
displayName: 'Generation',
description: 'Optional generation number for versioned objects (permanently deletes a specific revision)',
required: false,
}),
},
async run(context) {
const { bucket, object, generation } = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
let path = `/b/${bucket}/o/${encodeURIComponent(object as string)}`;
if (generation) {
path += `?generation=${generation}`;
}
try {
await gcsCommon.makeRequest(HttpMethod.DELETE, path, auth.access_token);
return {
success: true,
bucket,
object,
generation: generation || 'latest',
message: `Object "${object}" deleted successfully from bucket "${bucket}"`,
};
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(`Object "${object}" not found in bucket "${bucket}". It may have already been deleted.`);
}
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.objects.delete permission to delete this object.');
}
if (error.response?.status === 412) {
throw new Error('Precondition failed. Object may have been modified or deleted by another process.');
}
if (error.response?.status === 400) {
throw new Error('Invalid request. Check object name and generation parameters.');
}
throw new Error(`Failed to delete object: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,90 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const searchBuckets = createAction({
auth: googleCloudStorageAuth,
name: 'search_buckets',
displayName: 'Search Buckets',
description: 'Search buckets by name. Perfect for finding buckets in your project.',
props: {
projectId: projectIdProperty,
prefix: Property.ShortText({
displayName: 'Name Prefix',
description: 'Filter buckets whose names begin with this prefix',
required: false,
}),
includeSoftDeleted: Property.Checkbox({
displayName: 'Include Soft-Deleted',
description: 'Include soft-deleted bucket versions in results',
required: false,
}),
projection: Property.StaticDropdown({
displayName: 'Projection',
description: 'Set of properties to return',
required: false,
options: {
options: [
{ label: 'No ACLs (faster)', value: 'noAcl' },
{ label: 'Full (includes ACLs)', value: 'full' },
],
},
}),
pageToken: Property.ShortText({
displayName: 'Page Token',
description: 'Token for pagination (from previous response)',
required: false,
}),
maxResults: Property.Number({
displayName: 'Max Results',
description: 'Maximum number of buckets to return',
required: false,
}),
},
async run(context) {
const {
projectId,
prefix,
includeSoftDeleted,
projection,
pageToken,
maxResults
} = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const params = new URLSearchParams();
params.append('project', projectId!);
if (prefix) params.append('prefix', prefix);
if (includeSoftDeleted) params.append('softDeleted', 'true');
if (projection) params.append('projection', projection);
if (pageToken) params.append('pageToken', pageToken);
if (maxResults) params.append('maxResults', maxResults.toString());
const path = `/b?${params.toString()}`;
try {
const response = await gcsCommon.makeRequest(HttpMethod.GET, path, auth.access_token);
return {
success: true,
projectId,
items: response.items || [],
nextPageToken: response.nextPageToken,
totalBuckets: response.items?.length || 0,
};
} catch (error: any) {
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.buckets.list permission to search buckets in this project.');
}
if (error.response?.status === 404) {
throw new Error(`Project "${projectId}" not found or you don't have access to it.`);
}
if (error.response?.status === 400) {
throw new Error('Invalid search parameters. Check your project ID and other filters.');
}
throw new Error(`Failed to search buckets: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,99 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
export const searchObjects = createAction({
auth: googleCloudStorageAuth,
name: 'search_objects',
displayName: 'Search Objects',
description: 'Search objects by criteria. Perfect for finding files in your bucket.',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
prefix: Property.ShortText({
displayName: 'Prefix',
description: 'Filter objects whose names begin with this prefix',
required: false,
}),
matchGlob: Property.ShortText({
displayName: 'Glob Pattern',
description: 'Glob pattern to filter results (e.g., "folder/*", "backup-*.txt")',
required: false,
}),
delimiter: Property.ShortText({
displayName: 'Delimiter',
description: 'Delimiter for hierarchical listing (commonly "/")',
required: false,
}),
includeFoldersAsPrefixes: Property.Checkbox({
displayName: 'Include Folders',
description: 'Include empty folders and managed folders in results',
required: false,
}),
versions: Property.Checkbox({
displayName: 'Include Versions',
description: 'List all versions of objects as distinct results',
required: false,
}),
pageToken: Property.ShortText({
displayName: 'Page Token',
description: 'Token for pagination (from previous response)',
required: false,
}),
maxResults: Property.Number({
displayName: 'Max Results',
description: 'Maximum number of objects to return (recommended: ≤1000)',
required: false,
}),
},
async run(context) {
const {
bucket,
prefix,
matchGlob,
delimiter,
includeFoldersAsPrefixes,
versions,
pageToken,
maxResults
} = context.propsValue;
const auth = context.auth as OAuth2PropertyValue;
const params = new URLSearchParams();
if (prefix) params.append('prefix', prefix);
if (matchGlob) params.append('matchGlob', matchGlob);
if (delimiter) params.append('delimiter', delimiter);
if (includeFoldersAsPrefixes) params.append('includeFoldersAsPrefixes', 'true');
if (versions) params.append('versions', 'true');
if (pageToken) params.append('pageToken', pageToken);
if (maxResults) params.append('maxResults', maxResults.toString());
const path = `/b/${bucket}/o?${params.toString()}`;
try {
const response = await gcsCommon.makeRequest(HttpMethod.GET, path, auth.access_token);
return {
success: true,
bucket,
items: response.items || [],
nextPageToken: response.nextPageToken,
prefixes: response.prefixes || [],
totalItems: response.items?.length || 0,
};
} catch (error: any) {
if (error.response?.status === 403) {
throw new Error('Access denied. You need storage.objects.list permission to search objects in this bucket.');
}
if (error.response?.status === 404) {
throw new Error(`Bucket "${bucket}" not found.`);
}
if (error.response?.status === 400) {
throw new Error('Invalid search parameters. Check your prefix, glob pattern, or other filters.');
}
throw new Error(`Failed to search objects: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,14 @@
import { PieceAuth } from '@activepieces/pieces-framework';
export const googleCloudStorageAuth = PieceAuth.OAuth2({
description: '',
authUrl: 'https://accounts.google.com/o/oauth2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
required: true,
scope: [
'https://www.googleapis.com/auth/devstorage.read_write',
'https://www.googleapis.com/auth/devstorage.full_control',
'https://www.googleapis.com/auth/cloud-platform.read-only',
'https://www.googleapis.com/auth/pubsub'
],
});

View File

@@ -0,0 +1,63 @@
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { OAuth2PropertyValue } from '@activepieces/pieces-framework';
export const gcsCommon = {
gcsBaseUrl: 'https://www.googleapis.com/storage/v1',
pubsubBaseUrl: 'https://pubsub.googleapis.com/v1',
async makeGCSRequest(
method: HttpMethod,
path: string,
accessToken: string,
body?: any
): Promise<any> {
const url = path.startsWith('http') ? path : `${this.gcsBaseUrl}${path}`;
const response = await httpClient.sendRequest({
method,
url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
body,
});
return response.body;
},
async makePubSubRequest(
method: HttpMethod,
path: string,
accessToken: string,
body?: any
): Promise<any> {
const url = path.startsWith('http') ? path : `${this.pubsubBaseUrl}${path}`;
const response = await httpClient.sendRequest({
method,
url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
body,
});
return response.body;
},
// For backward compatibility
async makeRequest(
method: HttpMethod,
path: string,
accessToken: string,
body?: any
): Promise<any> {
return this.makeGCSRequest(method, path, accessToken, body);
},
};

View File

@@ -0,0 +1,249 @@
import { Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { gcsCommon } from './client';
import { HttpMethod } from '@activepieces/pieces-common';
import { googleCloudStorageAuth } from './auth';
export const bucketDropdown = Property.Dropdown<string,true,typeof googleCloudStorageAuth>({
displayName: 'Bucket',
required: true,
refreshers: ['projectId'],
auth: googleCloudStorageAuth,
options: async ({ auth, projectId }) => {
if (!auth || !projectId) {
return {
disabled: true,
options: [],
placeholder: projectId
? 'Please connect your account first'
: 'Please enter project ID first',
};
}
try {
const authValue = auth as OAuth2PropertyValue;
const response = await gcsCommon.makeRequest(
HttpMethod.GET,
`/b?project=${projectId}`,
authValue.access_token
);
return {
disabled: false,
options:
response.items?.map((bucket: any) => ({
label: bucket.name,
value: bucket.name,
})) || [],
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Failed to load buckets',
};
}
},
});
export const objectDropdown = (bucketProperty: string) =>
Property.Dropdown<string,true,typeof googleCloudStorageAuth>({
displayName: 'Object',
required: true,
auth: googleCloudStorageAuth,
refreshers: [bucketProperty],
options: async ({ auth, [bucketProperty]: bucket }) => {
if (!auth || !bucket) {
return {
disabled: true,
options: [],
placeholder: bucket
? 'Please connect your account first'
: 'Please select a bucket first',
};
}
try {
const authValue = auth as OAuth2PropertyValue;
const response = await gcsCommon.makeRequest(
HttpMethod.GET,
`/b/${bucket}/o?maxResults=100`,
authValue.access_token
);
return {
disabled: false,
options:
response.items?.map((object: any) => ({
label: object.name,
value: object.name,
})) || [],
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Failed to load objects',
};
}
},
});
export const projectIdProperty = Property.Dropdown<string,true,typeof googleCloudStorageAuth>({
displayName: 'Project',
required: true,
auth: googleCloudStorageAuth,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const authValue = auth as OAuth2PropertyValue;
// Use Google Cloud Resource Manager API to list projects
const response = await gcsCommon.makeRequest(
HttpMethod.GET,
'https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE',
authValue.access_token
);
return {
disabled: false,
options: response.projects?.map((project: any) => ({
label: `${project.displayName || project.name} (${project.projectId})`,
value: project.projectId,
})) || [],
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Failed to load projects. Check your permissions.',
};
}
},
});
export const bucketNameProperty = Property.ShortText({
displayName: 'Bucket Name',
description: 'Unique name for your bucket (must be globally unique, 3-63 characters)',
required: true,
});
export const objectNameProperty = Property.ShortText({
displayName: 'Object Name',
required: true,
});
export const locationProperty = Property.StaticDropdown({
displayName: 'Location',
required: false,
options: {
options: [
// Multi-region
{ label: 'US (Multi-region)', value: 'US' },
{ label: 'EU (Multi-region)', value: 'EU' },
{ label: 'ASIA (Multi-region)', value: 'ASIA' },
// US regions
{ label: 'us-central1 (Iowa)', value: 'us-central1' },
{ label: 'us-east1 (South Carolina)', value: 'us-east1' },
{ label: 'us-east4 (Northern Virginia)', value: 'us-east4' },
{ label: 'us-west1 (Oregon)', value: 'us-west1' },
{ label: 'us-west2 (Los Angeles)', value: 'us-west2' },
{ label: 'us-west3 (Salt Lake City)', value: 'us-west3' },
{ label: 'us-west4 (Las Vegas)', value: 'us-west4' },
{ label: 'us-south1 (Dallas)', value: 'us-south1' },
// Europe regions
{ label: 'europe-central2 (Warsaw)', value: 'europe-central2' },
{ label: 'europe-north1 (Finland)', value: 'europe-north1' },
{ label: 'europe-southwest1 (Madrid)', value: 'europe-southwest1' },
{ label: 'europe-west1 (Belgium)', value: 'europe-west1' },
{ label: 'europe-west2 (London)', value: 'europe-west2' },
{ label: 'europe-west3 (Frankfurt)', value: 'europe-west3' },
{ label: 'europe-west4 (Netherlands)', value: 'europe-west4' },
{ label: 'europe-west6 (Zurich)', value: 'europe-west6' },
{ label: 'europe-west8 (Milan)', value: 'europe-west8' },
{ label: 'europe-west9 (Paris)', value: 'europe-west9' },
// Asia regions
{ label: 'asia-east1 (Taiwan)', value: 'asia-east1' },
{ label: 'asia-east2 (Hong Kong)', value: 'asia-east2' },
{ label: 'asia-northeast1 (Tokyo)', value: 'asia-northeast1' },
{ label: 'asia-northeast2 (Osaka)', value: 'asia-northeast2' },
{ label: 'asia-northeast3 (Seoul)', value: 'asia-northeast3' },
{ label: 'asia-south1 (Mumbai)', value: 'asia-south1' },
{ label: 'asia-south2 (Delhi)', value: 'asia-south2' },
{ label: 'asia-southeast1 (Singapore)', value: 'asia-southeast1' },
{ label: 'asia-southeast2 (Jakarta)', value: 'asia-southeast2' },
// Other regions
{ label: 'australia-southeast1 (Sydney)', value: 'australia-southeast1' },
{ label: 'australia-southeast2 (Melbourne)', value: 'australia-southeast2' },
{ label: 'northamerica-northeast1 (Montreal)', value: 'northamerica-northeast1' },
{ label: 'northamerica-northeast2 (Toronto)', value: 'northamerica-northeast2' },
{ label: 'southamerica-east1 (São Paulo)', value: 'southamerica-east1' },
{ label: 'southamerica-west1 (Santiago)', value: 'southamerica-west1' },
],
},
});
export const storageClassProperty = Property.StaticDropdown({
displayName: 'Storage Class',
required: false,
options: {
options: [
{ label: 'Standard', value: 'STANDARD' },
{ label: 'Nearline', value: 'NEARLINE' },
{ label: 'Coldline', value: 'COLDLINE' },
{ label: 'Archive', value: 'ARCHIVE' },
{ label: 'Multi-regional', value: 'MULTI_REGIONAL' },
{ label: 'Regional', value: 'REGIONAL' },
{ label: 'Durable Reduced Availability', value: 'DURABLE_REDUCED_AVAILABILITY' },
],
},
});
export const aclEntityProperty = Property.ShortText({
displayName: 'Entity',
description: 'The entity to grant access to. Must include the entity type prefix. Format: user-emailAddress, group-groupId, group-emailAddress, domain-domainName, project-team-projectId, allUsers, or allAuthenticatedUsers. Examples: user-liz@example.com, group-mygroup@googlegroups.com, domain-example.com, allUsers',
required: true,
});
export const aclRoleProperty = Property.StaticDropdown({
displayName: 'Role',
required: true,
options: {
options: [
{ label: 'Reader', value: 'READER' },
{ label: 'Writer', value: 'WRITER' },
{ label: 'Owner', value: 'OWNER' },
],
},
});
// For bucket ACLs - supports OWNER, WRITER, READER
export const bucketAclRoleProperty = Property.StaticDropdown({
displayName: 'Role',
required: true,
options: {
options: [
{ label: 'Reader', value: 'READER' },
{ label: 'Writer', value: 'WRITER' },
{ label: 'Owner', value: 'OWNER' },
],
},
});
// For object ACLs - supports OWNER, READER only
export const objectAclRoleProperty = Property.StaticDropdown({
displayName: 'Role',
required: true,
options: {
options: [
{ label: 'Reader', value: 'READER' },
{ label: 'Owner', value: 'OWNER' },
],
},
});

View File

@@ -0,0 +1,171 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
interface TriggerData {
topicName: string;
subscriptionName: string;
notificationId: string;
}
export const newObjectCreated = createTrigger({
auth: googleCloudStorageAuth,
name: 'new_object_created',
displayName: 'New Object Created',
description: 'Triggers when a new object is created in a bucket',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
prefix: Property.ShortText({
displayName: 'Prefix Filter',
description: 'Only trigger for objects with this prefix',
required: false,
}),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
kind: 'storage#object',
id: 'example-bucket/example-object/1234567890',
name: 'example-object.txt',
bucket: 'example-bucket',
generation: '1234567890',
contentType: 'text/plain',
timeCreated: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
size: '1024',
},
onEnable: async (context) => {
const { projectId, bucket, prefix } = context.propsValue;
const auth = context.auth;
// Generate unique names for this trigger instance
const triggerId = `ap-gcs-${bucket}-${Date.now()}`;
const topicName = `projects/${projectId}/topics/${triggerId}`;
const subscriptionName = `projects/${projectId}/subscriptions/${triggerId}`;
try {
// 1. Create Pub/Sub topic
await gcsCommon.makePubSubRequest(
HttpMethod.PUT,
`/projects/${projectId}/topics/${triggerId}`,
auth.access_token
);
// 2. Create GCS notification configuration
const notificationConfig: any = {
topic: topicName,
payload_format: 'JSON_API_V1',
event_types: ['OBJECT_FINALIZE'],
};
if (prefix) {
notificationConfig.object_name_prefix = prefix;
}
const notificationResponse = await gcsCommon.makeGCSRequest(
HttpMethod.POST,
`/b/${bucket}/notificationConfigs`,
auth.access_token,
notificationConfig
);
// 3. Create Pub/Sub subscription with webhook push
const subscriptionConfig = {
topic: topicName,
pushConfig: {
pushEndpoint: context.webhookUrl,
},
ackDeadlineSeconds: 60,
};
await gcsCommon.makePubSubRequest(
HttpMethod.PUT,
`/projects/${projectId}/subscriptions/${triggerId}`,
auth.access_token,
subscriptionConfig
);
// Store trigger data for cleanup
await context.store.put<TriggerData>('_trigger', {
topicName: triggerId,
subscriptionName: triggerId,
notificationId: notificationResponse.id,
});
} catch (error) {
// Cleanup on failure
try {
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/subscriptions/${triggerId}`,
auth.access_token
);
} catch (e) { /* ignore */ }
try {
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/topics/${triggerId}`,
auth.access_token
);
} catch (e) { /* ignore */ }
throw new Error(`Failed to setup Pub/Sub notifications: ${(error as any)?.message || 'Unknown error'}`);
}
},
onDisable: async (context) => {
const triggerData = await context.store.get<TriggerData>('_trigger');
if (!triggerData) return;
const { projectId } = context.propsValue;
const { bucket } = context.propsValue;
const auth = context.auth;
// Clean up in reverse order
try {
// Delete subscription
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/subscriptions/${triggerData.subscriptionName}`,
auth.access_token
);
} catch (e) { /* ignore */ }
try {
// Delete notification config
await gcsCommon.makeGCSRequest(
HttpMethod.DELETE,
`/b/${bucket}/notificationConfigs/${triggerData.notificationId}`,
auth.access_token
);
} catch (e) { /* ignore */ }
try {
// Delete topic
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/topics/${triggerData.topicName}`,
auth.access_token
);
} catch (e) { /* ignore */ }
},
run: async (context) => {
const payload = context.payload.body as any;
if (!payload?.message?.data) {
return [];
}
// Decode Pub/Sub message
const messageData = JSON.parse(
Buffer.from(payload.message.data, 'base64').toString()
);
// Extract GCS object from notification payload
const gcsObject = messageData;
return [gcsObject];
},
});

View File

@@ -0,0 +1,171 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { googleCloudStorageAuth } from '../common/auth';
import { gcsCommon } from '../common/client';
import { bucketDropdown, projectIdProperty } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
interface TriggerData {
topicName: string;
subscriptionName: string;
notificationId: string;
}
export const objectUpdated = createTrigger({
auth: googleCloudStorageAuth,
name: 'object_updated',
displayName: 'Object Updated',
description: 'Triggers when an existing object is updated in a bucket',
props: {
projectId: projectIdProperty,
bucket: bucketDropdown,
prefix: Property.ShortText({
displayName: 'Prefix Filter',
description: 'Only trigger for objects with this prefix',
required: false,
}),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
kind: 'storage#object',
id: 'example-bucket/example-object/1234567890',
name: 'example-object.txt',
bucket: 'example-bucket',
generation: '1234567890',
contentType: 'text/plain',
timeCreated: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T01:00:00.000Z',
size: '2048',
},
onEnable: async (context) => {
const { projectId, bucket, prefix } = context.propsValue;
const auth = context.auth;
// Generate unique names for this trigger instance
const triggerId = `ap-gcs-update-${bucket}-${Date.now()}`;
const topicName = `projects/${projectId}/topics/${triggerId}`;
const subscriptionName = `projects/${projectId}/subscriptions/${triggerId}`;
try {
// 1. Create Pub/Sub topic
await gcsCommon.makePubSubRequest(
HttpMethod.PUT,
`/projects/${projectId}/topics/${triggerId}`,
auth.access_token
);
// 2. Create GCS notification configuration for metadata updates
const notificationConfig: any = {
topic: topicName,
payload_format: 'JSON_API_V1',
event_types: ['OBJECT_METADATA_UPDATE'],
};
if (prefix) {
notificationConfig.object_name_prefix = prefix;
}
const notificationResponse = await gcsCommon.makeGCSRequest(
HttpMethod.POST,
`/b/${bucket}/notificationConfigs`,
auth.access_token,
notificationConfig
);
// 3. Create Pub/Sub subscription with webhook push
const subscriptionConfig = {
topic: topicName,
pushConfig: {
pushEndpoint: context.webhookUrl,
},
ackDeadlineSeconds: 60,
};
await gcsCommon.makePubSubRequest(
HttpMethod.PUT,
`/projects/${projectId}/subscriptions/${triggerId}`,
auth.access_token,
subscriptionConfig
);
// Store trigger data for cleanup
await context.store.put<TriggerData>('_trigger', {
topicName: triggerId,
subscriptionName: triggerId,
notificationId: notificationResponse.id,
});
} catch (error) {
// Cleanup on failure
try {
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/subscriptions/${triggerId}`,
auth.access_token
);
} catch (e) { /* ignore */ }
try {
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/topics/${triggerId}`,
auth.access_token
);
} catch (e) { /* ignore */ }
throw new Error(`Failed to setup Pub/Sub notifications: ${(error as any)?.message || 'Unknown error'}`);
}
},
onDisable: async (context) => {
const triggerData = await context.store.get<TriggerData>('_trigger');
if (!triggerData) return;
const { projectId } = context.propsValue;
const { bucket } = context.propsValue;
const auth = context.auth;
// Clean up in reverse order
try {
// Delete subscription
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/subscriptions/${triggerData.subscriptionName}`,
auth.access_token
);
} catch (e) { /* ignore */ }
try {
// Delete notification config
await gcsCommon.makeGCSRequest(
HttpMethod.DELETE,
`/b/${bucket}/notificationConfigs/${triggerData.notificationId}`,
auth.access_token
);
} catch (e) { /* ignore */ }
try {
// Delete topic
await gcsCommon.makePubSubRequest(
HttpMethod.DELETE,
`/projects/${projectId}/topics/${triggerData.topicName}`,
auth.access_token
);
} catch (e) { /* ignore */ }
},
run: async (context) => {
const payload = context.payload.body as any;
if (!payload?.message?.data) {
return [];
}
// Decode Pub/Sub message
const messageData = JSON.parse(
Buffer.from(payload.message.data, 'base64').toString()
);
// Extract GCS object from notification payload
const gcsObject = messageData;
return [gcsObject];
},
});