pull:初次提交

This commit is contained in:
Yep_Q
2025-09-08 04:48:28 +08:00
parent 5c0619656d
commit f64f498365
11751 changed files with 1953723 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
import type { INodeProperties } from 'n8n-workflow';
export const collectionOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['collection'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Get many root collections',
action: 'Get many collections',
},
],
default: 'getAll',
},
];
export const collectionFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* collection:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['collection'],
operation: ['getAll'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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: 'Whether to return all results 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: 'Max number of results to return',
},
];

View File

@@ -0,0 +1,644 @@
import type { INodeProperties } from 'n8n-workflow';
export const documentOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['document'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a document',
action: 'Create a document',
},
{
name: 'Create or Update',
value: 'upsert',
description:
'Create a new document, or update the current one if it already exists (upsert)',
action: 'Create or update a document',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a document',
action: 'Delete a document',
},
{
name: 'Get',
value: 'get',
description: 'Get a document',
action: 'Get a document',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many documents from a collection',
action: 'Get many documents',
},
// {
// name: 'Update',
// value: 'update',
// description: 'Update a document',
// },
{
name: 'Query',
value: 'query',
description: 'Runs a query against your documents',
action: 'Query a document',
},
],
default: 'get',
},
];
export const documentFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* document:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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: 'Document ID',
name: 'documentId',
type: 'string',
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
default: '',
},
{
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: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: ['create'],
resource: ['document'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* document:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['document'],
operation: ['get'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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: '',
required: true,
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: ['get'],
resource: ['document'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* document:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['document'],
operation: ['getAll'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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: 'Whether to return all results 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: 'Max number of results to return',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['document'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* document:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['document'],
operation: ['delete'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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: '',
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 Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['document'],
operation: ['upsert'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
displayOptions: {
show: {
resource: ['document'],
operation: ['query'],
},
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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,
placeholder:
'{"structuredQuery": {"where": {"fieldFilter": {"field": {"fieldPath": "age"},"op": "EQUAL", "value": {"integerValue": 28}}}, "from": [{"collectionId": "users-collection"}]}}',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: ['query'],
resource: ['document'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];

View File

@@ -0,0 +1,177 @@
import moment from 'moment-timezone';
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IDataObject,
JsonObject,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
import { isSafeObjectProperty, NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../../GenericFunctions';
export async function googleApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
uri: string | null = null,
): Promise<any> {
const options: IRequestOptions = {
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 as IDataObject).length === 0) {
delete options.body;
}
let credentialType = 'googleFirebaseCloudFirestoreOAuth2Api';
const authentication = this.getNodeParameter('authentication', 0) as string;
if (authentication === 'serviceAccount') {
const credentials = await this.getCredentials('googleApi');
credentialType = 'googleApi';
const { access_token } = await getGoogleAccessToken.call(this, credentials, 'firestore');
(options.headers as IDataObject).Authorization = `Bearer ${access_token}`;
}
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function googleApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
uri: string | null = null,
): Promise<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] as IDataObject[]);
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
return returnData;
}
const isValidDate = (str: string) =>
moment(str, ['YYYY-MM-DD HH:mm:ss Z', moment.ISO_8601], true).isValid();
// 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 (value !== '' && !isNaN(value as number)) {
if (value.toString().indexOf('.') !== -1) {
return { doubleValue: value };
} else {
return { integerValue: value };
}
} else if (isValidDate(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: IDataObject = {};
for (const key of Object.keys(value)) {
if (value.hasOwnProperty(key) && isSafeObjectProperty(key)) {
obj[key] = jsonToDocument((value as IDataObject)[key] as IDataObject);
}
}
return { mapValue: { fields: obj } };
}
return {};
}
export function documentToJson(fields: IDataObject): IDataObject {
if (fields === undefined) return {};
const result = {};
for (const f of Object.keys(fields)) {
const key = f,
value = fields[f],
isDocumentType = [
'stringValue',
'booleanValue',
'doubleValue',
'integerValue',
'timestampValue',
'mapValue',
'arrayValue',
'nullValue',
'geoPointValue',
].find((t) => t === key);
if (isDocumentType) {
const item = [
'stringValue',
'booleanValue',
'doubleValue',
'integerValue',
'timestampValue',
'nullValue',
'geoPointValue',
].find((t) => t === key);
if (item) {
return value as IDataObject;
} else if ('mapValue' === key) {
//@ts-ignore
return documentToJson((value!.fields as IDataObject) || {});
} 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;
}
export function fullDocumentToJson(data: IDataObject): IDataObject {
if (data === undefined) {
return data;
}
return {
_name: data.name,
_id: data.id,
_createTime: data.createTime,
_updateTime: data.updateTime,
...documentToJson(data.fields as IDataObject),
};
}

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.googleFirebaseCloudFirestore",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlecloudfirestore/"
}
]
}
}

View File

@@ -0,0 +1,450 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, jsonParse } from 'n8n-workflow';
import { collectionFields, collectionOperations } from './CollectionDescription';
import { documentFields, documentOperations } from './DocumentDescription';
import {
fullDocumentToJson,
googleApiRequest,
googleApiRequestAllItems,
jsonToDocument,
} from './GenericFunctions';
import { generatePairedItemData } from '../../../../utils/utilities';
export class GoogleFirebaseCloudFirestore implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Cloud Firestore',
name: 'googleFirebaseCloudFirestore',
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
icon: 'file:googleFirebaseCloudFirestore.png',
group: ['input'],
version: [1, 1.1],
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Interact with Google Firebase - Cloud Firestore API',
defaults: {
name: 'Google Cloud Firestore',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'googleFirebaseCloudFirestoreOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['googleFirebaseCloudFirestoreOAuth2Api'],
},
},
},
{
name: 'googleApi',
required: true,
displayOptions: {
show: {
authentication: ['serviceAccount'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'OAuth2 (recommended)',
value: 'googleFirebaseCloudFirestoreOAuth2Api',
},
{
name: 'Service Account',
value: 'serviceAccount',
},
],
default: 'googleFirebaseCloudFirestoreOAuth2Api',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Document',
value: 'document',
},
{
name: 'Collection',
value: 'collection',
},
],
default: 'document',
},
...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 itemData = generatePairedItemData(items.length);
const returnData: INodeExecutionData[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion;
let itemsLength = items.length ? 1 : 0;
let fallbackPairedItems;
if (nodeVersion >= 1.1) {
itemsLength = items.length;
} else {
fallbackPairedItems = generatePairedItemData(items.length);
}
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((_: 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 },
);
responseData = responseData.map((element: { found: { id: string; name: string } }) => {
if (element.found) {
element.found.id = element.found.name.split('/').pop() as string;
}
return element;
});
if (simple) {
responseData = responseData
.map((element: IDataObject) => {
return fullDocumentToJson(element.found as IDataObject);
})
.filter((el: IDataObject) => !!el);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData },
);
returnData.push(...executionData);
} 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 documentId = this.getNodeParameter('documentId', i) as string;
const columnList = columns.split(',').map((column) => column.trim());
const document = { fields: {} };
columnList.map((column) => {
// @ts-ignore
if (item.json[column]) {
// @ts-ignore
document.fields[column] = jsonToDocument(item.json[column] as IDataObject);
} else {
// @ts-ignore
document.fields[column] = jsonToDocument(null);
}
});
responseData = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents/${collection}`,
document,
{ documentId },
);
responseData.id = (responseData.name as string).split('/').pop();
if (simple) {
responseData = fullDocumentToJson(responseData as IDataObject);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}),
);
} else if (operation === 'getAll') {
for (let i = 0; i < itemsLength; i++) {
try {
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 returnAll = this.getNodeParameter('returnAll', i);
const simple = this.getNodeParameter('simple', i) as boolean;
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
'documents',
'GET',
`/${projectId}/databases/${database}/documents/${collection}`,
);
} else {
const limit = this.getNodeParameter('limit', i);
const getAllResponse = (await googleApiRequest.call(
this,
'GET',
`/${projectId}/databases/${database}/documents/${collection}`,
{},
{ pageSize: limit },
)) as IDataObject;
responseData = getAllResponse.documents;
}
responseData = responseData.map((element: IDataObject) => {
element.id = (element.name as string).split('/').pop();
return element;
});
if (simple) {
responseData = responseData.map((element: IDataObject) =>
fullDocumentToJson(element),
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: fallbackPairedItems ?? [{ item: i }] },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: fallbackPairedItems ?? [{ item: i }],
});
continue;
}
throw error;
}
}
} else if (operation === 'delete') {
await Promise.all(
items.map(async (_: 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}`,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}),
);
} 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());
const document = {};
columnList.map((column) => {
// @ts-ignore
if (item.json.hasOwnProperty(column)) {
// @ts-ignore
document[column] = jsonToDocument(item.json[column] as IDataObject);
} else {
// @ts-ignore
document[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);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(writeResults[i] as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
} 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 (_: IDataObject, i: number) => {
const query = this.getNodeParameter('query', i) as string;
responseData = await googleApiRequest.call(
this,
'POST',
`/${projectId}/databases/${database}/documents:runQuery`,
jsonParse(query),
);
responseData = responseData.map(
(element: { document: { id: string; name: string } }) => {
if (element.document) {
element.document.id = element.document.name.split('/').pop() as string;
}
return element;
},
);
if (simple) {
responseData = responseData
.map((element: IDataObject) => {
return fullDocumentToJson(element.document as IDataObject);
})
.filter((element: IDataObject) => !!element);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}),
);
}
} else if (resource === 'collection') {
if (operation === 'getAll') {
for (let i = 0; i < itemsLength; i++) {
try {
const projectId = this.getNodeParameter('projectId', i) as string;
const database = this.getNodeParameter('database', i) as string;
const returnAll = this.getNodeParameter('returnAll', i);
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', i);
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 }));
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: fallbackPairedItems ?? [{ item: i }] },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: fallbackPairedItems ?? [{ item: i }],
});
continue;
}
throw error;
}
}
}
}
return [returnData];
}
}

