Add Firebase node (#1173)

*  Added node for Google firebase Firestore database

*  added firebase's realtime database node

* Added operation to run queries on documents collection

* Improvements to Firebase Database nodes
- Realtime Database: improved how the node interacts with input database
- Cloud Firestore: improved how the node interacts with input database
- Cloud Firestore: improved input / output format so it's similar to JSON and more intuitive, abstracting Firestore's format

*  Improvements to Firestore-Node

*  improvements to Firebase-Node

*  Improvements

 Improvements

*  Improvements

*  Minor improvements to Firebase Nodes

Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
Jan 2020-11-19 07:47:26 +01:00 committed by GitHub
parent c4518f8f17
commit 69a350e262
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1692 additions and 0 deletions

View file

@ -0,0 +1,26 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/firebase',
];
export class GoogleFirebaseCloudFirestoreOAuth2Api implements ICredentialType {
name = 'googleFirebaseCloudFirestoreOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google Firebase Cloud Firestore OAuth2 API';
documentationUrl = 'google';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View file

@ -0,0 +1,27 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/firebase.database',
'https://www.googleapis.com/auth/firebase',
];
export class GoogleFirebaseRealtimeDatabaseOAuth2Api implements ICredentialType {
name = 'googleFirebaseRealtimeDatabaseOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google Firebase Realtime Database OAuth2 API';
documentationUrl = 'google';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View file

@ -0,0 +1,341 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
fullDocumentToJson,
googleApiRequest,
googleApiRequestAllItems,
jsonToDocument
} from './GenericFunctions';
import {
collectionFields,
collectionOperations,
} from './CollectionDescription';
import {
documentFields,
documentOperations,
} from './DocumentDescription';
export class CloudFirestore implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Firebase Cloud Firestore',
name: 'googleFirebaseCloudFirestore',
icon: 'file:googleFirebaseCloudFirestore.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Interact with Google Firebase - Cloud Firestore API',
defaults: {
name: 'Google Cloud Firestore',
color: '#ffcb2d',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'googleFirebaseCloudFirestoreOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Document',
value: 'document',
},
{
name: 'Collection',
value: 'collection',
},
],
default: 'document',
description: 'The resource to operate on.',
},
...documentOperations,
...documentFields,
...collectionOperations,
...collectionFields,
],
};
methods = {
loadOptions: {
async getProjects(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const collections = await googleApiRequestAllItems.call(
this,
'results',
'GET',
'',
{},
{},
'https://firebase.googleapis.com/v1beta1/projects',
);
// @ts-ignore
const returnData = collections.map(o => ({ name: o.projectId, value: o.projectId })) as INodePropertyOptions[];
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
if (resource === 'document') {
if (operation === 'get') {
const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string;
const simple = this.getNodeParameter('simple', 0) as boolean;
const documentList = items.map((item: IDataObject, i: number) => {
const collection = this.getNodeParameter('collection', i) as string;
const documentId = this.getNodeParameter('documentId', i) as string;
return `projects/${projectId}/databases/${database}/documents/${collection}/${documentId}`;
});
responseData = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents:batchGet`,
{ documents: documentList },
);
if (simple === false) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push.apply(returnData, responseData.map((element: IDataObject) => {
return fullDocumentToJson(element.found as IDataObject);
}).filter((el: IDataObject) => !!el));
}
} else if (operation === 'create') {
const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string;
const simple = this.getNodeParameter('simple', 0) as boolean;
await Promise.all(items.map(async (item: IDataObject, i: number) => {
const collection = this.getNodeParameter('collection', i) as string;
const columns = this.getNodeParameter('columns', i) as string;
const columnList = columns.split(',').map(column => column.trim());
const document = { fields: {} };
columnList.map(column => {
// @ts-ignore
document.fields[column] = item['json'][column] ? jsonToDocument(item['json'][column]) : jsonToDocument(null);
});
responseData = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents/${collection}`,
document,
);
if (simple === false) {
returnData.push(responseData);
} else {
returnData.push(fullDocumentToJson(responseData as IDataObject));
}
}));
} else if (operation === 'getAll') {
const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string;
const collection = this.getNodeParameter('collection', 0) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as string;
const simple = this.getNodeParameter('simple', 0) as boolean;
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
'documents',
'GET',
`/${projectId}/databases/${database}/documents/${collection}`,
);
} else {
const limit = this.getNodeParameter('limit', 0) as string;
const getAllResponse = await googleApiRequest.call(
this,
'GET',
`/${projectId}/databases/${database}/documents/${collection}`,
{},
{ pageSize: limit },
) as IDataObject;
responseData = getAllResponse.documents;
}
if (simple === false) {
returnData.push.apply(returnData, responseData);
} else {
returnData.push.apply(returnData, responseData.map((element: IDataObject) => fullDocumentToJson(element as IDataObject)));
}
} else if (operation === 'delete') {
const responseData: IDataObject[] = [];
await Promise.all(items.map(async (item: IDataObject, i: number) => {
const projectId = this.getNodeParameter('projectId', i) as string;
const database = this.getNodeParameter('database', i) as string;
const collection = this.getNodeParameter('collection', i) as string;
const documentId = this.getNodeParameter('documentId', i) as string;
await googleApiRequest.call(
this,
'DELETE',
`/${projectId}/databases/${database}/documents/${collection}/${documentId}`,
);
responseData.push({ success: true });
}));
returnData.push.apply(returnData, responseData);
} else if (operation === 'upsert') {
const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string;
const updates = items.map((item: IDataObject, i: number) => {
const collection = this.getNodeParameter('collection', i) as string;
const updateKey = this.getNodeParameter('updateKey', i) as string;
// @ts-ignore
const documentId = item['json'][updateKey] as string;
const columns = this.getNodeParameter('columns', i) as string;
const columnList = columns.split(',').map(column => column.trim()) as string[];
const document = {};
columnList.map(column => {
// @ts-ignore
document[column] = item['json'].hasOwnProperty(column) ? jsonToDocument(item['json'][column]) : jsonToDocument(null);
});
return {
update: {
name: `projects/${projectId}/databases/${database}/documents/${collection}/${documentId}`,
fields: document,
},
updateMask: {
fieldPaths: columnList,
},
};
});
responseData = [];
const { writeResults, status } = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents:batchWrite`,
{ writes: updates },
);
for (let i = 0; i < writeResults.length; i++) {
writeResults[i]['status'] = status[i];
Object.assign(writeResults[i], items[i].json);
responseData.push(writeResults[i]);
}
returnData.push.apply(returnData, responseData);
// } else if (operation === 'update') {
// const projectId = this.getNodeParameter('projectId', 0) as string;
// const database = this.getNodeParameter('database', 0) as string;
// const simple = this.getNodeParameter('simple', 0) as boolean;
// await Promise.all(items.map(async (item: IDataObject, i: number) => {
// const collection = this.getNodeParameter('collection', i) as string;
// const updateKey = this.getNodeParameter('updateKey', i) as string;
// // @ts-ignore
// const documentId = item['json'][updateKey] as string;
// const columns = this.getNodeParameter('columns', i) as string;
// const columnList = columns.split(',').map(column => column.trim()) as string[];
// const document = {};
// columnList.map(column => {
// // @ts-ignore
// document[column] = item['json'].hasOwnProperty(column) ? jsonToDocument(item['json'][column]) : jsonToDocument(null);
// });
// responseData = await googleApiRequest.call(
// this,
// 'PATCH',
// `/${projectId}/databases/${database}/documents/${collection}/${documentId}`,
// { fields: document },
// { [`updateMask.fieldPaths`]: columnList },
// );
// if (simple === false) {
// returnData.push(responseData);
// } else {
// returnData.push(fullDocumentToJson(responseData as IDataObject));
// }
// }));
} else if (operation === 'query') {
const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string;
const simple = this.getNodeParameter('simple', 0) as boolean;
await Promise.all(items.map(async (item: IDataObject, i: number) => {
const query = this.getNodeParameter('query', i) as string;
responseData = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents:runQuery`,
JSON.parse(query),
);
if (simple === false) {
returnData.push.apply(returnData, responseData);
} else {
//@ts-ignore
returnData.push.apply(returnData, responseData.map((element: IDataObject) => {
return fullDocumentToJson(element.document as IDataObject);
}).filter((element: IDataObject) => !!element));
}
}));
}
} else if (resource === 'collection') {
if (operation === 'getAll') {
const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as string;
if (returnAll) {
const getAllResponse = await googleApiRequestAllItems.call(
this,
'collectionIds',
'POST',
`/${projectId}/databases/${database}/documents:listCollectionIds`,
);
// @ts-ignore
responseData = getAllResponse.map(o => ({ name: o }));
} else {
const limit = this.getNodeParameter('limit', 0) as string;
const getAllResponse = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents:listCollectionIds`,
{},
{ pageSize: limit },
) as IDataObject;
// @ts-ignore
responseData = getAllResponse.collectionIds.map(o => ({ name: o }));
}
returnData.push.apply(returnData, responseData);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,114 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const collectionOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'collection',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all root collections',
},
],
default: 'getAll',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const collectionFields = [
/* -------------------------------------------------------------------------- */
/* collection:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'collection',
],
operation: [
'getAll',
],
},
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'collection',
],
operation: [
'getAll',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'collection',
],
operation: [
'getAll',
],
},
},
description: 'If all results should be returned or only up to a given limit.',
required: true,
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'collection',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,745 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const documentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'document',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a document',
},
{
name: 'Create/Update',
value: 'upsert',
description: 'Create/Update a document',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a document',
},
{
name: 'Get',
value: 'get',
description: 'Get a document',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all documents from a collection',
},
// {
// name: 'Update',
// value: 'update',
// description: 'Update a document',
// },
{
name: 'Query',
value: 'query',
description: 'Runs a query against your documents',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const documentFields = [
/* -------------------------------------------------------------------------- */
/* document:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'create',
],
},
},
description: 'As displayed in firebase console URL.',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'create',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'create',
],
},
},
description: 'Collection name',
required: true,
},
{
displayName: 'Columns / attributes',
name: 'columns',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'create',
],
},
},
description: 'List of attributes to save',
required: true,
placeholder: 'productId, modelName, description',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'document',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
/* -------------------------------------------------------------------------- */
/* document:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'get',
],
},
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'get',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'get',
],
},
},
description: 'Collection name',
required: true,
},
{
displayName: 'Document ID',
name: 'documentId',
type: 'string',
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'document',
],
},
},
default: '',
description: 'Document ID',
required: true,
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'document',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
/* -------------------------------------------------------------------------- */
/* document:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'getAll',
],
},
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'getAll',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'getAll',
],
},
},
description: 'Collection name',
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'getAll',
],
},
},
description: 'If all results should be returned or only up to a given limit.',
required: true,
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'document',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
/* -------------------------------------------------------------------------- */
/* document:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'delete',
],
},
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'delete',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'delete',
],
},
},
description: 'Collection name',
required: true,
},
{
displayName: 'Document ID',
name: 'documentId',
type: 'string',
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'document',
],
},
},
default: '',
description: 'Document ID',
required: true,
},
// /* ---------------------------------------------------------------------- */
// /* document:update */
// /* -------------------------------------------------------------------------- */
// {
// displayName: 'Project ID',
// name: 'projectId',
// type: 'options',
// default: '',
// typeOptions: {
// loadOptionsMethod: 'getProjects',
// },
// displayOptions: {
// show: {
// resource: [
// 'document',
// ],
// operation: [
// 'update',
// ],
// },
// },
// description: 'As displayed in firebase console URL',
// required: true,
// },
// {
// displayName: 'Database',
// name: 'database',
// type: 'string',
// default: '(default)',
// displayOptions: {
// show: {
// resource: [
// 'document',
// ],
// operation: [
// 'update',
// ],
// },
// },
// description: 'Usually the provided default value will work',
// required: true,
// },
// {
// displayName: 'Collection',
// name: 'collection',
// type: 'string',
// default: '',
// displayOptions: {
// show: {
// resource: [
// 'document',
// ],
// operation: [
// 'update',
// ],
// },
// },
// description: 'Collection name',
// required: true,
// },
// {
// displayName: 'Update Key',
// name: 'updateKey',
// type: 'string',
// displayOptions: {
// show: {
// resource: [
// 'document',
// ],
// operation: [
// 'update',
// ],
// },
// },
// default: '',
// description: 'Must correspond to a document ID',
// required: true,
// placeholder: 'documentId',
// },
// {
// displayName: 'Columns /Attributes',
// name: 'columns',
// type: 'string',
// default: '',
// displayOptions: {
// show: {
// resource: [
// 'document',
// ],
// operation: [
// 'update',
// ],
// },
// },
// description: 'Columns to insert',
// required: true,
// placeholder: 'age, city, location',
// },
// {
// displayName: 'Simple',
// name: 'simple',
// type: 'boolean',
// displayOptions: {
// show: {
// operation: [
// 'update',
// ],
// resource: [
// 'document',
// ],
// },
// },
// default: true,
// description: 'When set to true a simplify version of the response will be used else the raw data.',
// },
/* -------------------------------------------------------------------------- */
/* document:upsert */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'upsert',
],
},
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'upsert',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'upsert',
],
},
},
description: 'Collection name',
required: true,
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'upsert',
],
},
},
default: '',
description: 'Must correspond to a document ID',
required: true,
placeholder: 'documentId',
},
{
displayName: 'Columns /Attributes',
name: 'columns',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'upsert',
],
},
},
description: 'Columns to insert',
required: true,
placeholder: 'age, city, location',
},
/* -------------------------------------------------------------------------- */
/* document:query */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'query',
],
},
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Database',
name: 'database',
type: 'string',
default: '(default)',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'query',
],
},
},
description: 'Usually the provided default value will work',
required: true,
},
{
displayName: 'Query JSON',
name: 'query',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'document',
],
operation: [
'query',
],
},
},
description: 'JSON query to execute',
required: true,
typeOptions: {
alwaysOpenEditWindow: true,
},
placeholder: '{"structuredQuery": {"where": {"fieldFilter": {"field": {"fieldPath": "age"},"op": "EQUAL", "value": {"integerValue": 28}}}, "from": [{"collectionId": "users-collection"}]}}',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: [
'query',
],
resource: [
'document',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
] as INodeProperties[];

View file

@ -0,0 +1,153 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri: string | null = null): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
qsStringifyOptions: {
arrayFormat: 'repeat',
},
uri: uri || `https://firestore.googleapis.com/v1/projects${resource}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'googleFirebaseCloudFirestoreOAuth2Api', options);
} catch (error) {
let errors;
if (error.response && error.response.body) {
if (Array.isArray(error.response.body)) {
errors = error.response.body;
errors = errors.map((e: { error: { message: string } }) => e.error.message).join('|');
} else {
errors = error.response.body.error.message;
}
// Try to return the error prettier
throw new Error(
`Google Firebase error response [${error.statusCode}]: ${errors}`,
);
}
throw error;
}
}
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri: string | null = null): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.pageSize = 100;
do {
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
query.pageToken = responseData['nextPageToken'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['nextPageToken'] !== undefined &&
responseData['nextPageToken'] !== ''
);
return returnData;
}
// Both functions below were taken from Stack Overflow jsonToDocument was fixed as it was unable to handle null values correctly
// https://stackoverflow.com/questions/62246410/how-to-convert-a-firestore-document-to-plain-json-and-vice-versa
// Great thanks to https://stackoverflow.com/users/3915246/mahindar
export function jsonToDocument(value: string | number | IDataObject | IDataObject[]): IDataObject {
if (value === 'true' || value === 'false' || typeof value === 'boolean') {
return { 'booleanValue': value };
} else if (value === null) {
return { 'nullValue': null };
} else if (!isNaN(value as number)) {
if (value.toString().indexOf('.') !== -1) {
return { 'doubleValue': value };
} else {
return { 'integerValue': value };
}
} else if (Date.parse(value as string)) {
const date = new Date(Date.parse(value as string));
return { 'timestampValue': date.toISOString() };
} else if (typeof value === 'string') {
return { 'stringValue': value };
} else if (value && value.constructor === Array) {
return { 'arrayValue': { values: value.map(v => jsonToDocument(v)) } };
} else if (typeof value === 'object') {
const obj = {};
for (const o of Object.keys(value)) {
//@ts-ignore
obj[o] = jsonToDocument(value[o]);
}
return { 'mapValue': { fields: obj } };
}
return {};
}
export function fullDocumentToJson(data: IDataObject): IDataObject {
if (data === undefined) {
return data;
}
return {
_name: data.name,
_createTime: data.createTime,
_updateTime: data.updateTime,
...documentToJson(data.fields as IDataObject),
};
}
export function documentToJson(fields: IDataObject): IDataObject {
const result = {};
for (const f of Object.keys(fields)) {
const key = f, value = fields[f],
isDocumentType = ['stringValue', 'booleanValue', 'doubleValue',
'integerValue', 'timestampValue', 'mapValue', 'arrayValue'].find(t => t === key);
if (isDocumentType) {
const item = ['stringValue', 'booleanValue', 'doubleValue', 'integerValue', 'timestampValue']
.find(t => t === key);
if (item) {
return value as IDataObject;
} else if ('mapValue' === key) {
//@ts-ignore
return documentToJson(value!.fields || {});
} else if ('arrayValue' === key) {
// @ts-ignore
const list = value.values as IDataObject[];
// @ts-ignore
return !!list ? list.map(l => documentToJson(l)) : [];
}
} else {
// @ts-ignore
result[key] = documentToJson(value);
}
}
return result;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,78 @@
import {
OptionsWithUrl,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, projectId: string, method: string, resource: string, body: any = {}, qs: IDataObject = {}, headers: IDataObject = {}, uri: string | null = null): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUrl = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
url: uri || `https://${projectId}.firebaseio.com/${resource}.json`,
json: true,
};
try {
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
return await this.helpers.requestOAuth2!.call(this, 'googleFirebaseRealtimeDatabaseOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
let errors;
if (error.response.body.error.errors) {
errors = error.response.body.error.errors;
errors = errors.map((e: IDataObject) => e.message).join('|');
} else {
errors = error.response.body.error.message;
}
// Try to return the error prettier
throw new Error(
`Google Firebase error response [${error.statusCode}]: ${errors}`,
);
}
throw error;
}
}
export async function googleApiRequestAllItems(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, projectId: string, method: string, resource: string, body: any = {}, qs: IDataObject = {}, headers: IDataObject = {}, uri: string | null = null): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
qs.pageSize = 100;
do {
responseData = await googleApiRequest.call(this, projectId, method, resource, body, qs, {}, uri);
qs.pageToken = responseData['nextPageToken'];
returnData.push.apply(returnData, responseData[resource]);
} while (
responseData['nextPageToken'] !== undefined &&
responseData['nextPageToken'] !== ''
);
return returnData;
}

