diff --git a/package.json b/package.json index 2aa7979b39..56aa53efc0 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "jest-mock": "^29.6.2", "jest-mock-extended": "^3.0.4", "lefthook": "^1.7.15", + "loader": "^2.1.1", "nock": "^13.3.2", "nodemon": "^3.0.1", "npm-run-all2": "^7.0.2", @@ -69,7 +70,7 @@ "tsc-alias": "^1.8.10", "tsc-watch": "^6.2.0", "turbo": "2.3.3", - "typescript": "*", + "typescript": "^5.6.2", "zx": "^8.1.4" }, "pnpm": { diff --git a/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts b/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts new file mode 100644 index 0000000000..f739d61b84 --- /dev/null +++ b/packages/nodes-base/credentials/AzureCosmosDbSharedKeyApi.credentials.ts @@ -0,0 +1,113 @@ +import { + ApplicationError, + type ICredentialDataDecryptedObject, + type ICredentialType, + type IHttpRequestOptions, + type INodeProperties, +} from 'n8n-workflow'; + +import { getAuthorizationTokenUsingMasterKey } from '../nodes/Microsoft/AzureCosmosDB/GenericFunctions'; + +export class AzureCosmosDbSharedKeyApi implements ICredentialType { + name = 'azureCosmosDbSharedKeyApi'; + + displayName = 'Azure Cosmos DB API'; + + documentationUrl = 'azureCosmosDb'; + + properties: INodeProperties[] = [ + { + displayName: 'Database', + name: 'databaseAccount', + description: 'Database account', + type: 'string', + default: '', + }, + { + displayName: 'Key', + name: 'key', + description: 'Account key', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + }, + ]; + + async authenticate( + credentials: ICredentialDataDecryptedObject, + requestOptions: IHttpRequestOptions, + ): Promise { + if (requestOptions.qs) { + for (const [key, value] of Object.entries(requestOptions.qs)) { + if (value === undefined) { + delete requestOptions.qs[key]; + } + } + } + + requestOptions.headers ??= {}; + const date = new Date().toUTCString(); + requestOptions.headers = { + ...requestOptions.headers, + 'x-ms-date': date, + 'x-ms-version': '2020-04-08', + }; + + if (credentials.sessionToken) { + requestOptions.headers['x-ms-session-token'] = credentials.sessionToken; + } + + let resourceType = ''; + let resourceLink = ''; + if (requestOptions.body && typeof requestOptions.body === 'object') { + const isCollectionRequest = 'colls' in requestOptions.body; + const isDocumentRequest = 'docs' in requestOptions.body; + + if (isCollectionRequest) { + resourceType = 'dbs'; + resourceLink = `dbs/${credentials.database}/colls`; + } else if (isDocumentRequest) { + resourceType = 'colls'; + const collId = requestOptions.qs?.collId || ''; + if (!collId) { + throw new ApplicationError('Collection ID (collId) is required for document requests.'); + } + resourceLink = `dbs/${credentials.database}/colls/${collId}/docs`; + } + } else if (requestOptions.qs && typeof requestOptions.qs === 'object') { + const queryType = requestOptions.qs.queryType; + + if (queryType === 'colls') { + resourceType = 'dbs'; + resourceLink = `dbs/${credentials.database}/colls`; + } else if (queryType === 'docs') { + resourceType = 'colls'; + const collId = requestOptions.qs.collId || ''; + if (!collId) { + throw new ApplicationError('Collection ID (collId) is required for document queries.'); + } + resourceLink = `dbs/${credentials.database}/colls/${collId}/docs`; + } + } else { + throw new ApplicationError( + 'Invalid requestOptions: Either body or query string (qs) is required.', + ); + } + + if (requestOptions.method) { + const authToken = getAuthorizationTokenUsingMasterKey( + requestOptions.method, + resourceType, + resourceLink, + date, + credentials.key as string, + ); + + requestOptions.headers.authorization = authToken; + } + + return requestOptions; + } +} diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts new file mode 100644 index 0000000000..752529bbc7 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.ts @@ -0,0 +1,84 @@ +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'; + +export class AzureCosmosDb implements INodeType { + description: INodeTypeDescription = { + displayName: 'Azure Cosmos DB', + name: 'azureCosmosDb', + icon: { + light: 'file:CosmosDB.svg', + dark: 'file:CosmosDB.svg', + }, + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Interact with Azure Cosmos DB API', + defaults: { + name: 'Azure Cosmos Db', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'azureCosmosDbSharedKeyApi', + required: true, + displayOptions: { + show: { + authentication: ['sharedKey'], + }, + }, + }, + ], + requestDefaults: { + baseURL: '=https://{$credentials.databaseAccount}.documents.azure.com', + headers: { + Accept: 'application/json', + }, + }, + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Shared Key', + value: 'sharedKey', + }, + ], + default: 'sharedKey', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Container', + value: 'container', + }, + { + name: 'Item', + value: 'item', + }, + ], + default: 'container', + }, + ...itemFields, + ...itemOperations, + ...containerOperations, + ...containerFields, + ], + }; + + methods = { + listSearch: { + // searchCollections, + // searchDatabases, + }, + }; +} diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts new file mode 100644 index 0000000000..0eae326328 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/GenericFunctions.ts @@ -0,0 +1,212 @@ +import * as crypto from 'crypto'; +import type { + IDataObject, + IHttpRequestOptions, + ILoadOptionsFunctions, + 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', + + //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', + + PREFIX_FOR_STORAGE: 'x-ms-', +}; + +export function getAuthorizationTokenUsingMasterKey( + verb: string, + resourceType: string, + resourceLink: string, + date: string, + masterKey: string, +): string { + const key = Buffer.from(masterKey, 'base64'); + + const payload = + `${verb.toLowerCase()}\n` + + `${resourceType.toLowerCase()}\n` + + `${resourceLink}\n` + + `${date.toLowerCase()}\n` + + '\n'; + + const hmacSha256 = crypto.createHmac('sha256', key); + const hashPayload = hmacSha256.update(payload, 'utf8').digest('base64'); + + const authorizationString = querystring.escape(`type=master&ver=1.0&sig=${hashPayload}`); + + return authorizationString; +} + +export async function azureCosmosDbRequest( + this: ILoadOptionsFunctions, + opts: IHttpRequestOptions, +): Promise { + const credentials = await this.getCredentials('azureCosmosDb'); + const databaseAccount = credentials?.database; + + if (!databaseAccount) { + throw new ApplicationError('Database account not found in credentials!', { level: 'error' }); + } + + const requestOptions: IHttpRequestOptions = { + ...opts, + baseURL: `https://${databaseAccount}.documents.azure.com`, + json: true, + }; + + const errorMapping: Record> = { + 403: { + 'The security token included in the request is invalid.': + 'The Cosmos DB credentials are not valid!', + 'The request signature we calculated does not match the signature you provided': + 'The Cosmos DB credentials are not valid!', + }, + 404: { + 'The specified resource does not exist.': 'The requested resource was not found!', + }, + }; + + try { + return (await this.helpers.requestWithAuthentication.call( + this, + 'azureCosmosDb', + requestOptions, + )) as IDataObject; + } catch (error) { + const statusCode = (error.statusCode || error.cause?.statusCode) as number; + let errorMessage = (error.response?.body?.message || + error.response?.body?.Message || + error.message) as string; + + if (statusCode in errorMapping && errorMessage in errorMapping[statusCode]) { + throw new ApplicationError(errorMapping[statusCode][errorMessage], { + level: 'error', + }); + } + + if (error.cause?.error) { + try { + errorMessage = error.cause?.error?.message as string; + } catch (ex) { + throw new ApplicationError( + `Failed to extract error details: ${ex.message || 'Unknown error'}`, + { level: 'error' }, + ); + } + } + + throw new ApplicationError(`Cosmos DB error response [${statusCode}]: ${errorMessage}`, { + level: 'error', + }); + } +} + +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; + + if (!databaseAccount) { + throw new ApplicationError('Database account not found in credentials!', { level: 'error' }); + } + + const opts: IHttpRequestOptions = { + method: 'GET', + url: `/dbs/${dbId}/colls`, + baseURL: `https://${databaseAccount}.documents.azure.com`, + headers: { + 'Content-Type': 'application/json', + }, + }; + + const responseData: IDataObject = await azureCosmosDbRequest.call(this, opts); + + const responseBody = responseData as { + Collections: IDataObject[]; + }; + const collections = responseBody.Collections; + + if (!collections) { + return { results: [] }; + } + + const results: INodeListSearchItems[] = collections + .map((collection) => ({ + name: String(collection.id), + value: String(collection.id), + })) + .filter((collection) => !filter || collection.name.includes(filter)) + .sort((a, b) => a.name.localeCompare(b.name)); + + return { + results, + }; +} + +export async function searchDatabases( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const opts: IHttpRequestOptions = { + method: 'GET', + url: '/dbs', + headers: { + 'Content-Type': 'application/json', + }, + }; + + const responseData: IDataObject = await azureCosmosDbRequest.call(this, opts); + + const responseBody = responseData as { + Databases: IDataObject[]; + }; + const databases = responseBody.Databases; + + if (!databases) { + return { results: [] }; + } + + const results: INodeListSearchItems[] = databases + .map((database) => ({ + name: String(database.id), + value: String(database.id), + })) + .filter((database) => !filter || database.name.includes(filter)) + .sort((a, b) => a.name.localeCompare(b.name)); + + return { + results, + }; +} diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts new file mode 100644 index 0000000000..bf651f7e89 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ContainerDescription.ts @@ -0,0 +1,429 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const containerOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['container'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a container', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'POST', + url: '=/dbs/{{ $parameter["dbId"] }}/colls', + }, + }, + action: 'Create container', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a container', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'DELETE', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}', + }, + }, + action: 'Delete container', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a container', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'GET', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}', + }, + }, + action: 'Get item', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve a list of containers', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'GET', + url: '=/dbs/{{ $parameter["dbId"] }}/colls', + }, + }, + action: 'Get many containers', + }, + ], + default: 'getAll', + }, +]; + +export const createFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['container'], + operation: ['create'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + placeholder: 'e.g. AndersenFamily', + description: "Container's ID", + required: true, + displayOptions: { + show: { + resource: ['container'], + operation: ['create'], + }, + }, + routing: { + send: { + type: 'body', + property: 'id', + value: '={{$value}}', + }, + }, + }, + { + displayName: 'Partition Key', + name: 'partitionKey', + type: 'json', + default: '{}', + placeholder: '"paths": ["/AccountNumber"],"kind": "Hash", "Version": 2', + description: 'User-defined JSON object representing the partition key', + required: true, + displayOptions: { + show: { + resource: ['container'], + operation: ['create'], + }, + }, + routing: { + send: { + type: 'body', + property: 'partitionKey', + value: '={{$value}}', + }, + }, + }, + { + displayName: 'Additional Keys', + name: 'additionalKeys', + type: 'fixedCollection', + default: {}, + placeholder: '"paths": ["/AccountNumber"],"kind": "Hash", "Version": 2', + description: 'User-defined JSON object representing the document properties', + displayOptions: { + show: { + resource: ['container'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Indexing Policy', + name: 'indexingPolicy', + type: 'json', + default: '{}', + placeholder: + '"automatic": true, "indexingMode": "Consistent", "includedPaths": [{ "path": "/*", "indexes": [{ "dataType": "String", "precision": -1, "kind": "Range" }]}]', + description: 'This value is used to configure indexing policy', + routing: { + send: { + type: 'body', + property: 'indexingPolicy', + value: '={{$value}}', + }, + }, + }, + ], + }, +]; + +export const getFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['container'], + operation: ['get'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Container ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the container you want to retrieve', + displayOptions: { + show: { + resource: ['container'], + operation: ['get'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'containerId', + type: 'string', + hint: 'Enter the container ID', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The container ID must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. AndersenFamily', + }, + ], + }, +]; + +export const getAllFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['container'], + operation: ['getAll'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, +]; + +export const deleteFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['container'], + operation: ['delete'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Container ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the container you want to delete', + displayOptions: { + show: { + resource: ['container'], + operation: ['delete'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'containerId', + type: 'string', + hint: 'Enter the container ID', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The container ID must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. AndersenFamily', + }, + ], + }, +]; + +export const containerFields: INodeProperties[] = [ + ...createFields, + ...deleteFields, + ...getFields, + ...getAllFields, +]; diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts new file mode 100644 index 0000000000..b794e145f5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDB/descriptions/ItemDescription.ts @@ -0,0 +1,807 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const itemOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['item'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an item', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'POST', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item', + }, + }, + action: 'Create item', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an item', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'DELETE', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item/{{ $parameter["id"] }}', + }, + }, + action: 'Delete item', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an item', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'GET', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item/{{ $parameter["id"] }}', + }, + }, + action: 'Get item', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve a list of items', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'GET', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/docs', + }, + }, + action: 'Get many items', + }, + { + name: 'Query', + value: 'query', + description: 'Query items', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'POST', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item', + }, + }, + action: 'Query items', + }, + { + name: 'Update', + value: 'update', + description: 'Update an item', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'PATCH', + url: '=/dbs/{{ $parameter["dbId"] }}/colls/{{ $parameter["collId"] }}/item/{{ $parameter["id"] }}', + }, + }, + action: 'Create item', + }, + ], + default: 'getAll', + }, +]; + +export const createFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['create'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Collection ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the collection you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['create'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'collectionName', + type: 'string', + hint: 'Enter the collection name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The collection name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersCollection', + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + placeholder: 'e.g. AndersenFamily', + description: "Item's ID", + required: true, + displayOptions: { + show: { + resource: ['item'], + operation: ['create'], + }, + }, + routing: { + send: { + type: 'body', + property: 'id', + value: '={{$value}}', + }, + }, + }, + { + displayName: 'Custom Properties', + name: 'customProperties', + type: 'json', + default: '{}', + placeholder: '{ "LastName": "Andersen", "Address": { "State": "WA", "City": "Seattle" } }', + description: 'User-defined JSON object representing the document properties', + required: true, + displayOptions: { + show: { + resource: ['item'], + operation: ['create'], + }, + }, + routing: { + send: { + type: 'body', + property: '', + value: '={{$value}}', + }, + }, + }, +]; + +export const deleteFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['delete'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Collection ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the collection you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['delete'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'collectionName', + type: 'string', + hint: 'Enter the collection name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The collection name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersCollection', + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + placeholder: 'e.g. AndersenFamily', + description: 'Unique ID for the item', + required: true, + displayOptions: { + show: { + resource: ['item'], + operation: ['delete'], + }, + }, + }, +]; + +export const getFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['get'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Collection ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the collection you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['get'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'collectionName', + type: 'string', + hint: 'Enter the collection name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The collection name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersCollection', + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + placeholder: 'e.g. AndersenFamily', + description: "Item's ID", + required: true, + displayOptions: { + show: { + resource: ['item'], + operation: ['get'], + }, + }, + routing: { + send: { + type: 'body', + property: 'id', + value: '={{$value}}', + }, + }, + }, +]; + +export const getAllFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['getAll'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Collection ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the collection you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['getAll'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'collectionName', + type: 'string', + hint: 'Enter the collection name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The collection name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersCollection', + }, + ], + }, +]; + +//TO-DO-check-fields +export const queryFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['query'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Collection ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the collection you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['query'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'collectionName', + type: 'string', + hint: 'Enter the collection name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The collection name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersCollection', + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + placeholder: 'e.g. AndersenFamily', + description: "Item's ID", + required: true, + displayOptions: { + show: { + resource: ['item'], + operation: ['query'], + }, + }, + routing: { + send: { + type: 'body', + property: 'id', + value: '={{$value}}', + }, + }, + }, +]; + +export const updateFields: INodeProperties[] = [ + { + displayName: 'Database ID', + name: 'dbId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the database you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['update'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDatabases', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'databaseName', + type: 'string', + hint: 'Enter the database name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The database name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersDB', + }, + ], + }, + { + displayName: 'Collection ID', + name: 'collId', + type: 'resourceLocator', + required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the collection you want to use', + displayOptions: { + show: { + resource: ['item'], + operation: ['update'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchCollections', + searchable: true, + }, + }, + { + displayName: 'By Name', + name: 'collectionName', + type: 'string', + hint: 'Enter the collection name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The collection name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. UsersCollection', + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + placeholder: 'e.g. AndersenFamily', + description: 'Unique ID for the document', + required: true, + displayOptions: { + show: { + resource: ['item'], + operation: ['update'], + }, + }, + }, + //TO-DO-check-this + { + displayName: 'Operations', + name: 'operations', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + required: true, + typeOptions: { + resourceMapper: { + resourceMapperMethod: 'getMappingColumns', + mode: 'update', + fieldWords: { + singular: 'operation', + plural: 'operations', + }, + addAllFields: true, + multiKeyMatch: false, + supportAutoMap: true, + matchingFieldsLabels: { + title: 'Custom Matching Operations', + description: 'Define the operations to perform, such as "set", "delete", or "add".', + hint: 'Map input data to the expected structure of the operations array.', + }, + }, + }, + description: 'Define the operations to perform, such as setting or updating document fields', + displayOptions: { + show: { + resource: ['item'], + operation: ['update'], + }, + }, + //TO-DO-presend-function + }, +]; + +export const itemFields: INodeProperties[] = [ + ...createFields, + ...deleteFields, + ...getFields, + ...getAllFields, + ...queryFields, + ...updateFields, +]; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 16d9e05d0d..6978635c64 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "1.74.0", + "version": "1.75.0", "description": "Base nodes of n8n", "main": "index.js", "scripts": { @@ -39,6 +39,7 @@ "dist/credentials/AutomizyApi.credentials.js", "dist/credentials/AutopilotApi.credentials.js", "dist/credentials/Aws.credentials.js", + "dist/credentials/AzureCosmosDbSharedKeyApi.credentials.js", "dist/credentials/BambooHrApi.credentials.js", "dist/credentials/BannerbearApi.credentials.js", "dist/credentials/BaserowApi.credentials.js", @@ -625,6 +626,7 @@ "dist/nodes/Merge/Merge.node.js", "dist/nodes/MessageBird/MessageBird.node.js", "dist/nodes/Metabase/Metabase.node.js", + "dist/nodes/Microsoft/AzureCosmosDB/AzureCosmosDb.node.js", "dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js", "dist/nodes/Microsoft/Entra/MicrosoftEntra.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 679b1155d2..8931279c42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,6 +180,9 @@ importers: lefthook: specifier: ^1.7.15 version: 1.7.15 + loader: + specifier: ^2.1.1 + version: 2.1.1 nock: specifier: ^13.3.2 version: 13.3.2 @@ -440,7 +443,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) + version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -467,7 +470,7 @@ importers: version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.15 - version: 0.3.15(vc5hvyy27o4cmm4jplsptc2fqm) + version: 0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe) '@langchain/core': specifier: 'catalog:' version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -554,7 +557,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.6 - version: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) + version: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) lodash: specifier: 'catalog:' version: 4.17.21 @@ -9734,6 +9737,9 @@ packages: enquirer: optional: true + loader@2.1.1: + resolution: {integrity: sha512-Z6nHbyKiECMexVUmpIAVXp+f+8okRtfiMAQ9j0eTtImCghxSqQwrbeSiAyCYKBnfvRlgXLdQt0tPuaZmcBQzpw==} + local-pkg@0.5.0: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} @@ -9788,6 +9794,7 @@ packages: lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} @@ -15764,7 +15771,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -15773,7 +15780,7 @@ snapshots: zod: 3.23.8 optionalDependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) - langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) + langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) transitivePeerDependencies: - encoding @@ -16237,7 +16244,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)': + '@langchain/community@0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)': dependencies: '@ibm-cloud/watsonx-ai': 1.1.2 '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -16247,7 +16254,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) + langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) uuid: 10.0.0 zod: 3.23.8 @@ -16260,7 +16267,7 @@ snapshots: '@aws-sdk/client-s3': 3.666.0 '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -19553,14 +19560,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.4(debug@4.3.7): - dependencies: - follow-redirects: 1.15.6(debug@4.3.7) - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.7.7: dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21284,7 +21283,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21309,7 +21308,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21329,7 +21328,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -22108,7 +22107,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22414,7 +22413,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 18.16.16 '@types/tough-cookie': 4.0.2 - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 camelcase: 6.3.0 debug: 4.3.7 dotenv: 16.4.5 @@ -22424,7 +22423,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.7.4) + retry-axios: 2.6.0(axios@1.7.4(debug@4.3.7)) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -23429,7 +23428,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu): + langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i): dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) @@ -23594,6 +23593,8 @@ snapshots: optionalDependencies: enquirer: 2.3.6 + loader@2.1.1: {} + local-pkg@0.5.0: dependencies: mlly: 1.4.2 @@ -24999,7 +25000,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25821,7 +25822,7 @@ snapshots: ret@0.1.15: {} - retry-axios@2.6.0(axios@1.7.4): + retry-axios@2.6.0(axios@1.7.4(debug@4.3.7)): dependencies: axios: 1.7.4 @@ -25848,7 +25849,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color