From 1e963d8e1ed6956e8af02e70da304211aa725ea1 Mon Sep 17 00:00:00 2001 From: freya Date: Tue, 13 Sep 2022 12:03:17 +0100 Subject: [PATCH] feat(GoogleCloudStorage Node): add GCS Node with Bucket and Object operations --- ...GoogleCloudStorageOAuth2Api.credentials.ts | 24 + .../Google/CloudStorage/BucketDescription.ts | 616 ++++++++++++ .../CloudStorage/GoogleCloudStorage.node.json | 23 + .../CloudStorage/GoogleCloudStorage.node.ts | 65 ++ .../Google/CloudStorage/ObjectDescription.ts | 911 ++++++++++++++++++ .../CloudStorage/googleCloudStorage.svg | 1 + packages/nodes-base/package.json | 2 + 7 files changed, 1642 insertions(+) create mode 100644 packages/nodes-base/credentials/GoogleCloudStorageOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts create mode 100644 packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.json create mode 100644 packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.ts create mode 100644 packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts create mode 100644 packages/nodes-base/nodes/Google/CloudStorage/googleCloudStorage.svg diff --git a/packages/nodes-base/credentials/GoogleCloudStorageOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleCloudStorageOAuth2Api.credentials.ts new file mode 100644 index 0000000000..f5b71684c6 --- /dev/null +++ b/packages/nodes-base/credentials/GoogleCloudStorageOAuth2Api.credentials.ts @@ -0,0 +1,24 @@ +import { ICredentialType, INodeProperties } from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/cloud-platform.read-only', + 'https://www.googleapis.com/auth/devstorage.full_control', + 'https://www.googleapis.com/auth/devstorage.read_only', + 'https://www.googleapis.com/auth/devstorage.read_write', +]; + +export class GoogleCloudStorageOAuth2Api implements ICredentialType { + name = 'googleCloudStorageOAuth2Api'; + extends = ['googleOAuth2Api']; + displayName = 'Google Cloud Storage OAuth2 API'; + documentationUrl = 'google'; + properties: INodeProperties[] = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden', + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts b/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts new file mode 100644 index 0000000000..3d0946768f --- /dev/null +++ b/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts @@ -0,0 +1,616 @@ +import { IDataObject, IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow'; +import { INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +// Projection field controls the page limit maximum +// When not returning all, return the max number for the current projection parameter +const PAGE_LIMITS = { + noAcl: 1000, + full: 200, +}; + +// Define a JSON parse function here to use it in two places +async function parseJSONBody( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + if (!requestOptions.body) requestOptions.body = {}; + const body = this.getNodeParameter('createBody') as IDataObject; + + // Parse all the JSON fields + if (body.acl) { + try { + body.acl = JSON.parse(body.acl as string); + } catch (error) {} + } + if (body.billing) { + try { + body.billing = JSON.parse(body.billing as string); + } catch (error) {} + } + if (body.cors) { + try { + body.cors = JSON.parse(body.cors as string); + } catch (error) {} + } + if (body.customPlacementConfig) { + try { + body.customPlacementConfig = JSON.parse(body.customPlacementConfig as string); + } catch (error) {} + } + if (body.dataLocations) { + try { + body.dataLocations = JSON.parse(body.dataLocations as string); + } catch (error) {} + } + if (body.defaultObjectAcl) { + try { + body.defaultObjectAcl = JSON.parse(body.defaultObjectAcl as string); + } catch (error) {} + } + if (body.encryption) { + try { + body.encryption = JSON.parse(body.encryption as string); + } catch (error) {} + } + if (body.iamConfiguration) { + try { + body.iamConfiguration = JSON.parse(body.iamConfiguration as string); + } catch (error) {} + } + if (body.labels) { + try { + body.labels = JSON.parse(body.labels as string); + } catch (error) {} + } + if (body.lifecycle) { + try { + body.lifecycle = JSON.parse(body.lifecycle as string); + } catch (error) {} + } + if (body.logging) { + try { + body.logging = JSON.parse(body.logging as string); + } catch (error) {} + } + if (body.retentionPolicy) { + try { + body.retentionPolicy = JSON.parse(body.retentionPolicy as string); + } catch (error) {} + } + if (body.versioning) { + try { + body.versioning = JSON.parse(body.versioning as string); + } catch (error) {} + } + if (body.website) { + try { + body.website = JSON.parse(body.website as string); + } catch (error) {} + } + + requestOptions.body = Object.assign(requestOptions.body, body); + return requestOptions; +} + +export const bucketOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['bucket'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new Bucket', + routing: { + request: { + method: 'POST', + url: '/b/', + qs: {}, + body: { + name: '={{$parameter["bucketName"]}}', + }, + returnFullResponse: true, + }, + send: { + preSend: [parseJSONBody], + }, + }, + action: 'Create a new Bucket', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an empty Bucket', + routing: { + request: { + method: 'DELETE', + url: '={{"/b/" + $parameter["bucketName"]}}', + returnFullResponse: true, + }, + }, + action: 'Delete an empty Bucket', + }, + { + name: 'Get', + value: 'get', + description: 'Get metadata for a specific Bucket', + routing: { + request: { + method: 'GET', + url: '={{"/b/" + $parameter["bucketName"]}}', + returnFullResponse: true, + qs: {}, + }, + }, + action: 'Get a Bucket', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get list of Buckets', + routing: { + request: { + method: 'GET', + url: '/b/', + qs: {}, + }, + send: { + paginate: true, + preSend: [ + async function (this, requestOptions) { + if (!requestOptions.qs) requestOptions.qs = {}; + const returnAll = this.getNodeParameter('returnAll') as boolean; + + if (!returnAll) { + const key = this.getNodeParameter('projection') as string; + requestOptions.qs.maxResults = + key === 'noAcl' ? PAGE_LIMITS.noAcl : PAGE_LIMITS.full; + } + return requestOptions; + }, + ], + }, + operations: { + async pagination(this, requestOptions) { + if (!requestOptions.options.qs) requestOptions.options.qs = {}; + let executions: INodeExecutionData[] = []; + let responseData: INodeExecutionData[]; + let nextPageToken: string | undefined = undefined; + const returnAll = this.getNodeParameter('returnAll') as boolean; + + do { + requestOptions.options.qs.pageToken = nextPageToken; + responseData = await this.makeRoutingRequest(requestOptions); + + // Check for another page + const lastItem = responseData[responseData.length - 1].json; + nextPageToken = lastItem.nextPageToken as string | undefined; + + // Extract just the list of buckets from the page data + responseData.forEach((page) => { + const buckets = page.json.items as IDataObject[]; + if (buckets) { + executions = executions.concat(buckets.map((bucket) => ({ json: bucket }))); + } + }); + // If we don't return all, just return the first page + } while (returnAll && nextPageToken); + + // Return all execution responses as an array + return executions; + }, + }, + }, + action: 'Get a list of Buckets for a given project', + }, + { + name: 'Update', + value: 'update', + description: 'Update the metadata of a bucket', + routing: { + request: { + method: 'PATCH', + url: '={{"/b/" + $parameter["bucketName"]}}', + qs: { + project: '={{$parameter["projectId"]}}', + }, + body: {}, + returnFullResponse: true, + }, + send: { + preSend: [parseJSONBody], + }, + }, + action: 'Create a new Bucket', + }, + ], + default: 'getAll', + }, +]; + +export const bucketFields: INodeProperties[] = [ + { + displayName: 'Project ID', + name: 'projectId', + type: 'string', + required: true, + placeholder: 'Project ID', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['create', 'getAll'], + }, + }, + default: '', + routing: { + request: { + qs: { + project: '={{$value}}', + }, + }, + }, + }, + { + displayName: 'Bucket Name', + name: 'bucketName', + type: 'string', + placeholder: 'Bucket Name', + required: true, + displayOptions: { + show: { + resource: ['bucket'], + operation: ['create', 'get', 'update', 'delete'], + }, + }, + default: '', + }, + { + displayName: 'Prefix', + name: 'prefix', + type: 'string', + placeholder: 'Filter for Bucket Names', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['getAll'], + }, + }, + default: '', + routing: { + request: { + qs: { + prefix: '={{$value}}', + }, + }, + }, + }, + { + displayName: 'Projection', + name: 'projection', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'All Properties', + value: 'full', + }, + { + name: 'No ACL', + value: 'noAcl', + }, + ], + default: 'noAcl', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['create', 'get', 'getAll', 'update'], + }, + }, + routing: { + request: { + qs: { + projection: '={{$value}}', + }, + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Filters', + name: 'getFilters', + type: 'collection', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['delete', 'get', 'update'], + }, + }, + default: {}, + placeholder: 'Add Filter', + options: [ + { + displayName: 'Metageneration Match', + name: 'ifMetagenerationMatch', + type: 'number', + description: + 'Only return data if the metageneration value of the Bucket matches the sent value', + default: 0, + routing: { + request: { + qs: { + ifMetagenerationMatch: '={{$value}}', + }, + }, + }, + }, + { + displayName: 'Metageneration Exclude', + name: 'ifMetagenerationNotMatch', + type: 'number', + description: + 'Only return data if the metageneration value of the Bucket does not match the sent value', + default: 0, + routing: { + request: { + qs: { + ifMetagenerationNotMatch: '={{$value}}', + }, + }, + }, + }, + ], + }, + { + displayName: 'Predefined Access Control', + name: 'createAcl', + type: 'collection', + noDataExpression: true, + default: {}, + placeholder: 'Add Access Control Parameters', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['create', 'update'], + }, + }, + options: [ + { + displayName: 'Predefined ACL', + name: 'predefinedAcl', + type: 'options', + default: 'authenticatedRead', + placeholder: 'Apply a predefined set of access controls to this bucket', + options: [ + { + name: 'Authenticated Read', + value: 'authenticatedRead', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Project Private', + value: 'projectPrivate', + }, + { + name: 'Public Read', + value: 'publicRead', + }, + { + name: 'Public Read/Write', + value: 'publicReadWrite', + }, + ], + routing: { + request: { + qs: { + predefinedAcl: '={{$value}}', + }, + }, + }, + }, + { + displayName: 'Predefined Default Object ACL', + name: 'predefinedDefaultObjectAcl', + type: 'options', + default: 'authenticatedRead', + placeholder: 'Apply a predefined set of default object access controls to this bucket', + options: [ + { + name: 'Authenticated Read', + value: 'authenticatedRead', + }, + { + name: 'Bucket Owner Full Control', + value: 'bucketOwnerFullControl', + }, + { + name: 'Bucket Owner Read', + value: 'bucketOwnerRead', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Project Private', + value: 'projectPrivate', + }, + { + name: 'Public Read', + value: 'publicRead', + }, + ], + routing: { + request: { + qs: { + predefinedObjectAcl: '={{$value}}', + }, + }, + }, + }, + ], + }, + { + displayName: 'Additional Parameters', + name: 'createBody', + type: 'collection', + noDataExpression: true, + default: {}, + placeholder: 'Add Metadata Parameter', + displayOptions: { + show: { + resource: ['bucket'], + operation: ['create', 'update'], + }, + }, + options: [ + { + displayName: 'Access Control', + name: 'acl', + type: 'json', + default: '[]', + placeholder: 'Access controls on the Bucket', + }, + { + displayName: 'Billing', + name: 'billing', + type: 'json', + default: '{}', + placeholder: "The bucket's billing configuration", + }, + { + displayName: 'CORS', + name: 'cors', + type: 'json', + default: '[]', + placeholder: "The bucket's Cross Origin Resource Sharing configuration", + }, + { + displayName: 'Custom Placement Config', + name: 'customPlacementConfig', + type: 'json', + default: '{}', + placeholder: 'The configuration for the region(s) for the Bucket', + }, + { + displayName: 'Data Locations', + name: 'dataLocations', + type: 'json', + default: '[]', + placeholder: 'The list of individual regions that comprise a dual-region Bucket', + }, + { + displayName: 'Default Event Based Hold', + name: 'defaultEventBasedHold', + type: 'boolean', + default: true, + placeholder: 'Whether or not to automatically apply an event based hold to new objects', + }, + { + displayName: 'Default Object ACL', + name: 'defaultObjectAcl', + type: 'json', + default: '[]', + placeholder: 'Default Access Controls for new objects when no ACL is provided', + }, + { + displayName: 'Encryption', + name: 'encryption', + type: 'json', + default: '{}', + placeholder: 'Encryption configuration for a bucket', + }, + { + displayName: 'IAM Configuration', + name: 'iamConfiguration', + type: 'json', + default: '{}', + placeholder: "The bucket's IAM configuration", + }, + { + displayName: 'Labels', + name: 'labels', + type: 'json', + default: '{}', + placeholder: 'User provided bucket labels, in key/value pairs', + }, + { + displayName: 'Lifecycle', + name: 'lifecycle', + type: 'json', + default: '{}', + placeholder: "The bucket's lifecycle configuration", + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: 'US', + placeholder: 'The location of the bucket', + }, + { + displayName: 'Logging', + name: 'logging', + type: 'json', + default: '{}', + placeholder: "The bucket's logging configuration", + }, + { + displayName: 'Retention Policy', + name: 'retentionPolicy', + type: 'json', + default: '{}', + placeholder: "The bucket's retention policy", + }, + { + displayName: 'Recovery Point Objective', + name: 'rpo', + type: 'string', + default: 'DEFAULT', + placeholder: 'The recovery point objective for the bucket', + }, + { + displayName: 'Storage Class', + name: 'storageClass', + type: 'string', + default: 'STANDARD', + placeholder: "The bucket's default storage class for objects that don't define one", + }, + { + displayName: 'Versioning', + name: 'versioning', + type: 'json', + default: '{}', + placeholder: "The bucket's versioning configuration", + }, + { + displayName: 'Website', + name: 'website', + type: 'json', + default: '{}', + placeholder: "The bucket's website configuration for when it is used to host a website", + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.json b/packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.json new file mode 100644 index 0000000000..dec877db9c --- /dev/null +++ b/packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.json @@ -0,0 +1,23 @@ +{ + "node": "n8n-nodes-base.googleCloudStorage", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development", + "Data & Storage" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/google" + } + ], + "generic": [ + { + "label": "15 Google apps you can combine and automate to increase productivity", + "icon": "💡", + "url": "https://n8n.io/blog/automate-google-apps-for-productivity/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.ts b/packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.ts new file mode 100644 index 0000000000..3b7a5de9d7 --- /dev/null +++ b/packages/nodes-base/nodes/Google/CloudStorage/GoogleCloudStorage.node.ts @@ -0,0 +1,65 @@ +import { INodeType, INodeTypeDescription } from 'n8n-workflow'; + +import { bucketFields, bucketOperations } from './BucketDescription'; +import { objectFields, objectOperations } from './ObjectDescription'; + +export class GoogleCloudStorage implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Cloud Storage', + name: 'googleCloudStorage', + icon: 'file:googleCloudStorage.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Use the Google Cloud Storage API', + defaults: { + name: 'Google Cloud Storage', + color: '#ff0000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleCloudStorageOAuth2Api', + required: true, + testedBy: { + request: { + method: 'GET', + url: '/b/', + }, + }, + }, + ], + requestDefaults: { + returnFullResponse: true, + baseURL: 'https://storage.googleapis.com/storage/v1', + }, + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Bucket', + value: 'bucket', + }, + { + name: 'Object', + value: 'object', + }, + ], + default: 'bucket', + }, + + // BUCKET + ...bucketOperations, + ...bucketFields, + + // OBJECT + ...objectOperations, + ...objectFields, + ], + }; +} diff --git a/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts b/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts new file mode 100644 index 0000000000..e2295601ce --- /dev/null +++ b/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts @@ -0,0 +1,911 @@ +import FormData from 'form-data'; +import { IDataObject, NodeOperationError } from 'n8n-workflow'; +import { INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +// Define these because we'll be using them in two separate places +const metagenerationFilters: INodeProperties[] = [ + { + displayName: 'Generation', + name: 'generation', + type: 'number', + placeholder: 'Select a specific revision of the chosen object', + default: -1, + }, + { + displayName: 'Generation Match', + name: 'ifGenerationMatch', + type: 'number', + placeholder: 'Make operation conditional of the object generation matching this value', + default: -1, + }, + { + displayName: 'Generation Exclude', + name: 'ifGenerationNotMatch', + type: 'number', + placeholder: 'Make operation conditional of the object generation not matching this value', + default: -1, + }, + { + displayName: 'Metageneration Match', + name: 'ifMetagenerationMatch', + type: 'number', + placeholder: + "Make operation conditional of the object's current metageneration matching this value", + default: -1, + }, + { + displayName: 'Metageneration Exclude', + name: 'ifMetagenerationNotMatch', + type: 'number', + placeholder: + "Make operation conditional of the object's current metageneration not matching this value", + default: -1, + }, +]; + +const predefinedAclOptions: INodeProperties = { + displayName: 'Predefined ACL', + name: 'predefinedAcl', + type: 'options', + placeholder: 'Apply a predefined set of Access Controls to the object', + default: 'authenticatedRead', + options: [ + { + name: 'Authenticated Read', + value: 'authenticatedRead', + }, + { + name: 'Bucket Owner Full Control', + value: 'bucketOwnerFullControl', + }, + { + name: 'Bucket Owner Read', + value: 'bucketOwnerRead', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Project Private', + value: 'projectPrivate', + }, + { + name: 'Public Read', + value: 'publicRead', + }, + ], +}; + +export const objectOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['object'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an object', + routing: { + request: { + method: 'POST', + baseURL: 'https://storage.googleapis.com/upload/storage/v1', + url: '={{"/b/" + $parameter["bucketName"] + "/o/"}}', + qs: { + name: '={{$parameter["objectName"]}}', + uploadType: 'multipart', + }, + headers: {}, + }, + send: { + preSend: [ + // Handle setup of Query and Headers + async function (this, requestOptions) { + // Merge in the options into the queryset and headers objects + if (!requestOptions.qs) requestOptions.qs = {}; + if (!requestOptions.headers) requestOptions.headers = {}; + const options = this.getNodeParameter('createQuery') as IDataObject; + const headers = this.getNodeParameter('encryptionHeaders') as IDataObject; + requestOptions.qs = Object.assign(requestOptions.qs, options); + requestOptions.headers = Object.assign(requestOptions.headers, headers); + return requestOptions; + }, + + // Handle body creation + async function (this, requestOptions) { + // Populate metadata JSON + let metadata: IDataObject = { name: this.getNodeParameter('objectName') as string }; + const bodyData = this.getNodeParameter('createData') as IDataObject; + const useBinary = this.getNodeParameter('createFromBinary') as boolean; + + // Parse JSON body parameters + if (bodyData.acl) { + try { + bodyData.acl = JSON.parse(bodyData.acl as string); + } catch (error) {} + } + if (bodyData.metadata) { + try { + bodyData.metadata = JSON.parse(bodyData.metadata as string); + } catch (error) {} + } + metadata = Object.assign(metadata, bodyData); + + // Populate request body + const body = new FormData(); + const item = this.getInputData(); + body.append('metadata', JSON.stringify(metadata), { + contentType: 'application/json', + }); + + // Determine content and content type + let content: string | Buffer; + let contentType: string; + if (useBinary) { + const binaryPropertyName = this.getNodeParameter( + 'createBinaryPropertyName', + ) as string; + if (!item.binary) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: this.getItemIndex(), + }); + } + if (item.binary[binaryPropertyName] === undefined) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exist on item!`, + { itemIndex: this.getItemIndex() }, + ); + } + + const binaryData = item.binary[binaryPropertyName]; + + // Decode from base64 for upload + content = Buffer.from(binaryData.data, 'base64'); + contentType = binaryData.mimeType; + } else { + content = this.getNodeParameter('createContent') as string; + contentType = 'text/plain'; + } + body.append('file', content, { contentType }); + + // Set the headers + requestOptions.headers!!['Content-Length'] = body.getLengthSync(); + requestOptions.headers!![ + 'Content-Type' + ] = `multipart/related; boundary=${body.getBoundary()}`; + + // Return the request data + requestOptions.body = body.getBuffer(); + return requestOptions; + }, + ], + }, + }, + action: 'Create an object', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an object', + routing: { + request: { + method: 'DELETE', + url: '={{"/b/" + $parameter["bucketName"] + "/o/" + $parameter["objectName"]}}', + qs: {}, + }, + }, + action: 'Delete an object from a bucket', + }, + { + name: 'Get', + value: 'get', + description: 'Get object data or metadata', + routing: { + request: { + method: 'GET', + url: '={{"/b/" + $parameter["bucketName"] + "/o/" + $parameter["objectName"]}}', + returnFullResponse: true, + qs: { + alt: '={{$parameter["alt"]}}', + }, + }, + send: { + preSend: [ + async function (this, requestOptions) { + if (!requestOptions.qs) requestOptions.qs = {}; + if (!requestOptions.headers) requestOptions.headers = {}; + const options = this.getNodeParameter('getParameters') as IDataObject; + const headers = this.getNodeParameter('encryptionHeaders') as IDataObject; + const datatype = this.getNodeParameter('alt') as string; + + if (datatype === 'media') { + requestOptions.encoding = 'arraybuffer'; + } + + // Merge in the options into the queryset and headers objects + requestOptions.qs = Object.assign(requestOptions.qs, options); + requestOptions.headers = Object.assign(requestOptions.headers, headers); + + // Return the request data + return requestOptions; + }, + ], + }, + output: { + postReceive: [ + async function (this, items, responseData) { + // If the request was for object data as opposed to metadata, change the json to binary field in the response + const datatype = this.getNodeParameter('alt') as string; + + if (datatype === 'media') { + // Adapt the binaryProperty part of Routing Node since it's conditional + const destinationName = this.getNodeParameter('binaryPropertyName') as string; + const fileName = this.getNodeParameter('objectName') as string; + const binaryData = await this.helpers.prepareBinaryData( + responseData.body as Buffer, + fileName, + ); + + // Transform items + items = items.map((item) => { + item.json = {}; + item.binary = { [destinationName]: binaryData }; + return item; + }); + } + return items; + }, + ], + }, + }, + action: 'Get object data or metadata', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve a list of objects', + routing: { + request: { + method: 'GET', + url: '={{"/b/" + $parameter["bucketName"] + "/o/"}}', + returnFullResponse: true, + qs: { + }, + }, + send: { + preSend: [ + async function (this, requestOptions) { + if (!requestOptions.qs) requestOptions.qs = {}; + const options = this.getNodeParameter('listFilters') as IDataObject; + + // Merge in the options into the queryset + requestOptions.qs = Object.assign(requestOptions.qs, options); + + // Check if we send a limit + const returnAll = this.getNodeParameter('returnAll') as boolean; + if (!returnAll) requestOptions.qs.maxResults = this.getNodeParameter('maxResults'); + + // Return the request data + return requestOptions; + }, + ], + paginate: true, + }, + operations: { + async pagination(this, requestOptions) { + if (!requestOptions.options.qs) requestOptions.options.qs = {}; + let executions: INodeExecutionData[] = []; + let responseData: INodeExecutionData[]; + let nextPageToken: string | undefined = undefined; + const returnAll = this.getNodeParameter('returnAll') as boolean; + + do { + requestOptions.options.qs.pageToken = nextPageToken; + responseData = await this.makeRoutingRequest(requestOptions); + + // Check for another page + const lastItem = responseData[responseData.length - 1].json; + nextPageToken = lastItem.nextPageToken as string | undefined; + + // Extract just the list of buckets from the page data + responseData.forEach((page) => { + const objects = page.json.items as IDataObject[]; + if (objects) { + executions = executions.concat(objects.map((object) => ({ json: object }))); + } + }); + } while (returnAll && nextPageToken); + + // Return all execution responses as an array + return executions; + }, + }, + }, + action: 'Get a list of objects', + }, + { + name: 'Update', + value: 'update', + description: "Update an object's metadata", + routing: { + request: { + method: 'PATCH', + url: '={{"/b/" + $parameter["bucketName"] + "/o/" + $parameter["objectName"]}}', + qs: { + }, + body: {}, + }, + send: { + preSend: [ + async function (this, requestOptions) { + if (!requestOptions.qs) requestOptions.qs = {}; + if (!requestOptions.headers) requestOptions.headers = {}; + if (!requestOptions.body) requestOptions.body = {}; + const options = this.getNodeParameter('metagenAndAclQuery') as IDataObject; + const headers = this.getNodeParameter('encryptionHeaders') as IDataObject; + const body = this.getNodeParameter('updateData') as IDataObject; + + // Parse JSON body parameters + if (body.acl) { + try { + body.acl = JSON.parse(body.acl as string); + } catch (error) {} + } + if (body.metadata) { + try { + body.metadata = JSON.parse(body.metadata as string); + } catch (error) {} + } + + // Merge in the options into the queryset and headers objects + requestOptions.qs = Object.assign(requestOptions.qs, options); + requestOptions.headers = Object.assign(requestOptions.headers, headers); + requestOptions.body = Object.assign(requestOptions.body, body); + + // Return the request data + return requestOptions; + }, + ], + }, + }, + action: "Update an object's metadata", + }, + ], + default: 'getAll', + }, +]; + +export const objectFields: INodeProperties[] = [ + { + displayName: 'Bucket Name', + name: 'bucketName', + type: 'string', + placeholder: 'Bucket Name', + required: true, + displayOptions: { + show: { + resource: ['object'], + }, + }, + default: '', + }, + { + displayName: 'Object Name', + name: 'objectName', + type: 'string', + placeholder: 'Object Name', + required: true, + displayOptions: { + show: { + resource: ['object'], + operation: ['create', 'delete', 'get', 'update'], + }, + }, + default: '', + }, + { + displayName: 'Projection', + name: 'projection', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'All Properties', + value: 'full', + }, + { + name: 'No ACL', + value: 'noAcl', + }, + ], + default: 'noAcl', + displayOptions: { + show: { + resource: ['object'], + operation: ['get', 'getAll'], + }, + }, + routing: { + request: { + qs: { + projection: '={{$value}}', + }, + }, + }, + }, + // Create / Update gets their own definition because the default value is swapped + { + displayName: 'Projection', + name: 'updateProjection', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'All Properties', + value: 'full', + }, + { + name: 'No ACL', + value: 'noAcl', + }, + ], + default: 'full', + displayOptions: { + show: { + resource: ['object'], + operation: ['create', 'update'], + }, + }, + routing: { + request: { + qs: { + projection: '={{$value}}', + }, + }, + }, + }, + { + displayName: 'Return Data', + name: 'alt', + type: 'options', + placeholder: 'The type of data to return from the request', + default: 'json', + options: [ + { + name: 'Metadata', + value: 'json', + }, + { + name: 'Object Data', + value: 'media', + }, + ], + displayOptions: { + show: { + resource: ['object'], + operation: ['get'], + }, + }, + }, + { + displayName: 'Use Binary Property', + name: 'createFromBinary', + type: 'boolean', + displayOptions: { + show: { + resource: ['object'], + operation: ['create'], + }, + }, + default: true, + noDataExpression: true, + description: 'Whether the data for creating a file should come from a binary field', + }, + { + displayName: 'Binary Property', + name: 'createBinaryPropertyName', + type: 'string', + displayOptions: { + show: { + resource: ['object'], + operation: ['create'], + createFromBinary: [true], + }, + }, + default: 'data', + }, + { + displayName: 'File Content', + name: 'createContent', + type: 'string', + displayOptions: { + show: { + resource: ['object'], + operation: ['create'], + createFromBinary: [false], + }, + }, + default: '', + description: 'Content of the file to be uploaded', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + displayOptions: { + show: { + resource: ['object'], + operation: ['get'], + alt: ['media'], + }, + }, + default: 'data', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['object'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'maxResults', + type: 'number', + displayOptions: { + show: { + resource: ['object'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Create Fields', + name: 'createData', + type: 'collection', + placeholder: 'Add Create Body Field', + displayOptions: { + show: { + resource: ['object'], + operation: ['create'], + }, + }, + default: {}, + options: [ + { + displayName: 'Access Control List', + name: 'acl', + type: 'json', + default: '[]', + }, + { + displayName: 'Cache Control', + name: 'cacheControl', + type: 'string', + default: '', + }, + { + displayName: 'Content Disposition', + name: 'contentDisposition', + type: 'string', + default: '', + }, + { + displayName: 'Content Encoding', + name: 'contentEncoding', + type: 'string', + default: '', + }, + { + displayName: 'Content Language', + name: 'contentLanguage', + type: 'string', + default: '', + }, + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + }, + { + displayName: 'CRC32c Checksum', + name: 'crc32c', + type: 'string', + default: '', + }, + { + displayName: 'Custom Time', + name: 'customTime', + type: 'string', + default: '', + }, + { + displayName: 'Event Based Hold', + name: 'eventBasedHold', + type: 'boolean', + default: false, + }, + { + displayName: 'MD5 Hash', + name: 'md5Hash', + type: 'string', + default: '', + }, + { + displayName: 'Metadata', + name: 'metadata', + type: 'json', + default: '{}', + }, + { + displayName: 'Storage Class', + name: 'storageClass', + type: 'string', + default: '', + }, + { + displayName: 'Temporary Hold', + name: 'temporaryHold', + type: 'boolean', + default: false, + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateData', + type: 'collection', + placeholder: 'Add Update Body Field', + displayOptions: { + show: { + resource: ['object'], + operation: ['update'], + }, + }, + default: { + acl: '[]', + }, + options: [ + { + displayName: 'Access Control', + name: 'acl', + type: 'json', + default: '[]', + }, + { + displayName: 'Cache Control', + name: 'cacheControl', + type: 'string', + default: '', + }, + { + displayName: 'Content Disposition', + name: 'contentDisposition', + type: 'string', + default: '', + }, + { + displayName: 'Content Encoding', + name: 'contentEncoding', + type: 'string', + default: '', + }, + { + displayName: 'Content Language', + name: 'contentLanguage', + type: 'string', + default: '', + }, + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + }, + { + displayName: 'Custom Time', + name: 'customTime', + type: 'string', + default: '', + }, + { + displayName: 'Event Based Hold', + name: 'eventBasedHold', + type: 'boolean', + default: false, + }, + { + displayName: 'Metadata', + name: 'metadata', + type: 'json', + default: '{}', + }, + { + displayName: 'Temporary Hold', + name: 'temporaryHold', + type: 'boolean', + default: false, + }, + ], + }, + { + displayName: 'Additional Parameters', + name: 'createQuery', + type: 'collection', + placeholder: 'Add Additional Parameters', + displayOptions: { + show: { + resource: ['object'], + operation: ['create'], + }, + }, + default: {}, + options: [ + { + displayName: 'Content Encoding', + name: 'contentEncoding', + type: 'string', + default: '', + }, + ...metagenerationFilters, + { + displayName: 'KMS Key Name', + name: 'kmsKeyName', + type: 'string', + default: '', + }, + predefinedAclOptions, + ], + }, + { + displayName: 'Additional Parameters', + name: 'getParameters', + type: 'collection', + placeholder: 'Add Additional Parameters', + displayOptions: { + show: { + resource: ['object'], + operation: ['delete', 'get'], + }, + }, + default: {}, + options: [...metagenerationFilters], + }, + { + displayName: 'Additional Parameters', + name: 'metagenAndAclQuery', + type: 'collection', + placeholder: 'Add Additional Parameters', + displayOptions: { + show: { + resource: ['object'], + operation: ['update'], + }, + }, + default: {}, + options: [...metagenerationFilters, predefinedAclOptions], + }, + { + displayName: 'Encryption Headers', + name: 'encryptionHeaders', + type: 'collection', + placeholder: 'Add Encryption Headers', + displayOptions: { + show: { + resource: ['object'], + operation: ['create', 'get', 'update'], + }, + }, + default: {}, + options: [ + { + displayName: 'Encryption Algorithm', + name: 'X-Goog-Encryption-Algorithm', + type: 'options', + placeholder: + 'The encryption algorithm to use, which must be AES256. Use to supply your own key in the request', + default: 'AES256', + options: [ + { + name: 'AES256', + value: 'AES256', + }, + ], + }, + { + displayName: 'Encryption Key', + name: 'X-Goog-Encryption-Key', + type: 'string', + placeholder: 'Base64 encoded string of your AES256 encryption key', + default: '', + }, + { + displayName: 'Encryption Key Hash', + name: 'X-Goog-Encryption-Key-Sha256', + type: 'string', + placeholder: 'Base64 encoded string of the SHA256 hash of your encryption key', + default: '', + }, + ], + }, + { + displayName: 'Additional Parameters', + name: 'listFilters', + type: 'collection', + placeholder: 'Add Additional Parameters', + displayOptions: { + show: { + resource: ['object'], + operation: ['getAll'], + }, + }, + default: {}, + options: [ + { + displayName: 'Delimiter', + name: 'delimiter', + type: 'string', + placeholder: 'Returns results in directory-like mode, using this value as the delimiter', + default: '/', + }, + { + displayName: 'End Offset', + name: 'endOffset', + type: 'string', + placeholder: 'Filter results to names lexicographically before this value', + default: '', + }, + { + displayName: 'Include Trailing Delimiter', + name: 'includeTrailingDelimiter', + type: 'boolean', + placeholder: + 'If true, objects will appear with exactly one instance of delimiter at the end of the name', + default: false, + }, + { + displayName: 'Prefix', + name: 'prefix', + type: 'string', + placeholder: 'Filter results to names that start with this value', + default: '', + }, + { + displayName: 'Start Offset', + name: 'startOffset', + type: 'string', + placeholder: 'Filter results to names lexicographically equal or after this value', + default: '', + }, + { + displayName: 'Versions', + name: 'versions', + type: 'boolean', + placeholder: 'If true, list all versions of objects as distinct entries', + default: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Google/CloudStorage/googleCloudStorage.svg b/packages/nodes-base/nodes/Google/CloudStorage/googleCloudStorage.svg new file mode 100644 index 0000000000..59db35d960 --- /dev/null +++ b/packages/nodes-base/nodes/Google/CloudStorage/googleCloudStorage.svg @@ -0,0 +1 @@ +Icon_24px_CloudStorage_Color \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dbd277af09..a010bb87ed 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -117,6 +117,7 @@ "dist/credentials/GoogleBooksOAuth2Api.credentials.js", "dist/credentials/GoogleCalendarOAuth2Api.credentials.js", "dist/credentials/GoogleCloudNaturalLanguageOAuth2Api.credentials.js", + "dist/credentials/GoogleCloudStorageOAuth2Api.credentials.js", "dist/credentials/GoogleContactsOAuth2Api.credentials.js", "dist/credentials/GoogleDocsOAuth2Api.credentials.js", "dist/credentials/GoogleDriveOAuth2Api.credentials.js", @@ -452,6 +453,7 @@ "dist/nodes/Google/Calendar/GoogleCalendarTrigger.node.js", "dist/nodes/Google/Chat/GoogleChat.node.js", "dist/nodes/Google/CloudNaturalLanguage/GoogleCloudNaturalLanguage.node.js", + "dist/nodes/Google/CloudStorage/GoogleCloudStorage.node.js", "dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Docs/GoogleDocs.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js",