mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
b5da0744c4
Co-authored-by: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com>
281 lines
6.5 KiB
TypeScript
281 lines
6.5 KiB
TypeScript
import type {
|
|
IDataObject,
|
|
IExecuteFunctions,
|
|
IHttpRequestMethods,
|
|
ILoadOptionsFunctions,
|
|
IRequestOptions,
|
|
JsonObject,
|
|
} from 'n8n-workflow';
|
|
import { NodeApiError } from 'n8n-workflow';
|
|
|
|
import type { BaserowCredentials, LoadedResource } from './types';
|
|
|
|
export const MULTI_STEP_DATE_OPERATORS = new Set([
|
|
'date_is',
|
|
'date_is_not',
|
|
'date_is_before',
|
|
'date_is_on_or_before',
|
|
'date_is_after',
|
|
'date_is_on_or_after',
|
|
'date_is_within',
|
|
]);
|
|
|
|
export const DEPRECATED_TIMEZONE_NUMBER_OPERATORS = new Set([
|
|
'date_within_days',
|
|
'date_within_weeks',
|
|
'date_within_months',
|
|
'date_equals_days_ago',
|
|
'date_equals_months_ago',
|
|
'date_equals_years_ago',
|
|
]);
|
|
|
|
export const DEPRECATED_TIMEZONE_ONLY_OPERATORS = new Set([
|
|
'date_equals_today',
|
|
'date_before_today',
|
|
'date_after_today',
|
|
'date_equals_week',
|
|
'date_equals_month',
|
|
'date_equals_year',
|
|
]);
|
|
|
|
const RELATIVE_DATE_OPERATORS = new Set([
|
|
'today',
|
|
'yesterday',
|
|
'tomorrow',
|
|
'one_week_ago',
|
|
'one_month_ago',
|
|
'one_year_ago',
|
|
'this_week',
|
|
'this_month',
|
|
'this_year',
|
|
'next_week',
|
|
'next_month',
|
|
'next_year',
|
|
'nr_days_ago',
|
|
'nr_weeks_ago',
|
|
'nr_months_ago',
|
|
'nr_years_ago',
|
|
'nr_days_from_now',
|
|
'nr_weeks_from_now',
|
|
'nr_months_from_now',
|
|
'nr_years_from_now',
|
|
]);
|
|
|
|
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
function isFullyFormattedMultiStepValue(value: string): boolean {
|
|
const parts = value.split('?');
|
|
return parts.length === 3 && parts[2].length > 0;
|
|
}
|
|
|
|
/**
|
|
* Formats filter values for Baserow date operators.
|
|
* Multi-step operators require `{timezone}?{value}?{operator}` (e.g. `UTC?2026-06-17?exact_date`).
|
|
* `date_equals_day_of_month` is not multi-step — pass the day number as-is (e.g. `15`).
|
|
*/
|
|
export function formatBaserowFilterValue(
|
|
operator: string,
|
|
value: string,
|
|
timezone = 'UTC',
|
|
): string {
|
|
const trimmed = value.trim();
|
|
|
|
if (!trimmed) {
|
|
if (DEPRECATED_TIMEZONE_ONLY_OPERATORS.has(operator)) {
|
|
return timezone;
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
if (MULTI_STEP_DATE_OPERATORS.has(operator)) {
|
|
const malformedMatch = /^([^?]+)\?\?(\d{4}-\d{2}-\d{2})$/.exec(trimmed);
|
|
if (malformedMatch) {
|
|
return `${malformedMatch[1]}?${malformedMatch[2]}?exact_date`;
|
|
}
|
|
|
|
if (isFullyFormattedMultiStepValue(trimmed)) {
|
|
return trimmed;
|
|
}
|
|
|
|
if (ISO_DATE_REGEX.test(trimmed)) {
|
|
return `${timezone}?${trimmed}?exact_date`;
|
|
}
|
|
|
|
if (RELATIVE_DATE_OPERATORS.has(trimmed)) {
|
|
return `${timezone}??${trimmed}`;
|
|
}
|
|
|
|
if (operator === 'date_is_within' && /^\d+$/.test(trimmed)) {
|
|
return `${timezone}?${trimmed}?nr_days_from_now`;
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
if (DEPRECATED_TIMEZONE_NUMBER_OPERATORS.has(operator)) {
|
|
if (trimmed.includes('?')) {
|
|
return trimmed;
|
|
}
|
|
return `${timezone}?${trimmed}`;
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
export function normalizeFilterFieldId(field: string | number): string {
|
|
return String(field).replace(/^field_/, '');
|
|
}
|
|
|
|
/**
|
|
* Make a request to Baserow API.
|
|
*/
|
|
export async function baserowApiRequest(
|
|
this: IExecuteFunctions | ILoadOptionsFunctions,
|
|
method: IHttpRequestMethods,
|
|
endpoint: string,
|
|
credentialType: string,
|
|
body: IDataObject = {},
|
|
qs: IDataObject = {},
|
|
) {
|
|
const credentials = await this.getCredentials<BaserowCredentials>(credentialType);
|
|
const host = (credentials.host as string).replace(/\/$/, '');
|
|
|
|
const options: IRequestOptions = {
|
|
method,
|
|
body,
|
|
qs,
|
|
uri: `${host}${endpoint}`,
|
|
json: true,
|
|
};
|
|
|
|
if (Object.keys(qs).length === 0) {
|
|
delete options.qs;
|
|
}
|
|
|
|
if (Object.keys(body).length === 0) {
|
|
delete options.body;
|
|
}
|
|
|
|
try {
|
|
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
|
|
} catch (error) {
|
|
throw new NodeApiError(this.getNode(), error as JsonObject);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all results from a paginated query to Baserow API.
|
|
*/
|
|
export async function baserowApiRequestAllItems(
|
|
this: IExecuteFunctions,
|
|
method: IHttpRequestMethods,
|
|
endpoint: string,
|
|
credentialType: string,
|
|
body: IDataObject,
|
|
qs: IDataObject = {},
|
|
): Promise<IDataObject[]> {
|
|
const returnData: IDataObject[] = [];
|
|
let responseData;
|
|
|
|
qs.page = 1;
|
|
qs.size = 100;
|
|
|
|
const returnAll = this.getNodeParameter('returnAll', 0, false);
|
|
const limit = this.getNodeParameter('limit', 0, 0);
|
|
|
|
do {
|
|
responseData = await baserowApiRequest.call(this, method, endpoint, credentialType, body, qs);
|
|
returnData.push.apply(returnData, responseData.results as IDataObject[]);
|
|
|
|
if (!returnAll && returnData.length > limit) {
|
|
return returnData.slice(0, limit);
|
|
}
|
|
|
|
qs.page += 1;
|
|
} while (responseData.next !== null);
|
|
|
|
return returnData;
|
|
}
|
|
|
|
export async function getFieldNamesAndIds(
|
|
this: IExecuteFunctions,
|
|
tableId: string,
|
|
credentialType: string,
|
|
) {
|
|
const endpoint = `/api/database/fields/table/${tableId}/`;
|
|
const response = (await baserowApiRequest.call(
|
|
this,
|
|
'GET',
|
|
endpoint,
|
|
credentialType,
|
|
)) as LoadedResource[];
|
|
|
|
return {
|
|
names: response.map((field) => field.name),
|
|
ids: response.map((field) => `field_${field.id}`),
|
|
};
|
|
}
|
|
|
|
export const toOptions = (items: LoadedResource[]) =>
|
|
items.map(({ name, id }) => ({ name, value: id }));
|
|
|
|
/**
|
|
* Responsible for mapping field IDs `field_n` to names and vice versa.
|
|
*/
|
|
export class TableFieldMapper {
|
|
nameToIdMapping: Record<string, string> = {};
|
|
|
|
idToNameMapping: Record<string, string> = {};
|
|
|
|
mapIds = true;
|
|
|
|
async getTableFields(
|
|
this: IExecuteFunctions,
|
|
table: string,
|
|
credentialType: string,
|
|
): Promise<LoadedResource[]> {
|
|
const endpoint = `/api/database/fields/table/${table}/`;
|
|
return await baserowApiRequest.call(this, 'GET', endpoint, credentialType);
|
|
}
|
|
|
|
createMappings(tableFields: LoadedResource[]) {
|
|
this.nameToIdMapping = this.createNameToIdMapping(tableFields);
|
|
this.idToNameMapping = this.createIdToNameMapping(tableFields);
|
|
}
|
|
|
|
private createIdToNameMapping(responseData: LoadedResource[]) {
|
|
return responseData.reduce<Record<string, string>>((acc, cur) => {
|
|
acc[`field_${cur.id}`] = cur.name;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
private createNameToIdMapping(responseData: LoadedResource[]) {
|
|
return responseData.reduce<Record<string, string>>((acc, cur) => {
|
|
acc[cur.name] = `field_${cur.id}`;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
setField(field: string) {
|
|
return this.mapIds ? field : (this.nameToIdMapping[field] ?? field);
|
|
}
|
|
|
|
idsToNames(obj: Record<string, unknown>) {
|
|
Object.entries(obj).forEach(([key, value]) => {
|
|
if (this.idToNameMapping[key] !== undefined) {
|
|
delete obj[key];
|
|
obj[this.idToNameMapping[key]] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
namesToIds(obj: Record<string, unknown>) {
|
|
Object.entries(obj).forEach(([key, value]) => {
|
|
if (this.nameToIdMapping[key] !== undefined) {
|
|
delete obj[key];
|
|
obj[this.nameToIdMapping[key]] = value;
|
|
}
|
|
});
|
|
}
|
|
}
|