first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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: 'Name of the field in an input item that contains the 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',
|
||||
},
|
||||
];
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
+18
@@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
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',
|
||||
schemaPath: 'Google/Firebase/CloudFirestore',
|
||||
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];
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_createTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"_updateTime": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 4
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_createTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"_updateTime": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_createTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"_updateTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateTime": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 4
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_createTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"_updateTime": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updateTime": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+565
@@ -0,0 +1,565 @@
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getGoogleAccessToken } from '../../../GenericFunctions';
|
||||
import {
|
||||
googleApiRequest,
|
||||
jsonToDocument,
|
||||
documentToJson,
|
||||
fullDocumentToJson,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
// Mock the getGoogleAccessToken function
|
||||
jest.mock('../../../GenericFunctions', () => ({
|
||||
getGoogleAccessToken: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetGoogleAccessToken = getGoogleAccessToken as jest.MockedFunction<
|
||||
typeof getGoogleAccessToken
|
||||
>;
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore > GenericFunctions', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
mockNode = mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'Test CloudFirestore Node',
|
||||
type: 'n8n-nodes-base.googleFirebaseCloudFirestore',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('googleApiRequest', () => {
|
||||
const mockRequestWithAuth = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = mockRequestWithAuth;
|
||||
mockLoadOptionsFunctions.helpers.requestWithAuthentication = mockRequestWithAuth;
|
||||
});
|
||||
|
||||
describe('OAuth2 authentication', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('oAuth2Api');
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('oAuth2Api');
|
||||
});
|
||||
|
||||
it('should make successful API request with OAuth2', async () => {
|
||||
const mockResponse = { success: true, data: 'test' };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/databases/test/documents/users',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
uri: 'https://firestore.googleapis.com/v1/projects/databases/test/documents/users',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
qs: {},
|
||||
qsStringifyOptions: {
|
||||
arrayFormat: 'repeat',
|
||||
},
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove empty body for GET requests', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test', {});
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.not.objectContaining({ body: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include body for POST requests', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const requestBody = { name: 'Test', value: 'data' };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'POST', '/test', requestBody);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include query parameters', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const queryParams = { pageSize: 100, orderBy: 'name' };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test', {}, queryParams);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
qs: queryParams,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom URI when provided', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const customUri = 'https://custom.googleapis.com/v1/test';
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test', {}, {}, customUri);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
uri: customUri,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service Account authentication', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('serviceAccount');
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('serviceAccount');
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test-key',
|
||||
});
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue({
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test-key',
|
||||
});
|
||||
});
|
||||
|
||||
it('should make API request with service account', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const mockAccessToken = 'mock-access-token';
|
||||
mockGetGoogleAccessToken.mockResolvedValue({ access_token: mockAccessToken });
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockGetGoogleAccessToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test-key',
|
||||
}),
|
||||
'firestore',
|
||||
);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleApi',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle service account credential errors', async () => {
|
||||
const credentialError = new Error('Invalid service account');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(credentialError);
|
||||
|
||||
await expect(googleApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeApiError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle access token errors', async () => {
|
||||
const tokenError = new Error('Failed to get access token');
|
||||
mockGetGoogleAccessToken.mockRejectedValue(tokenError);
|
||||
|
||||
await expect(googleApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeApiError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('oAuth2Api');
|
||||
});
|
||||
|
||||
it('should throw NodeAPIError on API failure', async () => {
|
||||
const apiError = new Error('API request failed');
|
||||
mockRequestWithAuth.mockRejectedValue(apiError);
|
||||
|
||||
await expect(googleApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeApiError,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getNode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work with ILoadOptionsFunctions', async () => {
|
||||
const mockResponse = { options: ['option1', 'option2'] };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(mockLoadOptionsFunctions, 'GET', '/options');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonToDocument', () => {
|
||||
it('should convert boolean values', () => {
|
||||
expect(jsonToDocument(true as any)).toEqual({ booleanValue: true });
|
||||
expect(jsonToDocument(false as any)).toEqual({ booleanValue: false });
|
||||
// Note: The current implementation preserves string 'true'/'false' as strings in booleanValue
|
||||
// While Firestore API spec expects actual boolean values for booleanValue fields,
|
||||
// this behavior is maintained for backward compatibility and to avoid breaking existing workflows
|
||||
// that may depend on this string preservation behavior in certain edge cases
|
||||
expect(jsonToDocument('true')).toEqual({ booleanValue: 'true' });
|
||||
expect(jsonToDocument('false')).toEqual({ booleanValue: 'false' });
|
||||
});
|
||||
|
||||
it('should convert null values', () => {
|
||||
expect(jsonToDocument(null as any)).toEqual({ nullValue: null });
|
||||
});
|
||||
|
||||
it('should convert numeric values', () => {
|
||||
expect(jsonToDocument(42)).toEqual({ integerValue: 42 });
|
||||
expect(jsonToDocument(3.14)).toEqual({ doubleValue: 3.14 });
|
||||
// Note: The current implementation preserves numeric strings as strings in Firestore document values
|
||||
// While Firestore API spec typically expects numeric types for doubleValue/integerValue,
|
||||
// this behavior is maintained for backward compatibility and to avoid breaking existing workflows
|
||||
// that may depend on string preservation in certain edge cases
|
||||
expect(jsonToDocument('123')).toEqual({ integerValue: '123' });
|
||||
expect(jsonToDocument('123.45')).toEqual({ doubleValue: '123.45' });
|
||||
});
|
||||
|
||||
it('should convert string values', () => {
|
||||
expect(jsonToDocument('hello world')).toEqual({ stringValue: 'hello world' });
|
||||
expect(jsonToDocument('')).toEqual({ stringValue: '' });
|
||||
});
|
||||
|
||||
it('should convert date strings to timestamps', () => {
|
||||
const dateString = '2023-01-01T10:00:00.000Z';
|
||||
const result = jsonToDocument(dateString);
|
||||
expect(result).toHaveProperty('timestampValue');
|
||||
expect(result.timestampValue).toBe('2023-01-01T10:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should convert arrays', () => {
|
||||
const array = [1, 'test', true, null] as any;
|
||||
const result = jsonToDocument(array);
|
||||
expect(result).toEqual({
|
||||
arrayValue: {
|
||||
values: [
|
||||
{ integerValue: 1 },
|
||||
{ stringValue: 'test' },
|
||||
{ booleanValue: true },
|
||||
{ nullValue: null },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert objects', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
active: true,
|
||||
metadata: null,
|
||||
};
|
||||
const result = jsonToDocument(obj);
|
||||
expect(result).toEqual({
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
age: { integerValue: 30 },
|
||||
active: { booleanValue: true },
|
||||
metadata: { nullValue: null },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert nested objects', () => {
|
||||
const nestedObj = {
|
||||
user: {
|
||||
profile: {
|
||||
name: 'John',
|
||||
settings: {
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = jsonToDocument(nestedObj);
|
||||
expect(result).toEqual({
|
||||
mapValue: {
|
||||
fields: {
|
||||
user: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
profile: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
settings: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
notifications: { booleanValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(jsonToDocument(0)).toEqual({ integerValue: 0 });
|
||||
expect(jsonToDocument(NaN as any)).toEqual({});
|
||||
expect(jsonToDocument(undefined as any)).toEqual({});
|
||||
});
|
||||
|
||||
it('should skip unsafe object properties', () => {
|
||||
const obj = { safeProperty: 'safe' } as any;
|
||||
Object.defineProperty(obj, '__proto__', {
|
||||
value: 'unsafe',
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
const result = jsonToDocument(obj);
|
||||
expect(result).toEqual({
|
||||
mapValue: {
|
||||
fields: {
|
||||
safeProperty: { stringValue: 'safe' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('documentToJson', () => {
|
||||
it('should handle undefined input', () => {
|
||||
expect(documentToJson(undefined as any)).toEqual({});
|
||||
});
|
||||
|
||||
it('should convert primitive document values', () => {
|
||||
expect(documentToJson({ stringValue: 'hello' })).toBe('hello');
|
||||
expect(documentToJson({ integerValue: 42 })).toBe(42);
|
||||
expect(documentToJson({ doubleValue: 3.14 })).toBe(3.14);
|
||||
expect(documentToJson({ booleanValue: true })).toBe(true);
|
||||
expect(documentToJson({ nullValue: null })).toBe(null);
|
||||
expect(documentToJson({ timestampValue: '2023-01-01T00:00:00.000Z' })).toBe(
|
||||
'2023-01-01T00:00:00.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert map values', () => {
|
||||
const mapValue = {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
age: { integerValue: 30 },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(documentToJson(mapValue)).toEqual({
|
||||
name: 'John',
|
||||
age: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert array values', () => {
|
||||
const arrayValue = {
|
||||
arrayValue: {
|
||||
values: [{ stringValue: 'item1' }, { integerValue: 42 }, { booleanValue: true }],
|
||||
},
|
||||
};
|
||||
expect(documentToJson(arrayValue)).toEqual(['item1', 42, true]);
|
||||
});
|
||||
|
||||
it('should convert nested structures', () => {
|
||||
const nested = {
|
||||
user: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
tags: {
|
||||
arrayValue: {
|
||||
values: [{ stringValue: 'developer' }, { stringValue: 'javascript' }],
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
created: { timestampValue: '2023-01-01T00:00:00.000Z' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = documentToJson(nested);
|
||||
expect(result).toEqual({
|
||||
user: {
|
||||
name: 'John',
|
||||
tags: ['developer', 'javascript'],
|
||||
metadata: {
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty arrays', () => {
|
||||
const emptyArray = { arrayValue: { values: [] } };
|
||||
expect(documentToJson(emptyArray)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle arrays with undefined values', () => {
|
||||
const arrayWithUndefined = { arrayValue: {} };
|
||||
expect(documentToJson(arrayWithUndefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty map values', () => {
|
||||
const emptyMap = { mapValue: { fields: {} } };
|
||||
expect(documentToJson(emptyMap)).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle map values with undefined fields', () => {
|
||||
const mapWithUndefinedFields = { mapValue: {} };
|
||||
expect(documentToJson(mapWithUndefinedFields)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fullDocumentToJson', () => {
|
||||
it('should handle undefined input', () => {
|
||||
expect(fullDocumentToJson(undefined as any)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract metadata and fields', () => {
|
||||
const document = {
|
||||
name: 'projects/test/databases/(default)/documents/users/123',
|
||||
id: 'user123',
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
age: { integerValue: 30 },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
};
|
||||
|
||||
const result = fullDocumentToJson(document);
|
||||
expect(result).toEqual({
|
||||
_name: 'projects/test/databases/(default)/documents/users/123',
|
||||
_id: 'user123',
|
||||
_createTime: '2023-01-01T10:00:00.000Z',
|
||||
_updateTime: '2023-01-01T11:00:00.000Z',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
age: 30,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle document without fields', () => {
|
||||
const document = {
|
||||
name: 'projects/test/databases/(default)/documents/empty/123',
|
||||
id: 'empty123',
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
};
|
||||
|
||||
const result = fullDocumentToJson(document);
|
||||
expect(result).toEqual({
|
||||
_name: 'projects/test/databases/(default)/documents/empty/123',
|
||||
_id: 'empty123',
|
||||
_createTime: '2023-01-01T10:00:00.000Z',
|
||||
_updateTime: '2023-01-01T11:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex nested document', () => {
|
||||
const document = {
|
||||
name: 'projects/test/databases/(default)/documents/complex/123',
|
||||
id: 'complex123',
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
fields: {
|
||||
profile: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
preferences: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
theme: { stringValue: 'dark' },
|
||||
notifications: { booleanValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
arrayValue: {
|
||||
values: [{ stringValue: 'developer' }, { stringValue: 'admin' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = fullDocumentToJson(document);
|
||||
expect(result).toEqual({
|
||||
_name: 'projects/test/databases/(default)/documents/complex/123',
|
||||
_id: 'complex123',
|
||||
_createTime: '2023-01-01T10:00:00.000Z',
|
||||
_updateTime: '2023-01-01T11:00:00.000Z',
|
||||
profile: {
|
||||
name: 'John',
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
tags: ['developer', 'admin'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "CloudFirestore Collection Get All Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "getAll",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-getAll",
|
||||
"name": "Get All Documents",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get All Documents": [
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user1",
|
||||
"_id": "user1",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user2",
|
||||
"_id": "user2",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get All Documents",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock
|
||||
.get('/v1/projects/test-project/databases/(default)/documents/users')
|
||||
.query({ pageSize: 100 })
|
||||
.reply(200, {
|
||||
documents: [
|
||||
{
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user1',
|
||||
id: 'user1',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user2',
|
||||
id: 'user2',
|
||||
fields: {
|
||||
name: { stringValue: 'Jane Smith' },
|
||||
email: { stringValue: 'jane@example.com' },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['collection-getAll.workflow.json'],
|
||||
});
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "delete",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users",
|
||||
"documentId": "test-doc-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-delete",
|
||||
"name": "Delete Document",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Document": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Get Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "get",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users",
|
||||
"documentId": "test-doc-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-get",
|
||||
"name": "Get Document",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Document": [
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/test-doc-id",
|
||||
"_id": "test-doc-id",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"age": "30",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Query Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "query",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"query": "{\"structuredQuery\": {\"where\": {\"fieldFilter\": {\"field\": {\"fieldPath\": \"active\"}, \"op\": \"EQUAL\", \"value\": {\"booleanValue\": true}}}, \"from\": [{\"collectionId\": \"users\"}]}}"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-query",
|
||||
"name": "Query Documents",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Query Documents": [
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user1",
|
||||
"_id": "user1",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user2",
|
||||
"_id": "user2",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Query Documents",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Update Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "upsert",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users",
|
||||
"updateKey": "documentId",
|
||||
"columns": "name,email,age,active"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-update",
|
||||
"name": "Update Document",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Update Document": [
|
||||
{
|
||||
"json": {
|
||||
"status": {
|
||||
"code": 0
|
||||
},
|
||||
"updateTime": "2023-01-01T11:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock
|
||||
.delete('/v1/projects/test-project/databases/(default)/documents/users/test-doc-id')
|
||||
.reply(200, {});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-delete.workflow.json'],
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock.post('/v1/projects/test-project/databases/(default)/documents:batchGet').reply(200, [
|
||||
{
|
||||
found: {
|
||||
name: 'projects/test-project/databases/(default)/documents/users/test-doc-id',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
age: { integerValue: '30' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-get.workflow.json'],
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock.post('/v1/projects/test-project/databases/(default)/documents:runQuery').reply(200, [
|
||||
{
|
||||
document: {
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user1',
|
||||
id: 'user1',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
document: {
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user2',
|
||||
id: 'user2',
|
||||
fields: {
|
||||
name: { stringValue: 'Jane Smith' },
|
||||
email: { stringValue: 'jane@example.com' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-query.workflow.json'],
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock.post('/v1/projects/test-project/databases/(default)/documents:batchWrite').reply(200, {
|
||||
writeResults: [
|
||||
{
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
},
|
||||
],
|
||||
status: [
|
||||
{
|
||||
code: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-update.workflow.json'],
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -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;
|
||||
}
|
||||
+25
@@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+256
@@ -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];
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { googleApiRequest } from '../GenericFunctions';
|
||||
|
||||
describe('GoogleFirebaseRealtimeDatabase > GenericFunctions', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
mockNode = mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'Test RealtimeDatabase Node',
|
||||
type: 'n8n-nodes-base.googleFirebaseRealtimeDatabase',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('googleApiRequest', () => {
|
||||
const mockRequestOAuth2 = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.helpers.requestOAuth2 = mockRequestOAuth2;
|
||||
mockLoadOptionsFunctions.helpers.requestOAuth2 = mockRequestOAuth2;
|
||||
});
|
||||
|
||||
describe('successful requests', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'firebaseio.com',
|
||||
});
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'firebaseio.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should make successful API request with default options', async () => {
|
||||
const mockResponse = { data: { test: 'value' } };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://test-project.firebaseio.com//users.json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
qs: {},
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include body for POST requests', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const requestBody = { name: 'John Doe', email: 'john@example.com' };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'POST',
|
||||
'/users',
|
||||
requestBody,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove empty body for GET requests', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users', {});
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
expect.not.objectContaining({ body: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include query parameters', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
const queryParams = { orderBy: '"$key"', limitToFirst: 10 };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
queryParams,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
qs: queryParams,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include custom headers', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
const customHeaders = { 'X-Custom-Header': 'custom-value' };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
{},
|
||||
customHeaders,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Custom-Header': 'custom-value',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom URI when provided', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
const customUri = 'https://custom-project.firebaseio.com/custom/path.json';
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
customUri,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
url: customUri,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use different regions correctly', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'europe-west1-firebase.cloudfunctions.net',
|
||||
});
|
||||
const mockResponse = { data: [] };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users');
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
url: 'https://test-project.europe-west1-firebase.cloudfunctions.net//users.json',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'firebaseio.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NodeApiError on API failure', async () => {
|
||||
const apiError = new Error('API request failed');
|
||||
mockRequestOAuth2.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockExecuteFunctions.getNode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle credential errors', async () => {
|
||||
const credentialError = new Error('Invalid credentials');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(credentialError);
|
||||
|
||||
await expect(
|
||||
googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should handle missing region in credentials', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({});
|
||||
const mockResponse = { data: [] };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users');
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
url: 'https://test-project.undefined//users.json',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
@@ -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 |
Reference in New Issue
Block a user