View file

@ -0,0 +1,204 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
googleApiRequest,
googleApiRequestAllItems,
} from './GenericFunctions';
export class RealtimeDatabase implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Firebase Realtime Database',
name: 'googleFirebaseRealtimeDatabase',
icon: 'file:googleFirebaseRealtimeDatabase.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Interact with Google Firebase - Realtime Database API',
defaults: {
name: 'Google Cloud Realtime Database',
color: '#ffcb2d',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'googleFirebaseRealtimeDatabaseOAuth2Api',
},
],
properties: [
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
description: 'As displayed in firebase console URL',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Create',
value: 'create',
description: 'Write data to a database',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete data from a database',
},
{
name: 'Get',
value: 'get',
description: 'Get a record from a database',
},
{
name: 'Push',
value: 'push',
description: 'Append to a list of data',
},
{
name: 'Update',
value: 'update',
description: 'Update item on a database',
},
],
default: 'create',
description: 'The operation to perform.',
required: true,
},
{
displayName: 'Object Path',
name: 'path',
type: 'string',
default: '',
placeholder: '/app/users',
description: 'Object path on database. With leading slash. Do not append .json.',
required: true,
},
{
displayName: 'Columns / Attributes',
name: 'attributes',
type: 'string',
default: '',
displayOptions: {
show: {
operation: [
'create',
'push',
'update',
],
},
},
description: 'Attributes to save',
required: true,
placeholder: 'age, name, city',
},
],
};
methods = {
loadOptions: {
async getProjects(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const projects = await googleApiRequestAllItems.call(
this,
'projects',
'GET',
'results',
{},
{},
{},
'https://firebase.googleapis.com/v1beta1/projects',
);
const returnData = projects.map((o: IDataObject) => ({ name: o.projectId, value: o.projectId })) as INodePropertyOptions[];
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
let responseData;
const operation = this.getNodeParameter('operation', 0) as string;
//https://firebase.google.com/docs/reference/rest/database
if (['push', 'create', 'update'].includes(operation) && items.length === 1 && Object.keys(items[0].json).length === 0) {
throw new Error(`The ${operation} operation needs input data`);
}
for (let i = 0; i < length; i++) {
const projectId = this.getNodeParameter('projectId', i) as string;
let method = 'GET', attributes = '';
const document: IDataObject = {};
if (operation === 'create') {
method = 'PUT';
attributes = this.getNodeParameter('attributes', i) as string;
} else if (operation === 'delete') {
method = 'DELETE';
} else if (operation === 'get') {
method = 'GET';
} else if (operation === 'push') {
method = 'POST';
attributes = this.getNodeParameter('attributes', i) as string;
} else if (operation === 'update') {
method = 'PATCH';
attributes = this.getNodeParameter('attributes', i) as string;
}
if (attributes) {
const attributeList = attributes.split(',').map(el => el.trim());
attributeList.map((attribute: string) => {
if (items[i].json.hasOwnProperty(attribute)) {
document[attribute] = items[i].json[attribute];
}
});
}
responseData = await googleApiRequest.call(
this,
projectId,
method,
this.getNodeParameter('path', i) as string,
document,
);
if (responseData === null) {
if (operation === 'get') {
throw new Error(`Google Firebase error response: Requested entity was not found.`);
} else if (method === 'DELETE') {
responseData = { success: true };
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (typeof responseData === 'string' || typeof responseData === 'number') {
returnData.push({ [this.getNodeParameter('path', i) as string]: responseData } as IDataObject);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -83,6 +83,8 @@
"dist/credentials/GoogleCalendarOAuth2Api.credentials.js", "dist/credentials/GoogleCalendarOAuth2Api.credentials.js",
"dist/credentials/GoogleContactsOAuth2Api.credentials.js", "dist/credentials/GoogleContactsOAuth2Api.credentials.js",
"dist/credentials/GoogleDriveOAuth2Api.credentials.js", "dist/credentials/GoogleDriveOAuth2Api.credentials.js",
"dist/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.js",
"dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js",
"dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js",
"dist/credentials/GoogleSheetsOAuth2Api.credentials.js", "dist/credentials/GoogleSheetsOAuth2Api.credentials.js",
"dist/credentials/GSuiteAdminOAuth2Api.credentials.js", "dist/credentials/GSuiteAdminOAuth2Api.credentials.js",
@ -291,6 +293,8 @@
"dist/nodes/Google/Calendar/GoogleCalendar.node.js", "dist/nodes/Google/Calendar/GoogleCalendar.node.js",
"dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Contacts/GoogleContacts.node.js",
"dist/nodes/Google/Drive/GoogleDrive.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js",
"dist/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.js",
"dist/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.js",
"dist/nodes/Google/Gmail/Gmail.node.js", "dist/nodes/Google/Gmail/Gmail.node.js",
"dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js",
"dist/nodes/Google/Sheet/GoogleSheets.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js",