Worked on 'update' action

This commit is contained in:
Adina Totorean 2025-02-06 13:29:03 +02:00
parent 6f0c98289b
commit 5780596cb1
3 changed files with 273 additions and 40 deletions

View file

@ -3,7 +3,7 @@ import { NodeConnectionType } from 'n8n-workflow';
import { containerFields, containerOperations } from './descriptions/ContainerDescription';
import { itemFields, itemOperations } from './descriptions/ItemDescription';
import { searchCollections, searchItems } from './GenericFunctions';
import { getDynamicFields, searchCollections, searchItems } from './GenericFunctions';
export class CosmosDb implements INodeType {
description: INodeTypeDescription = {
@ -64,6 +64,7 @@ export class CosmosDb implements INodeType {
listSearch: {
searchCollections,
searchItems,
getDynamicFields,
},
};
}

View file

@ -118,6 +118,41 @@ export async function handlePagination(
return aggregatedResult.map((result) => ({ json: result }));
}
export async function handleErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
const responseBody = response.body as IDataObject;
let errorMessage = 'Unknown error occurred';
if (typeof responseBody.message === 'string') {
try {
const jsonMatch = responseBody.message.match(/Message: (\{.*\})/);
if (jsonMatch && jsonMatch[1]) {
const parsedMessage = JSON.parse(jsonMatch[1]);
if (
parsedMessage.Errors &&
Array.isArray(parsedMessage.Errors) &&
parsedMessage.Errors.length > 0
) {
errorMessage = parsedMessage.Errors[0].split(' Learn more:')[0].trim();
}
}
} catch (error) {
errorMessage = 'Failed to extract error message';
}
}
throw new ApplicationError(errorMessage);
}
return data;
}
export async function microsoftCosmosDbRequest(
this: ILoadOptionsFunctions,
opts: IHttpRequestOptions,
@ -133,6 +168,7 @@ export async function microsoftCosmosDbRequest(
...opts,
baseURL: `${credentials.baseUrl}`,
headers: {
...opts.headers,
Accept: 'application/json',
'Content-Type': 'application/json',
},
@ -298,35 +334,69 @@ export async function validateOperations(
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const rawOperations = this.getNodeParameter('operations', []) as IDataObject;
console.log('Operations', rawOperations);
if (!rawOperations || !Array.isArray(rawOperations.operations)) {
throw new ApplicationError('The "operations" field must contain at least one operation.');
}
const operations = rawOperations.operations as Array<{
op: string;
path: string;
value?: string;
path?: { mode: string; value: string };
toPath?: { mode: string; value: string };
from?: { mode: string; value: string };
value?: string | number;
}>;
for (const operation of operations) {
if (!['add', 'increment', 'move', 'remove', 'replace', 'set'].includes(operation.op)) {
throw new ApplicationError(
`Invalid operation type "${operation.op}". Allowed values are "add", "increment", "move", "remove", "replace", and "set".`,
);
}
if (!operation.path || operation.path.trim() === '') {
const transformedOperations = operations.map((operation) => {
if (
operation.op !== 'move' &&
(!operation.path?.value ||
typeof operation.path.value !== 'string' ||
operation.path.value.trim() === '')
) {
throw new ApplicationError('Each operation must have a valid "path".');
}
if (
['set', 'replace', 'add', 'increment'].includes(operation.op) &&
['set', 'replace', 'add', 'incr'].includes(operation.op) &&
(operation.value === undefined || operation.value === null)
) {
throw new ApplicationError(`The operation "${operation.op}" must include a valid "value".`);
throw new ApplicationError(`The "${operation.op}" operation must include a valid "value".`);
}
}
if (operation.op === 'move') {
if (
!operation.from?.value ||
typeof operation.from.value !== 'string' ||
operation.from.value.trim() === ''
) {
throw new ApplicationError('The "move" operation must have a valid "from" path.');
}
if (
!operation.toPath?.value ||
typeof operation.toPath.value !== 'string' ||
operation.toPath.value.trim() === ''
) {
throw new ApplicationError('The "move" operation must have a valid "toPath".');
}
}
if (operation.op === 'incr' && isNaN(Number(operation.value))) {
throw new ApplicationError('The "increment" operation must have a numeric value.');
}
return {
op: operation.op,
path: operation.op === 'move' ? operation.toPath?.value : operation.path?.value,
...(operation.from ? { from: operation.from.value } : {}),
...(operation.op === 'incr'
? { value: Number(operation.value) }
: { value: isNaN(Number(operation.value)) ? operation.value : Number(operation.value) }),
};
});
requestOptions.body = transformedOperations;
return requestOptions;
}
@ -488,3 +558,85 @@ export async function processResponseContainers(
return [];
}
function extractFieldPaths(obj: any, prefix = ''): string[] {
let paths: string[] = [];
Object.entries(obj).forEach(([key, value]) => {
if (key.startsWith('_') || key === 'id') {
return;
}
const newPath = prefix ? `${prefix}/${key}` : `/${key}`;
if (Array.isArray(value) && value.length > 0) {
value.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
paths = paths.concat(extractFieldPaths(item, `${newPath}/${index}`));
} else {
paths.push(`${newPath}/${index}`);
}
});
} else if (typeof value === 'object' && value !== null) {
paths = paths.concat(extractFieldPaths(value, newPath));
} else {
paths.push(newPath);
}
});
return paths;
}
export async function searchItemById(
this: ILoadOptionsFunctions,
itemId: string,
): Promise<IDataObject | null> {
const collection = this.getNodeParameter('collId') as { mode: string; value: string };
if (!collection?.value) {
throw new ApplicationError('Collection ID is required.');
}
if (!itemId) {
throw new ApplicationError('Item ID is required.');
}
const opts: IHttpRequestOptions = {
method: 'GET',
url: `/colls/${collection.value}/docs/${itemId}`,
headers: {
'x-ms-documentdb-partitionkey': `["${itemId}"]`,
},
};
const responseData: IDataObject = await microsoftCosmosDbRequest.call(this, opts);
if (!responseData) {
return null;
}
return responseData;
}
export async function getDynamicFields(
this: ILoadOptionsFunctions,
): Promise<INodeListSearchResult> {
const itemId = this.getNodeParameter('id', '') as { mode: string; value: string };
if (!itemId) {
throw new ApplicationError('Item ID is required to fetch fields.');
}
const itemData = await searchItemById.call(this, itemId.value);
if (!itemData) {
throw new ApplicationError(`Item with ID "${itemId.value}" not found.`);
}
const fieldPaths = extractFieldPaths(itemData);
return {
results: fieldPaths.map((path) => ({
name: path,
value: path,
})),
};
}

