From 6307ac162f1ea015e2f654cf909c48b13709fbb4 Mon Sep 17 00:00:00 2001 From: Adina Totorean Date: Sun, 26 Jan 2025 15:28:04 +0200 Subject: [PATCH] Worked on pagination --- .../AzureCosmosDbSharedKeyApi.credentials.ts | 6 +- .../AzureCosmosDB/AzureCosmosDb.node.ts | 11 +- .../Microsoft/AzureCosmosDB/CosmosDB.svg | 1 + .../AzureCosmosDB/GenericFunctions.ts | 124 +++++++++++----- .../descriptions/ContainerDescription.ts | 11 +- .../descriptions/ItemDescription.ts | 133 +++++++++++++++--- 6 files changed, 223 insertions(+), 63 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/AzureCosmosDB/CosmosDB.svg diff --git a/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts b/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts index f739d61b84..499779c20c 100644 --- a/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts +++ b/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts @@ -17,9 +17,9 @@ export class AzureCosmosDbSharedKeyApi implements ICredentialType { properties: INodeProperties[] = [ { - displayName: 'Database', - name: 'databaseAccount', - description: 'Database account', + displayName: 'Account', + name: 'account', + description: 'Account name', type: 'string', default: '', }, diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts index 752529bbc7..0373f08d30 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts @@ -2,7 +2,8 @@ import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; import { containerFields, containerOperations } from './descriptions/ContainerDescription'; -import { itemFields, itemOperations } from '../../Aws/DynamoDB/ItemDescription'; +import { itemFields, itemOperations } from './descriptions/ItemDescription'; +import { searchCollections, searchDatabases } from './GenericFunctions'; export class AzureCosmosDb implements INodeType { description: INodeTypeDescription = { @@ -33,7 +34,7 @@ export class AzureCosmosDb implements INodeType { }, ], requestDefaults: { - baseURL: '=https://{$credentials.databaseAccount}.documents.azure.com', + baseURL: '=https://{$credentials.account}.documents.azure.com', headers: { Accept: 'application/json', }, @@ -68,8 +69,8 @@ export class AzureCosmosDb implements INodeType { ], default: 'container', }, - ...itemFields, ...itemOperations, + ...itemFields, ...containerOperations, ...containerFields, ], @@ -77,8 +78,8 @@ export class AzureCosmosDb implements INodeType { methods = { listSearch: { - // searchCollections, - // searchDatabases, + searchCollections, + searchDatabases, }, }; } diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/CosmosDB.svg b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/CosmosDB.svg new file mode 100644 index 0000000000..c4f1f8cabe --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/CosmosDB.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts index 0eae326328..51e3c1b187 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts @@ -1,41 +1,45 @@ import * as crypto from 'crypto'; import type { + DeclarativeRestApiSettings, IDataObject, + IExecutePaginationFunctions, IHttpRequestOptions, ILoadOptionsFunctions, + INodeExecutionData, INodeListSearchItems, INodeListSearchResult, } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow'; import * as querystring from 'querystring'; -export const HeaderConstants = { - // Required - AUTHORIZATION: 'Authorization', - CONTENT_TYPE: 'Content-Type', - X_MS_DATE: 'x-ms-date', - X_MS_VERSION: 'x-ms-version', +// export const HeaderConstants = { +// // Required +// AUTHORIZATION: 'Authorization', +// CONTENT_TYPE: 'Content-Type', +// X_MS_DATE: 'x-ms-date', +// X_MS_VERSION: 'x-ms-version', - //Required - for session consistency only - X_MS_SESSION_TOKEN: 'x-ms-session-token', +// //Required - for session consistency only +// X_MS_SESSION_TOKEN: 'x-ms-session-token', - // Optional - IF_MATCH: 'If-Match', - IF_NONE_MATCH: 'If-None-Match', - IF_MODIFIED_SINCE: 'If-Modified-Since', - USER_AGENT: 'User-Agent', - X_MS_ACTIVITY_ID: 'x-ms-activity-id', - X_MS_CONSISTENCY_LEVEL: 'x-ms-consistency-level', - X_MS_CONTINUATION: 'x-ms-continuation', - X_MS_MAX_ITEM_COUNT: 'x-ms-max-item-count', - X_MS_DOCUMENTDB_PARTITIONKEY: 'x-ms-documentdb-partitionkey', - X_MS_DOCUMENTDB_QUERY_ENABLECROSSPARTITION: 'x-ms-documentdb-query-enablecrosspartition', - A_IM: 'A-IM', - X_MS_DOCUMENTDB_PARTITIONKEYRANGEID: 'x-ms-documentdb-partitionkeyrangeid', - X_MS_COSMOS_ALLOW_TENTATIVE_WRITES: 'x-ms-cosmos-allow-tentative-writes', +// // Optional +// IF_MATCH: 'If-Match', +// IF_NONE_MATCH: 'If-None-Match', +// IF_MODIFIED_SINCE: 'If-Modified-Since', +// USER_AGENT: 'User-Agent', +// X_MS_ACTIVITY_ID: 'x-ms-activity-id', +// X_MS_CONSISTENCY_LEVEL: 'x-ms-consistency-level', +// X_MS_CONTINUATION: 'x-ms-continuation', +// X_MS_MAX_ITEM_COUNT: 'x-ms-max-item-count', +// X_MS_DOCUMENTDB_PARTITIONKEY: 'x-ms-documentdb-partitionkey', +// X_MS_DOCUMENTDB_ISQUERY: 'x-ms-documentdb-isquery', +// X_MS_DOCUMENTDB_QUERY_ENABLECROSSPARTITION: 'x-ms-documentdb-query-enablecrosspartition', +// A_IM: 'A-IM', +// X_MS_DOCUMENTDB_PARTITIONKEYRANGEID: 'x-ms-documentdb-partitionkeyrangeid', +// X_MS_COSMOS_ALLOW_TENTATIVE_WRITES: 'x-ms-cosmos-allow-tentative-writes', - PREFIX_FOR_STORAGE: 'x-ms-', -}; +// PREFIX_FOR_STORAGE: 'x-ms-', +// }; export function getAuthorizationTokenUsingMasterKey( verb: string, @@ -61,12 +65,67 @@ export function getAuthorizationTokenUsingMasterKey( return authorizationString; } +export async function handlePagination( + this: IExecutePaginationFunctions, + resultOptions: DeclarativeRestApiSettings.ResultOptions, +): Promise { + const aggregatedResult: IDataObject[] = []; + let nextPageToken: string | undefined; + const returnAll = this.getNodeParameter('returnAll') as boolean; + let limit = 60; + + if (!returnAll) { + limit = this.getNodeParameter('limit') as number; + resultOptions.maxResults = limit; + } + + resultOptions.paginate = true; + + do { + if (nextPageToken) { + resultOptions.options.headers = resultOptions.options.headers ?? {}; + resultOptions.options.headers['x-ms-continuation'] = nextPageToken; + } + + const responseData = await this.makeRoutingRequest(resultOptions); + + if (Array.isArray(responseData)) { + for (const responsePage of responseData) { + aggregatedResult.push(responsePage); + + if (!returnAll && aggregatedResult.length >= limit) { + return aggregatedResult.slice(0, limit).map((result) => ({ json: result })); + } + } + } + + //TO-DO-check-if-works + if (responseData.length > 0) { + const lastItem = responseData[responseData.length - 1]; + + if ('headers' in lastItem) { + const headers = (lastItem as unknown as { headers: { [key: string]: string } }).headers; + + if (headers) { + nextPageToken = headers['x-ms-continuation'] as string | undefined; + } + } + } + + if (!nextPageToken) { + break; + } + } while (nextPageToken); + + return aggregatedResult.map((result) => ({ json: result })); +} + export async function azureCosmosDbRequest( this: ILoadOptionsFunctions, opts: IHttpRequestOptions, ): Promise { - const credentials = await this.getCredentials('azureCosmosDb'); - const databaseAccount = credentials?.database; + const credentials = await this.getCredentials('azureCosmosDbSharedKeyApi'); + const databaseAccount = credentials?.account; if (!databaseAccount) { throw new ApplicationError('Database account not found in credentials!', { level: 'error' }); @@ -93,7 +152,7 @@ export async function azureCosmosDbRequest( try { return (await this.helpers.requestWithAuthentication.call( this, - 'azureCosmosDb', + 'azureCosmosDbSharedKeyApi', requestOptions, )) as IDataObject; } catch (error) { @@ -128,18 +187,17 @@ export async function azureCosmosDbRequest( export async function searchCollections( this: ILoadOptionsFunctions, filter?: string, - paginationToken?: string, ): Promise { const dbId = this.getNodeParameter('dbId') as string; if (!dbId) { throw new ApplicationError('Database ID is required'); } - const credentials = await this.getCredentials('azureCosmosDb'); - const databaseAccount = credentials?.databaseAccount; + const credentials = await this.getCredentials('azureCosmosDbSharedKeyApi'); + const databaseAccount = credentials?.account; if (!databaseAccount) { - throw new ApplicationError('Database account not found in credentials!', { level: 'error' }); + throw new ApplicationError('Account name not found in credentials!', { level: 'error' }); } const opts: IHttpRequestOptions = { @@ -154,9 +212,9 @@ export async function searchCollections( const responseData: IDataObject = await azureCosmosDbRequest.call(this, opts); const responseBody = responseData as { - Collections: IDataObject[]; + DocumentCollections: IDataObject[]; }; - const collections = responseBody.Collections; + const collections = responseBody.DocumentCollections; if (!collections) { return { results: [] }; diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts index bf651f7e89..317acc1275 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts @@ -49,7 +49,7 @@ export const containerOperations: INodeProperties[] = [ url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}', }, }, - action: 'Get item', + action: 'Get container', }, { name: 'Get Many', @@ -159,12 +159,9 @@ export const createFields: INodeProperties[] = [ }, }, { - displayName: 'Additional Keys', - name: 'additionalKeys', - type: 'fixedCollection', + displayName: 'Additional Fields', + name: 'additionalFields', default: {}, - placeholder: '"paths": ["/AccountNumber"],"kind": "Hash", "Version": 2', - description: 'User-defined JSON object representing the document properties', displayOptions: { show: { resource: ['container'], @@ -189,6 +186,8 @@ export const createFields: INodeProperties[] = [ }, }, ], + placeholder: 'Add Option', + type: 'collection', }, ]; diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts index b794e145f5..4b08354a38 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts @@ -1,4 +1,6 @@ -import type { INodeProperties } from 'n8n-workflow'; +import type { IExecuteSingleFunctions, IHttpRequestOptions, INodeProperties } from 'n8n-workflow'; + +import { handlePagination } from '../GenericFunctions'; export const itemOperations: INodeProperties[] = [ { @@ -15,12 +17,12 @@ export const itemOperations: INodeProperties[] = [ { name: 'Create', value: 'create', - description: 'Create an item', + description: 'Create a new item', routing: { request: { ignoreHttpStatusErrors: true, method: 'POST', - url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/docs', }, }, action: 'Create item', @@ -28,12 +30,12 @@ export const itemOperations: INodeProperties[] = [ { name: 'Delete', value: 'delete', - description: 'Delete an item', + description: 'Delete an existing item', routing: { request: { ignoreHttpStatusErrors: true, method: 'DELETE', - url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item/{{ $parameter["id"] }}', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', }, }, action: 'Delete item', @@ -46,7 +48,7 @@ export const itemOperations: INodeProperties[] = [ request: { ignoreHttpStatusErrors: true, method: 'GET', - url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item/{{ $parameter["id"] }}', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', }, }, action: 'Get item', @@ -56,6 +58,12 @@ export const itemOperations: INodeProperties[] = [ value: 'getAll', description: 'Retrieve a list of items', routing: { + send: { + paginate: true, + }, + operations: { + pagination: handlePagination, + }, request: { ignoreHttpStatusErrors: true, method: 'GET', @@ -72,7 +80,11 @@ export const itemOperations: INodeProperties[] = [ request: { ignoreHttpStatusErrors: true, method: 'POST', - url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/docs', + headers: { + 'Content-Type': 'application/query+json', + 'x-ms-documentdb-isquery': 'True', + }, }, }, action: 'Query items', @@ -80,15 +92,15 @@ export const itemOperations: INodeProperties[] = [ { name: 'Update', value: 'update', - description: 'Update an item', + description: 'Update an existing item', routing: { request: { ignoreHttpStatusErrors: true, method: 'PATCH', - url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item/{{ $parameter["id"] }}', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', }, }, - action: 'Create item', + action: 'Update item', }, ], default: 'getAll', @@ -538,9 +550,58 @@ export const getAllFields: INodeProperties[] = [ }, ], }, + { + displayName: 'Return All', + name: 'returnAll', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: ['item'], + operation: ['getAll'], + }, + }, + routing: { + send: { + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + return requestOptions; + }, + ], + }, + }, + type: 'boolean', + }, + { + displayName: 'Limit', + name: 'limit', + default: 50, + description: 'Max number of results to return', + displayOptions: { + show: { + resource: ['item'], + operation: ['getAll'], + returnAll: [false], + }, + }, + routing: { + send: { + property: 'x-ms-max-item-count', + type: 'query', + value: '={{ $value }}', + }, + }, + type: 'number', + typeOptions: { + minValue: 1, + }, + validateType: 'number', + }, ]; -//TO-DO-check-fields export const queryFields: INodeProperties[] = [ { displayName: 'Database ID', @@ -631,23 +692,63 @@ export const queryFields: INodeProperties[] = [ ], }, { - displayName: 'ID', - name: 'id', + displayName: 'Query', + name: 'query', type: 'string', default: '', - placeholder: 'e.g. AndersenFamily', - description: "Item's ID", required: true, + description: 'The SQL query text to execute', displayOptions: { show: { resource: ['item'], operation: ['query'], }, }, + placeholder: 'SELECT * FROM c WHERE c.name = @name', routing: { send: { type: 'body', - property: 'id', + property: 'query', + value: '={{$value}}', + }, + }, + }, + { + displayName: 'Parameters', + name: 'parameters', + type: 'fixedCollection', + required: true, + default: [], + placeholder: 'Add Parameter', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'parameters', + displayName: 'Parameter', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g., @name', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'e.g., John', + }, + ], + }, + ], + routing: { + send: { + type: 'body', + property: 'parameters', value: '={{$value}}', }, },