View File

@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"version": 1
}

View File

@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"_id": {
"type": "string"
},
"_name": {
"type": "string"
}
},
"version": 3
}

View File

@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"_createTime": {
"type": "string"
},
"_id": {
"type": "string"
},
"_name": {
"type": "string"
},
"_updateTime": {
"type": "string"
}
},
"version": 2
}

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"_createTime": {
"type": "string"
},
"_id": {
"type": "string"
},
"_name": {
"type": "string"
},
"_updateTime": {
"type": "string"
}
},
"version": 3
}

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"_createTime": {
"type": "string"
},
"_id": {
"type": "string"
},
"_name": {
"type": "string"
},
"_updateTime": {
"type": "string"
}
},
"version": 3
}

View File

@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"updateTime": {
"type": "string"
}
},
"version": 1
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,85 @@
import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
IRequestOptions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export async function googleApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
projectId: string,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
headers: IDataObject = {},
uri: string | null = null,
): Promise<any> {
const { region } = await this.getCredentials('googleFirebaseRealtimeDatabaseOAuth2Api');
const options: IRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
url: uri || `https://${projectId}.${region}/${resource}.json`,
json: true,
};
try {
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body as IDataObject).length === 0) {
delete options.body;
}
return await this.helpers.requestOAuth2.call(
this,
'googleFirebaseRealtimeDatabaseOAuth2Api',
options,
);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function googleApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
projectId: string,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
_headers: IDataObject = {},
uri: string | null = null,
): Promise<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] as IDataObject[]);
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
return returnData;
}

