@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.
- package/.eslintrc.json +33 -0
- package/README.md +5 -0
- package/assets/logo.png +0 -0
- package/package.json +21 -0
- package/src/i18n/de.json +123 -0
- package/src/i18n/es.json +123 -0
- package/src/i18n/fr.json +123 -0
- package/src/i18n/ja.json +123 -0
- package/src/i18n/nl.json +123 -0
- package/src/i18n/pt.json +123 -0
- package/src/i18n/translation.json +123 -0
- package/src/i18n/zh.json +123 -0
- package/src/index.ts +46 -0
- package/src/lib/actions/clone-object.ts +73 -0
- package/src/lib/actions/create-bucket-acl.ts +56 -0
- package/src/lib/actions/create-bucket-default-object-acl.ts +56 -0
- package/src/lib/actions/create-bucket.ts +73 -0
- package/src/lib/actions/create-object-acl.ts +79 -0
- package/src/lib/actions/delete-bucket-acl.ts +50 -0
- package/src/lib/actions/delete-bucket-default-object-acl.ts +50 -0
- package/src/lib/actions/delete-empty-bucket.ts +40 -0
- package/src/lib/actions/delete-object-acl.ts +62 -0
- package/src/lib/actions/delete-object.ts +57 -0
- package/src/lib/actions/search-buckets.ts +90 -0
- package/src/lib/actions/search-objects.ts +99 -0
- package/src/lib/common/auth.ts +14 -0
- package/src/lib/common/client.ts +63 -0
- package/src/lib/common/props.ts +249 -0
- package/src/lib/triggers/new-object-created.ts +171 -0
- package/src/lib/triggers/object-updated.ts +171 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +13 -0
|
@@ -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
|
+
});
|