View file

@ -2,6 +2,7 @@ import type { INodeProperties } from 'n8n-workflow';
import {
formatCustomProperties,
handleErrorPostReceive,
handlePagination,
processResponseItems,
validateOperations,
@ -145,6 +146,9 @@ export const itemOperations: INodeProperties[] = [
'x-ms-documentdb-partitionkey': '=["{{$parameter["id"]}}"]',
},
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Update item',
},
@ -155,7 +159,7 @@ export const itemOperations: INodeProperties[] = [
export const createFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -184,7 +188,7 @@ export const createFields: INodeProperties[] = [
displayName: 'By ID',
name: 'containerId',
type: 'string',
hint: 'Enter the container ID',
hint: 'Enter the container Id',
validation: [
{
type: 'regex',
@ -239,7 +243,7 @@ export const createFields: INodeProperties[] = [
export const deleteFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -274,7 +278,7 @@ export const deleteFields: INodeProperties[] = [
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The container name must follow the allowed pattern.',
errorMessage: 'The container id must follow the allowed pattern.',
},
},
],
@ -330,7 +334,7 @@ export const deleteFields: INodeProperties[] = [
export const getFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -365,7 +369,7 @@ export const getFields: INodeProperties[] = [
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The container name must follow the allowed pattern.',
errorMessage: 'The container id must follow the allowed pattern.',
},
},
],
@ -421,7 +425,7 @@ export const getFields: INodeProperties[] = [
export const getAllFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -456,7 +460,7 @@ export const getAllFields: INodeProperties[] = [
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The container name must follow the allowed pattern.',
errorMessage: 'The container id must follow the allowed pattern.',
},
},
],
@ -506,7 +510,7 @@ export const getAllFields: INodeProperties[] = [
export const queryFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -621,7 +625,7 @@ export const queryFields: INodeProperties[] = [
export const updateFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -656,7 +660,7 @@ export const updateFields: INodeProperties[] = [
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The container name must follow the allowed pattern.',
errorMessage: 'The container id must follow the allowed pattern.',
},
},
],
@ -736,29 +740,112 @@ export const updateFields: INodeProperties[] = [
type: 'options',
options: [
{ name: 'Add', value: 'add' },
{ name: 'Increment', value: 'increment' },
{ name: 'Increment', value: 'incr' },
{ name: 'Move', value: 'move' },
{ name: 'Remove', value: 'remove' },
{ name: 'Replace', value: 'replace' },
{ name: 'Set', value: 'set' },
],
default: 'set',
},
{
displayName: 'From',
displayName: 'From Path',
name: 'from',
type: 'string',
default: '',
type: 'resourceLocator',
description: 'Select a field from the list or enter it manually',
displayOptions: {
show: {
op: ['move'],
},
},
default: {
mode: 'list',
value: '',
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getDynamicFields',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'manual',
type: 'string',
hint: 'Enter the field name manually',
placeholder: 'e.g. /Parents/0/FamilyName',
},
],
},
{
displayName: 'To Path',
name: 'toPath',
type: 'resourceLocator',
description: 'Select a field from the list or enter it manually',
displayOptions: {
show: {
op: ['move'],
},
},
default: {
mode: 'list',
value: '',
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getDynamicFields',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'manual',
type: 'string',
hint: 'Enter the field name manually',
placeholder: 'e.g. /Parents/0/FamilyName',
},
],
},
{
displayName: 'Path',
name: 'path',
type: 'string',
default: '',
type: 'resourceLocator',
description: 'Select a field from the list or enter it manually',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
op: ['add', 'remove', 'set', 'incr', 'replace'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getDynamicFields',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'manual',
type: 'string',
hint: 'Enter the field name manually',
placeholder: 'e.g. /Parents/0/FamilyName',
},
],
},
{
displayName: 'Value',
@ -767,20 +854,13 @@ export const updateFields: INodeProperties[] = [
default: '',
displayOptions: {
show: {
op: ['add', 'set', 'increment'],
op: ['add', 'set', 'replace', 'incr'],
},
},
},
],
},
],
routing: {
send: {
type: 'body',
property: 'operations',
value: '={{ $parameter["operations"].operations }}',
},
},
},
];