View File

@@ -0,0 +1,25 @@
{
"node": "n8n-nodes-base.googleFirebaseRealtimeDatabase",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlecloudrealtimedatabase/"
}
],
"generic": [
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
}
]
}
}

View File

@@ -0,0 +1,256 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
JsonObject,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError, NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
export class GoogleFirebaseRealtimeDatabase implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Cloud Realtime Database',
name: 'googleFirebaseRealtimeDatabase',
icon: 'file:googleFirebaseRealtimeDatabase.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Interact with Google Firebase - Realtime Database API',
defaults: {
name: 'Google Cloud Realtime Database',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'googleFirebaseRealtimeDatabaseOAuth2Api',
},
],
properties: [
{
displayName: 'Project Name or ID',
name: 'projectId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
description:
'As displayed in firebase console URL. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Create',
value: 'create',
description: 'Write data to a database',
action: 'Write data to a database',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete data from a database',
action: 'Delete data from a database',
},
{
name: 'Get',
value: 'get',
description: 'Get a record from a database',
action: 'Get a record from a database',
},
{
name: 'Push',
value: 'push',
description: 'Append to a list of data',
action: 'Append to a list of data',
},
{
name: 'Update',
value: 'update',
description: 'Update item on a database',
action: 'Update item in a database',
},
],
default: 'create',
required: true,
},
{
displayName: 'Object Path',
name: 'path',
type: 'string',
default: '',
placeholder: 'e.g. /app/users',
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-json
description: 'Object path on database. Do not append .json.',
required: true,
displayOptions: {
hide: {
operation: ['get'],
},
},
},
{
displayName: 'Object Path',
name: 'path',
type: 'string',
default: '',
placeholder: 'e.g. /app/users',
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-json
description: 'Object path on database. Do not append .json.',
hint: 'Leave blank to get a whole database object',
displayOptions: {
show: {
operation: ['get'],
},
},
},
{
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,
'',
'GET',
'results',
{},
{},
{},
'https://firebase.googleapis.com/v1beta1/projects',
);
const returnData = projects
// select only realtime database projects
.filter(
(project: IDataObject) => (project.resources as IDataObject).realtimeDatabaseInstance,
)
.map((project: IDataObject) => ({
name: project.projectId,
value: (project.resources as IDataObject).realtimeDatabaseInstance,
})) as INodePropertyOptions[];
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
let responseData;
const operation = this.getNodeParameter('operation', 0);
//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 NodeOperationError(this.getNode(), `The ${operation} operation needs input data`);
}
for (let i = 0; i < length; i++) {
try {
const projectId = this.getNodeParameter('projectId', i) as string;
let method: IHttpRequestMethods = '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 NodeApiError(this.getNode(), responseData as JsonObject, {
message: 'Requested entity was not found.',
});
} else if (method === 'DELETE') {
responseData = { success: true };
}
}
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
if (typeof responseData === 'string' || typeof responseData === 'number') {
responseData = {
[this.getNodeParameter('path', i) as string]: responseData,
};
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
return [returnData];
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192"><defs><linearGradient id="b" x1="56.9" x2="48.9" y1="102.54" y2="98.36" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a52714"/><stop offset=".4" stop-color="#a52714" stop-opacity=".5"/><stop offset=".8" stop-color="#a52714" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="90.89" x2="87.31" y1="90.91" y2="87.33" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a52714" stop-opacity=".8"/><stop offset=".5" stop-color="#a52714" stop-opacity=".21"/><stop offset="1" stop-color="#a52714" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="27.188" x2="160.875" y1="40.281" y2="173.968" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><clipPath id="a"><path fill="none" d="M143.41 47.34a4 4 0 0 0-6.77-2.16L115.88 66 99.54 34.89a4 4 0 0 0-7.08 0l-8.93 17-22.4-41.77a4 4 0 0 0-7.48 1.28L32 150l57.9 32.46a12 12 0 0 0 11.7 0L160 150z"/></clipPath></defs><g fill="none"><path d="M0 0h192v192H0z"/><g clip-path="url(#a)"><path fill="#ffa000" d="M32 150 53.66 11.39a4 4 0 0 1 7.48-1.27l22.4 41.78 8.93-17a4 4 0 0 1 7.08 0L160 150z"/><path fill="url(#b)" d="M106 9 0 0v192l32-42z" opacity=".12"/><path fill="#f57c00" d="m106.83 96.01-23.3-44.12L32 150z"/><path fill="url(#c)" d="M0 0h192v192H0z" opacity=".2"/><path fill="#ffca28" d="M160 150 143.41 47.34a4 4 0 0 0-6.77-2.16L32 150l57.9 32.47a12 12 0 0 0 11.7 0z"/><path fill="#fff" fill-opacity=".2" d="M143.41 47.34a4 4 0 0 0-6.77-2.16L115.88 66 99.54 34.89a4 4 0 0 0-7.08 0l-8.93 17-22.4-41.77a4 4 0 0 0-7.48 1.28L32 150h-.08l.07.08.57.28L115.83 67l20.78-20.8a4 4 0 0 1 6.78 2.16l16.45 101.74.16-.1zM32.19 149.81 53.66 12.39a4 4 0 0 1 7.48-1.28l22.4 41.78 8.93-17a4 4 0 0 1 7.08 0l16 30.43z"/><path fill="#a52714" d="M101.6 181.49a12 12 0 0 1-11.7 0l-57.76-32.4-.14.91 57.9 32.46a12 12 0 0 0 11.7 0L160 150l-.15-.92z" opacity=".2"/><path fill="url(#d)" d="M143.41 47.34a4 4 0 0 0-6.77-2.16L115.88 66 99.54 34.89a4 4 0 0 0-7.08 0l-8.93 17-22.4-41.77a4 4 0 0 0-7.48 1.28L32 150l57.9 32.46a12 12 0 0 0 11.7 0L160 150z"/></g><circle cx="144" cy="144" r="40" fill="#757575"/><path fill="#fff" fill-rule="evenodd" d="M126 150h36v8.004a3.99 3.99 0 0 1-3.99 3.996h-28.02a4 4 0 0 1-3.99-3.996zm0-20.016c0-2.2 1.786-3.984 3.99-3.984h28.02c2.204 0 3.99 1.8 3.99 3.984v14.032c0 2.2-1.786 3.984-3.99 3.984h-28.02c-2.204 0-3.99-1.8-3.99-3.984zm4 .016h28v6h-28zm0 11.01c0-.56.428-1.01 1.01-1.01h1.98c.56 0 1.01.428 1.01 1.01v1.98a.994.994 0 0 1-1.01 1.01h-1.98a.994.994 0 0 1-1.01-1.01zm0 14c0-.56.428-1.01 1.01-1.01h1.98c.56 0 1.01.428 1.01 1.01v1.98a.994.994 0 0 1-1.01 1.01h-1.98a.994.994 0 0 1-1.01-1.01z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB