@guayaba/workflow-piece-google-cloud-storage 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ import { createAction, Property, OAuth2PropertyValue } from '@guayaba/workflows-framework';
2
+ import { googleCloudStorageAuth } from '../common/auth';
3
+ import { gcsCommon } from '../common/client';
4
+ import { bucketDropdown, projectIdProperty } from '../common/props';
5
+ import { HttpMethod } from '@guayaba/workflows-common';
6
+
7
+ export const searchObjects = createAction({
8
+ auth: googleCloudStorageAuth,
9
+ name: 'search_objects',
10
+ displayName: 'Search Objects',
11
+ description: 'Search objects by criteria. Perfect for finding files in your bucket.',
12
+ props: {
13
+ projectId: projectIdProperty,
14
+ bucket: bucketDropdown,
15
+ prefix: Property.ShortText({
16
+ displayName: 'Prefix',
17
+ description: 'Filter objects whose names begin with this prefix',
18
+ required: false,
19
+ }),
20
+ matchGlob: Property.ShortText({
21
+ displayName: 'Glob Pattern',
22
+ description: 'Glob pattern to filter results (e.g., "folder/*", "backup-*.txt")',
23
+ required: false,
24
+ }),
25
+ delimiter: Property.ShortText({
26
+ displayName: 'Delimiter',
27
+ description: 'Delimiter for hierarchical listing (commonly "/")',
28
+ required: false,
29
+ }),
30
+ includeFoldersAsPrefixes: Property.Checkbox({
31
+ displayName: 'Include Folders',
32
+ description: 'Include empty folders and managed folders in results',
33
+ required: false,
34
+ }),
35
+ versions: Property.Checkbox({
36
+ displayName: 'Include Versions',
37
+ description: 'List all versions of objects as distinct results',
38
+ required: false,
39
+ }),
40
+ pageToken: Property.ShortText({
41
+ displayName: 'Page Token',
42
+ description: 'Token for pagination (from previous response)',
43
+ required: false,
44
+ }),
45
+ maxResults: Property.Number({
46
+ displayName: 'Max Results',
47
+ description: 'Maximum number of objects to return (recommended: ≤1000)',
48
+ required: false,
49
+ }),
50
+ },
51
+ async run(context) {
52
+ const {
53
+ bucket,
54
+ prefix,
55
+ matchGlob,
56
+ delimiter,
57
+ includeFoldersAsPrefixes,
58
+ versions,
59
+ pageToken,
60
+ maxResults
61
+ } = context.propsValue;
62
+ const auth = context.auth as OAuth2PropertyValue;
63
+
64
+ const params = new URLSearchParams();
65
+ if (prefix) params.append('prefix', prefix);
66
+ if (matchGlob) params.append('matchGlob', matchGlob);
67
+ if (delimiter) params.append('delimiter', delimiter);
68
+ if (includeFoldersAsPrefixes) params.append('includeFoldersAsPrefixes', 'true');
69
+ if (versions) params.append('versions', 'true');
70
+ if (pageToken) params.append('pageToken', pageToken);
71
+ if (maxResults) params.append('maxResults', maxResults.toString());
72
+
73
+ const path = `/b/${bucket}/o?${params.toString()}`;
74
+
75
+ try {
76
+ const response = await gcsCommon.makeRequest(HttpMethod.GET, path, auth.access_token);
77
+
78
+ return {
79
+ success: true,
80
+ bucket,
81
+ items: response.items || [],
82
+ nextPageToken: response.nextPageToken,
83
+ prefixes: response.prefixes || [],
84
+ totalItems: response.items?.length || 0,
85
+ };
86
+ } catch (error: any) {
87
+ if (error.response?.status === 403) {
88
+ throw new Error('Access denied. You need storage.objects.list permission to search objects in this bucket.');
89
+ }
90
+ if (error.response?.status === 404) {
91
+ throw new Error(`Bucket "${bucket}" not found.`);
92
+ }
93
+ if (error.response?.status === 400) {
94
+ throw new Error('Invalid search parameters. Check your prefix, glob pattern, or other filters.');
95
+ }
96
+ throw new Error(`Failed to search objects: ${error.message || 'Unknown error'}`);
97
+ }
98
+ },
99
+ });
@@ -0,0 +1,14 @@
1
+ import { PieceAuth } from '@guayaba/workflows-framework';
2
+
3
+ export const googleCloudStorageAuth = PieceAuth.OAuth2({
4
+ description: '',
5
+ authUrl: 'https://accounts.google.com/o/oauth2/auth',
6
+ tokenUrl: 'https://oauth2.googleapis.com/token',
7
+ required: true,
8
+ scope: [
9
+ 'https://www.googleapis.com/auth/devstorage.read_write',
10
+ 'https://www.googleapis.com/auth/devstorage.full_control',
11
+ 'https://www.googleapis.com/auth/cloud-platform.read-only',
12
+ 'https://www.googleapis.com/auth/pubsub'
13
+ ],
14
+ });
@@ -0,0 +1,63 @@
1
+ import {
2
+ httpClient,
3
+ HttpMethod,
4
+ AuthenticationType,
5
+ } from '@guayaba/workflows-common';
6
+ import { OAuth2PropertyValue } from '@guayaba/workflows-framework';
7
+
8
+ export const gcsCommon = {
9
+ gcsBaseUrl: 'https://www.googleapis.com/storage/v1',
10
+ pubsubBaseUrl: 'https://pubsub.googleapis.com/v1',
11
+
12
+ async makeGCSRequest(
13
+ method: HttpMethod,
14
+ path: string,
15
+ accessToken: string,
16
+ body?: any
17
+ ): Promise<any> {
18
+ const url = path.startsWith('http') ? path : `${this.gcsBaseUrl}${path}`;
19
+
20
+ const response = await httpClient.sendRequest({
21
+ method,
22
+ url,
23
+ authentication: {
24
+ type: AuthenticationType.BEARER_TOKEN,
25
+ token: accessToken,
26
+ },
27
+ body,
28
+ });
29
+
30
+ return response.body;
31
+ },
32
+
33
+ async makePubSubRequest(
34
+ method: HttpMethod,
35
+ path: string,
36
+ accessToken: string,
37
+ body?: any
38
+ ): Promise<any> {
39
+ const url = path.startsWith('http') ? path : `${this.pubsubBaseUrl}${path}`;
40
+
41
+ const response = await httpClient.sendRequest({
42
+ method,
43
+ url,
44
+ authentication: {
45
+ type: AuthenticationType.BEARER_TOKEN,
46
+ token: accessToken,
47
+ },
48
+ body,
49
+ });
50
+
51
+ return response.body;
52
+ },
53
+
54
+ // For backward compatibility
55
+ async makeRequest(
56
+ method: HttpMethod,
57
+ path: string,
58
+ accessToken: string,
59
+ body?: any
60
+ ): Promise<any> {
61
+ return this.makeGCSRequest(method, path, accessToken, body);
62
+ },
63
+ };
@@ -0,0 +1,249 @@
1
+ import { Property, OAuth2PropertyValue } from '@guayaba/workflows-framework';
2
+ import { gcsCommon } from './client';
3
+ import { HttpMethod } from '@guayaba/workflows-common';
4
+ import { googleCloudStorageAuth } from './auth';
5
+
6
+ export const bucketDropdown = Property.Dropdown<string,true,typeof googleCloudStorageAuth>({
7
+ displayName: 'Bucket',
8
+ required: true,
9
+ refreshers: ['projectId'],
10
+ auth: googleCloudStorageAuth,
11
+ options: async ({ auth, projectId }) => {
12
+ if (!auth || !projectId) {
13
+ return {
14
+ disabled: true,
15
+ options: [],
16
+ placeholder: projectId
17
+ ? 'Please connect your account first'
18
+ : 'Please enter project ID first',
19
+ };
20
+ }
21
+
22
+ try {
23
+ const authValue = auth as OAuth2PropertyValue;
24
+ const response = await gcsCommon.makeRequest(
25
+ HttpMethod.GET,
26
+ `/b?project=${projectId}`,
27
+ authValue.access_token
28
+ );
29
+
30
+ return {
31
+ disabled: false,
32
+ options:
33
+ response.items?.map((bucket: any) => ({
34
+ label: bucket.name,
35
+ value: bucket.name,
36
+ })) || [],
37
+ };
38
+ } catch (error) {
39
+ return {
40
+ disabled: true,
41
+ options: [],
42
+ placeholder: 'Failed to load buckets',
43
+ };
44
+ }
45
+ },
46
+ });
47
+
48
+ export const objectDropdown = (bucketProperty: string) =>
49
+ Property.Dropdown<string,true,typeof googleCloudStorageAuth>({
50
+ displayName: 'Object',
51
+ required: true,
52
+ auth: googleCloudStorageAuth,
53
+ refreshers: [bucketProperty],
54
+ options: async ({ auth, [bucketProperty]: bucket }) => {
55
+ if (!auth || !bucket) {
56
+ return {
57
+ disabled: true,
58
+ options: [],
59
+ placeholder: bucket
60
+ ? 'Please connect your account first'
61
+ : 'Please select a bucket first',
62
+ };
63
+ }
64
+
65
+ try {
66
+ const authValue = auth as OAuth2PropertyValue;
67
+ const response = await gcsCommon.makeRequest(
68
+ HttpMethod.GET,
69
+ `/b/${bucket}/o?maxResults=100`,
70
+ authValue.access_token
71
+ );
72
+
73
+ return {
74
+ disabled: false,
75
+ options:
76
+ response.items?.map((object: any) => ({
77
+ label: object.name,
78
+ value: object.name,
79
+ })) || [],
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ disabled: true,
84
+ options: [],
85
+ placeholder: 'Failed to load objects',
86
+ };
87
+ }
88
+ },
89
+ });
90
+
91
+ export const projectIdProperty = Property.Dropdown<string,true,typeof googleCloudStorageAuth>({
92
+ displayName: 'Project',
93
+ required: true,
94
+ auth: googleCloudStorageAuth,
95
+ refreshers: [],
96
+ options: async ({ auth }) => {
97
+ if (!auth) {
98
+ return {
99
+ disabled: true,
100
+ options: [],
101
+ placeholder: 'Please connect your account first',
102
+ };
103
+ }
104
+
105
+ try {
106
+ const authValue = auth as OAuth2PropertyValue;
107
+ // Use Google Cloud Resource Manager API to list projects
108
+ const response = await gcsCommon.makeRequest(
109
+ HttpMethod.GET,
110
+ 'https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE',
111
+ authValue.access_token
112
+ );
113
+
114
+ return {
115
+ disabled: false,
116
+ options: response.projects?.map((project: any) => ({
117
+ label: `${project.displayName || project.name} (${project.projectId})`,
118
+ value: project.projectId,
119
+ })) || [],
120
+ };
121
+ } catch (error) {
122
+ return {
123
+ disabled: true,
124
+ options: [],
125
+ placeholder: 'Failed to load projects. Check your permissions.',
126
+ };
127
+ }
128
+ },
129
+ });
130
+
131
+ export const bucketNameProperty = Property.ShortText({
132
+ displayName: 'Bucket Name',
133
+ description: 'Unique name for your bucket (must be globally unique, 3-63 characters)',
134
+ required: true,
135
+ });
136
+
137
+ export const objectNameProperty = Property.ShortText({
138
+ displayName: 'Object Name',
139
+ required: true,
140
+ });
141
+
142
+ export const locationProperty = Property.StaticDropdown({
143
+ displayName: 'Location',
144
+ required: false,
145
+ options: {
146
+ options: [
147
+ // Multi-region
148
+ { label: 'US (Multi-region)', value: 'US' },
149
+ { label: 'EU (Multi-region)', value: 'EU' },
150
+ { label: 'ASIA (Multi-region)', value: 'ASIA' },
151
+ // US regions
152
+ { label: 'us-central1 (Iowa)', value: 'us-central1' },
153
+ { label: 'us-east1 (South Carolina)', value: 'us-east1' },
154
+ { label: 'us-east4 (Northern Virginia)', value: 'us-east4' },
155
+ { label: 'us-west1 (Oregon)', value: 'us-west1' },
156
+ { label: 'us-west2 (Los Angeles)', value: 'us-west2' },
157
+ { label: 'us-west3 (Salt Lake City)', value: 'us-west3' },
158
+ { label: 'us-west4 (Las Vegas)', value: 'us-west4' },
159
+ { label: 'us-south1 (Dallas)', value: 'us-south1' },
160
+ // Europe regions
161
+ { label: 'europe-central2 (Warsaw)', value: 'europe-central2' },
162
+ { label: 'europe-north1 (Finland)', value: 'europe-north1' },
163
+ { label: 'europe-southwest1 (Madrid)', value: 'europe-southwest1' },
164
+ { label: 'europe-west1 (Belgium)', value: 'europe-west1' },
165
+ { label: 'europe-west2 (London)', value: 'europe-west2' },
166
+ { label: 'europe-west3 (Frankfurt)', value: 'europe-west3' },
167
+ { label: 'europe-west4 (Netherlands)', value: 'europe-west4' },
168
+ { label: 'europe-west6 (Zurich)', value: 'europe-west6' },
169
+ { label: 'europe-west8 (Milan)', value: 'europe-west8' },
170
+ { label: 'europe-west9 (Paris)', value: 'europe-west9' },
171
+ // Asia regions
172
+ { label: 'asia-east1 (Taiwan)', value: 'asia-east1' },
173
+ { label: 'asia-east2 (Hong Kong)', value: 'asia-east2' },
174
+ { label: 'asia-northeast1 (Tokyo)', value: 'asia-northeast1' },
175
+ { label: 'asia-northeast2 (Osaka)', value: 'asia-northeast2' },
176
+ { label: 'asia-northeast3 (Seoul)', value: 'asia-northeast3' },
177
+ { label: 'asia-south1 (Mumbai)', value: 'asia-south1' },
178
+ { label: 'asia-south2 (Delhi)', value: 'asia-south2' },
179
+ { label: 'asia-southeast1 (Singapore)', value: 'asia-southeast1' },
180
+ { label: 'asia-southeast2 (Jakarta)', value: 'asia-southeast2' },
181
+ // Other regions
182
+ { label: 'australia-southeast1 (Sydney)', value: 'australia-southeast1' },
183
+ { label: 'australia-southeast2 (Melbourne)', value: 'australia-southeast2' },
184
+ { label: 'northamerica-northeast1 (Montreal)', value: 'northamerica-northeast1' },
185
+ { label: 'northamerica-northeast2 (Toronto)', value: 'northamerica-northeast2' },
186
+ { label: 'southamerica-east1 (São Paulo)', value: 'southamerica-east1' },
187
+ { label: 'southamerica-west1 (Santiago)', value: 'southamerica-west1' },
188
+ ],
189
+ },
190
+ });
191
+
192
+ export const storageClassProperty = Property.StaticDropdown({
193
+ displayName: 'Storage Class',
194
+ required: false,
195
+ options: {
196
+ options: [
197
+ { label: 'Standard', value: 'STANDARD' },
198
+ { label: 'Nearline', value: 'NEARLINE' },
199
+ { label: 'Coldline', value: 'COLDLINE' },
200
+ { label: 'Archive', value: 'ARCHIVE' },
201
+ { label: 'Multi-regional', value: 'MULTI_REGIONAL' },
202
+ { label: 'Regional', value: 'REGIONAL' },
203
+ { label: 'Durable Reduced Availability', value: 'DURABLE_REDUCED_AVAILABILITY' },
204
+ ],
205
+ },
206
+ });
207
+
208
+ export const aclEntityProperty = Property.ShortText({
209
+ displayName: 'Entity',
210
+ 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',
211
+ required: true,
212
+ });
213
+
214
+ export const aclRoleProperty = Property.StaticDropdown({
215
+ displayName: 'Role',
216
+ required: true,
217
+ options: {
218
+ options: [
219
+ { label: 'Reader', value: 'READER' },
220
+ { label: 'Writer', value: 'WRITER' },
221
+ { label: 'Owner', value: 'OWNER' },
222
+ ],
223
+ },
224
+ });
225
+
226
+ // For bucket ACLs - supports OWNER, WRITER, READER
227
+ export const bucketAclRoleProperty = Property.StaticDropdown({
228
+ displayName: 'Role',
229
+ required: true,
230
+ options: {
231
+ options: [
232
+ { label: 'Reader', value: 'READER' },
233
+ { label: 'Writer', value: 'WRITER' },
234
+ { label: 'Owner', value: 'OWNER' },
235
+ ],
236
+ },
237
+ });
238
+
239
+ // For object ACLs - supports OWNER, READER only
240
+ export const objectAclRoleProperty = Property.StaticDropdown({
241
+ displayName: 'Role',
242
+ required: true,
243
+ options: {
244
+ options: [
245
+ { label: 'Reader', value: 'READER' },
246
+ { label: 'Owner', value: 'OWNER' },
247
+ ],
248
+ },
249
+ });
@@ -0,0 +1,171 @@
1
+ import { createTrigger, TriggerStrategy, Property } from '@guayaba/workflows-framework';
2
+ import { googleCloudStorageAuth } from '../common/auth';
3
+ import { gcsCommon } from '../common/client';
4
+ import { bucketDropdown, projectIdProperty } from '../common/props';
5
+ import { HttpMethod } from '@guayaba/workflows-common';
6
+
7
+ interface TriggerData {
8
+ topicName: string;
9
+ subscriptionName: string;
10
+ notificationId: string;
11
+ }
12
+
13
+ export const newObjectCreated = createTrigger({
14
+ auth: googleCloudStorageAuth,
15
+ name: 'new_object_created',
16
+ displayName: 'New Object Created',
17
+ description: 'Triggers when a new object is created in a bucket',
18
+ props: {
19
+ projectId: projectIdProperty,
20
+ bucket: bucketDropdown,
21
+ prefix: Property.ShortText({
22
+ displayName: 'Prefix Filter',
23
+ description: 'Only trigger for objects with this prefix',
24
+ required: false,
25
+ }),
26
+ },
27
+ type: TriggerStrategy.WEBHOOK,
28
+ sampleData: {
29
+ kind: 'storage#object',
30
+ id: 'example-bucket/example-object/1234567890',
31
+ name: 'example-object.txt',
32
+ bucket: 'example-bucket',
33
+ generation: '1234567890',
34
+ contentType: 'text/plain',
35
+ timeCreated: '2023-01-01T00:00:00.000Z',
36
+ updated: '2023-01-01T00:00:00.000Z',
37
+ size: '1024',
38
+ },
39
+ onEnable: async (context) => {
40
+ const { projectId, bucket, prefix } = context.propsValue;
41
+ const auth = context.auth;
42
+
43
+ // Generate unique names for this trigger instance
44
+ const triggerId = `ap-gcs-${bucket}-${Date.now()}`;
45
+ const topicName = `projects/${projectId}/topics/${triggerId}`;
46
+ const subscriptionName = `projects/${projectId}/subscriptions/${triggerId}`;
47
+
48
+ try {
49
+ // 1. Create Pub/Sub topic
50
+ await gcsCommon.makePubSubRequest(
51
+ HttpMethod.PUT,
52
+ `/projects/${projectId}/topics/${triggerId}`,
53
+ auth.access_token
54
+ );
55
+
56
+ // 2. Create GCS notification configuration
57
+ const notificationConfig: any = {
58
+ topic: topicName,
59
+ payload_format: 'JSON_API_V1',
60
+ event_types: ['OBJECT_FINALIZE'],
61
+ };
62
+
63
+ if (prefix) {
64
+ notificationConfig.object_name_prefix = prefix;
65
+ }
66
+
67
+ const notificationResponse = await gcsCommon.makeGCSRequest(
68
+ HttpMethod.POST,
69
+ `/b/${bucket}/notificationConfigs`,
70
+ auth.access_token,
71
+ notificationConfig
72
+ );
73
+
74
+ // 3. Create Pub/Sub subscription with webhook push
75
+ const subscriptionConfig = {
76
+ topic: topicName,
77
+ pushConfig: {
78
+ pushEndpoint: context.webhookUrl,
79
+ },
80
+ ackDeadlineSeconds: 60,
81
+ };
82
+
83
+ await gcsCommon.makePubSubRequest(
84
+ HttpMethod.PUT,
85
+ `/projects/${projectId}/subscriptions/${triggerId}`,
86
+ auth.access_token,
87
+ subscriptionConfig
88
+ );
89
+
90
+ // Store trigger data for cleanup
91
+ await context.store.put<TriggerData>('_trigger', {
92
+ topicName: triggerId,
93
+ subscriptionName: triggerId,
94
+ notificationId: notificationResponse.id,
95
+ });
96
+
97
+ } catch (error) {
98
+ // Cleanup on failure
99
+ try {
100
+ await gcsCommon.makePubSubRequest(
101
+ HttpMethod.DELETE,
102
+ `/projects/${projectId}/subscriptions/${triggerId}`,
103
+ auth.access_token
104
+ );
105
+ } catch (e) { /* ignore */ }
106
+
107
+ try {
108
+ await gcsCommon.makePubSubRequest(
109
+ HttpMethod.DELETE,
110
+ `/projects/${projectId}/topics/${triggerId}`,
111
+ auth.access_token
112
+ );
113
+ } catch (e) { /* ignore */ }
114
+
115
+ throw new Error(`Failed to setup Pub/Sub notifications: ${(error as any)?.message || 'Unknown error'}`);
116
+ }
117
+ },
118
+ onDisable: async (context) => {
119
+ const triggerData = await context.store.get<TriggerData>('_trigger');
120
+ if (!triggerData) return;
121
+
122
+ const { projectId } = context.propsValue;
123
+ const { bucket } = context.propsValue;
124
+ const auth = context.auth;
125
+
126
+ // Clean up in reverse order
127
+ try {
128
+ // Delete subscription
129
+ await gcsCommon.makePubSubRequest(
130
+ HttpMethod.DELETE,
131
+ `/projects/${projectId}/subscriptions/${triggerData.subscriptionName}`,
132
+ auth.access_token
133
+ );
134
+ } catch (e) { /* ignore */ }
135
+
136
+ try {
137
+ // Delete notification config
138
+ await gcsCommon.makeGCSRequest(
139
+ HttpMethod.DELETE,
140
+ `/b/${bucket}/notificationConfigs/${triggerData.notificationId}`,
141
+ auth.access_token
142
+ );
143
+ } catch (e) { /* ignore */ }
144
+
145
+ try {
146
+ // Delete topic
147
+ await gcsCommon.makePubSubRequest(
148
+ HttpMethod.DELETE,
149
+ `/projects/${projectId}/topics/${triggerData.topicName}`,
150
+ auth.access_token
151
+ );
152
+ } catch (e) { /* ignore */ }
153
+ },
154
+ run: async (context) => {
155
+ const payload = context.payload.body as any;
156
+
157
+ if (!payload?.message?.data) {
158
+ return [];
159
+ }
160
+
161
+ // Decode Pub/Sub message
162
+ const messageData = JSON.parse(
163
+ Buffer.from(payload.message.data, 'base64').toString()
164
+ );
165
+
166
+ // Extract GCS object from notification payload
167
+ const gcsObject = messageData;
168
+
169
+ return [gcsObject];
170
+ },
171
+ });