diff --git a/packages/frontend/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue b/packages/frontend/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue index e3fa6a32dc..4695acd6dd 100644 --- a/packages/frontend/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue +++ b/packages/frontend/editor-ui/src/components/ResourceMapper/MappingModeSelect.vue @@ -70,7 +70,7 @@ const errorMessage = computed(() => { }); } // No data error message - if (props.fieldsToMap.length === 0) { + if (props.fieldsToMap.length === 0 && !props.typeOptions?.resourceMapper?.hideNoDataError) { return ( // Use custom error message if defined resourceMapperTypeOptions.value?.noFieldsError || diff --git a/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts b/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts index 8539c15763..b60a461667 100644 --- a/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts +++ b/packages/nodes-base/nodes/DataTable/actions/row/get.operation.ts @@ -17,7 +17,20 @@ const displayOptions: IDisplayOptions = { }, }; -export const description: INodeProperties[] = [...getSelectFields(displayOptions)]; +export const description: INodeProperties[] = [ + ...getSelectFields(displayOptions), + { + displayName: 'Limit', + name: 'limit', + type: 'number', + typeOptions: { + minValue: 1, + }, + displayOptions, + default: null, + description: 'Max number of results to return', + }, +]; export async function execute( this: IExecuteFunctions, diff --git a/packages/nodes-base/nodes/DataTable/common/addRow.ts b/packages/nodes-base/nodes/DataTable/common/addRow.ts index 6260e4610b..78ae5ea4fe 100644 --- a/packages/nodes-base/nodes/DataTable/common/addRow.ts +++ b/packages/nodes-base/nodes/DataTable/common/addRow.ts @@ -23,7 +23,7 @@ export function makeAddRow(operation: string, displayOptions: IDisplayOptions) { typeOptions: { loadOptionsDependsOn: [`${DATA_TABLE_ID_FIELD}.value`], resourceMapper: { - valuesLabel: `Columns to ${operation}`, + valuesLabel: `Values to ${operation}`, resourceMapperMethod: 'getDataTables', mode: 'add', fieldWords: { @@ -32,6 +32,7 @@ export function makeAddRow(operation: string, displayOptions: IDisplayOptions) { }, addAllFields: true, multiKeyMatch: true, + hideNoDataError: true, }, }, displayOptions, diff --git a/packages/nodes-base/nodes/DataTable/common/methods.ts b/packages/nodes-base/nodes/DataTable/common/methods.ts index c2fa87cef3..792b41de4a 100644 --- a/packages/nodes-base/nodes/DataTable/common/methods.ts +++ b/packages/nodes-base/nodes/DataTable/common/methods.ts @@ -44,17 +44,22 @@ export async function tableSearch( export async function getDataTableColumns(this: ILoadOptionsFunctions) { const returnData: Array = [ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased-id, n8n-nodes-base/node-param-display-name-miscased - { name: 'id - (number)', value: 'id', type: 'number' }, + { name: 'id (number)', value: 'id', type: 'number' }, // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - { name: 'createdAt - (date)', value: 'createdAt', type: 'date' }, + { name: 'createdAt (date)', value: 'createdAt', type: 'date' }, // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - { name: 'updatedAt - (date)', value: 'updatedAt', type: 'date' }, + { name: 'updatedAt (date)', value: 'updatedAt', type: 'date' }, ]; + const proxy = await getDataTableProxyLoadOptions(this); + if (!proxy) { + return returnData; + } + const columns = await proxy.getColumns(); for (const column of columns) { returnData.push({ - name: `${column.name} - (${column.type})`, + name: `${column.name} (${column.type})`, value: column.name, type: column.type, }); @@ -69,6 +74,10 @@ const systemColumns = [ ] as const; export async function getConditionsForColumn(this: ILoadOptionsFunctions) { + const proxy = await getDataTableProxyLoadOptions(this); + if (!proxy) { + return []; + } const keyName = this.getCurrentNodeParameter('&keyName') as string; // Base conditions available for all column types @@ -87,18 +96,8 @@ export async function getConditionsForColumn(this: ILoadOptionsFunctions) { ]; const stringConditions: INodePropertyOptions[] = [ - { - name: 'LIKE operator', - value: 'like', - description: - 'Case-sensitive pattern matching. Use % as wildcard (e.g., "%Mar%" to match "Anne-Marie").', - }, - { - name: 'ILIKE operator', - value: 'ilike', - description: - 'Case-insensitive pattern matching. Use % as wildcard (e.g., "%mar%" to match "Anne-Marie").', - }, + { name: 'Contains (Case-Sensitive)', value: 'like' }, + { name: 'Contains (Case-Insensitive)', value: 'ilike' }, ]; const allConditions = [...baseConditions, ...comparableConditions, ...stringConditions]; @@ -111,9 +110,7 @@ export async function getConditionsForColumn(this: ILoadOptionsFunctions) { // Get column type to determine available conditions const column = systemColumns.find((col) => col.name === keyName) ?? - (await (await getDataTableProxyLoadOptions(this)).getColumns()).find( - (col) => col.name === keyName, - ); + (await proxy.getColumns()).find((col) => col.name === keyName); if (!column) { return baseConditions; @@ -135,6 +132,9 @@ export async function getConditionsForColumn(this: ILoadOptionsFunctions) { export async function getDataTables(this: ILoadOptionsFunctions): Promise { const proxy = await getDataTableProxyLoadOptions(this); + if (!proxy) { + return { fields: [] }; + } const result = await proxy.getColumns(); const fields: ResourceMapperField[] = []; diff --git a/packages/nodes-base/nodes/DataTable/common/selectMany.ts b/packages/nodes-base/nodes/DataTable/common/selectMany.ts index 33e3d1d5ca..09a5a6d09f 100644 --- a/packages/nodes-base/nodes/DataTable/common/selectMany.ts +++ b/packages/nodes-base/nodes/DataTable/common/selectMany.ts @@ -122,33 +122,48 @@ export async function executeSelectMany( throw new NodeOperationError(ctx.getNode(), 'At least one condition is required'); } - let take = 1000; + const PAGE_SIZE = 1000; const result: Array<{ json: DataStoreRowReturn }> = []; - let totalCount = undefined; - do { - const response = await dataStoreProxy.getManyRowsAndCount({ - skip: result.length, - take, + + const limit = ctx.getNodeParameter('limit', index, undefined); + + let expectedTotal: number | undefined; + let skip = 0; + let take = PAGE_SIZE; + + while (true) { + const { data, count } = await dataStoreProxy.getManyRowsAndCount({ + skip, + take: limit ? Math.min(take, limit - result.length) : take, filter, }); - const data = response.data.map((json) => ({ json })); + const wrapped = data.map((json) => ({ json })); - // Optimize common path of <1000 results - if (response.count === response.data.length) { - return data; + // Fast path: everything fits in a single page + if (skip === 0 && count === data.length) { + return wrapped; } - if (totalCount !== undefined && response.count !== totalCount) { + // Ensure the total doesn't change mid-pagination + if (expectedTotal !== undefined && count !== expectedTotal) { throw new NodeOperationError( ctx.getNode(), 'synchronization error: result count changed during pagination', ); } - totalCount = response.count; + expectedTotal = count; - result.push.apply(result, data); - take = Math.min(take, response.count - result.length); - } while (take > 0); + result.push.apply(result, wrapped); + + // Stop if we've hit the limit + if (limit && result.length >= limit) break; + + // Stop if we've collected everything + if (result.length >= count) break; + + skip = result.length; + take = Math.min(PAGE_SIZE, count - result.length); + } return result; } diff --git a/packages/nodes-base/nodes/DataTable/common/utils.ts b/packages/nodes-base/nodes/DataTable/common/utils.ts index bc09959519..602d2c00db 100644 --- a/packages/nodes-base/nodes/DataTable/common/utils.ts +++ b/packages/nodes-base/nodes/DataTable/common/utils.ts @@ -44,7 +44,7 @@ export async function getDataTableProxyExecute( export async function getDataTableProxyLoadOptions( ctx: ILoadOptionsFunctions, -): Promise { +): Promise { if (ctx.helpers.getDataStoreProxy === undefined) throw new NodeOperationError( ctx.getNode(), @@ -55,6 +55,10 @@ export async function getDataTableProxyLoadOptions( extractValue: true, }) as string; + if (!dataStoreId) { + return; + } + return await ctx.helpers.getDataStoreProxy(dataStoreId); } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index c6c0083e5d..01dbe37896 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -1435,6 +1435,7 @@ export interface ResourceMapperTypeOptionsBase { noFieldsError?: string; multiKeyMatch?: boolean; supportAutoMap?: boolean; + hideNoDataError?: boolean; // Hide "No data found" error when no fields are available matchingFieldsLabels?: { title?: string; description?: string;