n8n/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1192 lines
29 KiB
TypeScript
Raw Normal View History

import type {
IBinaryKeyData,
IDataObject,
IDisplayOptions,
IExecuteFunctions,
IHookFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
INode,
INodeExecutionData,
INodeProperties,
IPairedItemData,
IPollFunctions,
IRequestOptions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { camelCase, capitalCase, snakeCase } from 'change-case';
import moment from 'moment-timezone';
import { validate as uuidValidate } from 'uuid';
import set from 'lodash/set';
import { filters } from './descriptions/Filters';
import { blockUrlExtractionRegexp } from './constants';
function uuidValidateWithoutDashes(this: IExecuteFunctions, value: string) {
if (uuidValidate(value)) return true;
if (value.length == 32) {
const strWithDashes = `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
if (uuidValidate(strWithDashes)) return true;
}
throw new NodeOperationError(
this.getNode(),
`The relation id "${value}" is not a valid uuid with optional dashes.`,
);
}
export type SortData = { key: string; type: string; direction: string; timestamp: boolean };
const apiVersion: { [key: number]: string } = {
1: '2021-05-13',
2: '2021-08-16',
2.1: '2021-08-16',
2.2: '2021-08-16',
};
export async function notionApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
option: IDataObject = {},
): Promise<any> {
try {
let options: IRequestOptions = {
headers: {
'Notion-Version': apiVersion[this.getNode().typeVersion],
},
method,
qs,
body,
uri: uri || `https://api.notion.com/v1${resource}`,
json: true,
};
options = Object.assign({}, options, option);
if (Object.keys(body as IDataObject).length === 0) {
delete options.body;
}
if (!uri) {
return await this.helpers.requestWithAuthentication.call(this, 'notionApi', options);
}
return await this.helpers.request(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function notionApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const resource = this.getNodeParameter('resource', 0);
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await notionApiRequest.call(this, method, endpoint, body, query);
const { next_cursor } = responseData;
if (resource === 'block' || resource === 'user') {
query.start_cursor = next_cursor;
} else {
body.start_cursor = next_cursor;
}
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
const limit = query.limit as number | undefined;
if (limit && limit <= returnData.length) {
return returnData;
}
} while (responseData.has_more !== false);
return returnData;
}
export async function notionApiRequestGetBlockChildrens(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
blocks: IDataObject[],
responseData: IDataObject[] = [],
limit?: number,
) {
if (blocks.length === 0) return responseData;
for (const block of blocks) {
responseData.push(block);
if (block.type === 'child_page') continue;
if (block.has_children) {
let childrens = await notionApiRequestAllItems.call(
this,
'results',
'GET',
`/blocks/${block.id}/children`,
);
childrens = (childrens || []).map((entry: IDataObject) => ({
object: entry.object,
parent_id: block.id,
...entry,
}));
await notionApiRequestGetBlockChildrens.call(this, childrens, responseData);
}
if (limit && responseData.length === limit) {
return responseData;
}
if (limit && responseData.length > limit) {
return responseData.slice(0, limit);
}
}
return responseData;
}
export function getBlockTypesOptions() {
return [
{
name: 'Paragraph',
value: 'paragraph',
},
{
name: 'Heading 1',
value: 'heading_1',
},
{
name: 'Heading 2',
value: 'heading_2',
},
{
name: 'Heading 3',
value: 'heading_3',
},
{
name: 'Toggle',
value: 'toggle',
},
{
name: 'To-Do',
value: 'to_do',
},
// {
// name: 'Child Page',
// value: 'child_page',
// },
{
name: 'Bulleted List Item',
value: 'bulleted_list_item',
},
{
name: 'Numbered List Item',
value: 'numbered_list_item',
},
{
name: 'Image',
value: 'image',
},
];
}
function textContent(content: string) {
return {
text: {
content,
},
};
}
export function formatTitle(content: string) {
return {
title: [textContent(content)],
};
}
export function formatText(content: string) {
return {
text: [textContent(content)],
};
}
function getLink(text: { textLink: string; isLink: boolean }) {
if (text.isLink && text.textLink !== '') {
return {
link: {
url: text.textLink,
},
};
}
return {};
}
type TextData = {
textType: string;
text: string;
isLink: boolean;
range: boolean;
textLink: string;
mentionType: string;
dateStart: string;
dateEnd: string;
date: string;
annotationUi: IDataObject;
expression: string;
};
function getTexts(texts: TextData[]) {
const results = [];
for (const text of texts) {
if (text.textType === 'text') {
results.push({
type: 'text',
text: {
content: text.text,
...getLink(text),
},
annotations: text.annotationUi,
});
} else if (text.textType === 'mention') {
if (text.mentionType === 'date') {
results.push({
type: 'mention',
mention: {
type: text.mentionType,
[text.mentionType]: text.range
? { start: text.dateStart, end: text.dateEnd }
: { start: text.date, end: null },
},
annotations: text.annotationUi,
});
} else {
results.push({
type: 'mention',
mention: {
type: text.mentionType,
//@ts-expect-error any
[text.mentionType]: { id: text[text.mentionType] as string },
},
annotations: text.annotationUi,
});
}
} else if (text.textType === 'equation') {
results.push({
type: 'equation',
equation: {
expression: text.expression,
},
annotations: text.annotationUi,
});
}
}
return results;
}
function getTextBlocks(block: IDataObject) {
return {
text:
block.richText === false
? formatText(block.textContent as string).text
: getTexts(((block.text as IDataObject).text as TextData[]) || []),
};
}
export function formatBlocks(blocks: IDataObject[]) {
const results = [];
for (const block of blocks) {
results.push({
object: 'block',
type: block.type,
[block.type as string]: {
...(block.type === 'to_do' ? { checked: block.checked } : {}),
...(block.type === 'image' ? { type: 'external', external: { url: block.url } } : {}),
...(!['image'].includes(block.type as string) ? getTextBlocks(block) : {}),
},
});
}
return results;
}
function getDateFormat(includeTime: boolean) {
if (!includeTime) {
return 'yyyy-MM-DD';
}
return '';
}
function isEmpty(value: unknown): boolean {
return value === undefined || value === null || value === '';
}
function getPropertyKeyValue(
this: IExecuteFunctions,
value: any,
type: string,
timezone: string,
version = 1,
) {
const ignoreIfEmpty = <T>(v: T, cb: (v: T) => any) =>
!v && value.ignoreIfEmpty ? undefined : cb(v);
let result: IDataObject = {};
switch (type) {
case 'rich_text':
if (value.richText === false) {
result = { rich_text: [{ text: { content: value.textContent } }] };
} else {
result = { rich_text: getTexts(value.text.text as TextData[]) };
}
break;
case 'title':
result = { title: [{ text: { content: value.title } }] };
break;
case 'number':
result = { type: 'number', number: value.numberValue };
break;
case 'url':
result = ignoreIfEmpty(value.urlValue, (url) => ({ type: 'url', url }));
break;
case 'checkbox':
result = { type: 'checkbox', checkbox: value.checkboxValue };
break;
case 'relation':
result = {
type: 'relation',
relation: value.relationValue
.filter((relation: IDataObject) => {
return relation && typeof relation === 'string';
})
.reduce((acc: string[], cur: string) => {
return acc.concat(cur.split(',').map((relation: string) => relation.trim()));
}, [])
.filter((relation: string) => {
return uuidValidateWithoutDashes.call(this, relation);
})
.map((relation: string) => ({ id: relation })),
};
break;
case 'multi_select':
if (isEmpty(value.multiSelectValue)) {
result = {
type: 'multi_select',
multi_select: [],
};
break;
}
const multiSelectValue = value.multiSelectValue;
result = {
type: 'multi_select',
multi_select: (Array.isArray(multiSelectValue)
? multiSelectValue
: multiSelectValue.split(',').map((v: string) => v.trim())
)
.filter((entry: any) => entry !== null)
.map((option: string) => (!uuidValidate(option) ? { name: option } : { id: option })),
};
break;
case 'email':
result = {
type: 'email',
email: value.emailValue,
};
break;
case 'people':
//if expression it's a single value, make it an array
if (!Array.isArray(value.peopleValue)) {
value.peopleValue = [value.peopleValue];
}
result = {
type: 'people',
people: value.peopleValue.map((option: string) => ({ id: option })),
};
break;
case 'phone_number':
result = {
type: 'phone_number',
phone_number: value.phoneValue,
};
break;
case 'select':
if (isEmpty(value.selectValue)) {
result = {
type: 'select',
select: null,
};
break;
}
result = {
type: 'select',
select: version === 1 ? { id: value.selectValue } : { name: value.selectValue },
};
break;
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
case 'status':
result = {
type: 'status',
status: { name: value.statusValue },
};
break;
case 'date':
const format = getDateFormat(value.includeTime as boolean);
const timezoneValue = value.timezone === 'default' ? timezone : value.timezone;
if (value.range === true) {
result = {
type: 'date',
date: {
start: moment.tz(value.dateStart as number, timezoneValue as string).format(format),
end: moment.tz(value.dateEnd as number, timezoneValue as string).format(format),
},
};
} else {
result = {
type: 'date',
date: {
start: moment.tz(value.date as number, timezoneValue as string).format(format),
end: null,
},
};
}
//if the date was left empty, set it to null so it resets the value in notion
if (value.date === '' || (value.dateStart === '' && value.dateEnd === '')) {
result.date = null;
}
break;
case 'files':
result = {
type: 'files',
files: value.fileUrls.fileUrl.map((file: { name: string; url: string }) => ({
name: file.name,
type: 'external',
external: { url: file.url },
})),
};
break;
default:
}
return result;
}
function getNameAndType(key: string) {
const [name, type] = key.split('|');
return {
name,
type,
};
}
export function mapProperties(
this: IExecuteFunctions,
properties: IDataObject[],
timezone: string,
version = 1,
) {
return properties
.filter(
(property): property is Record<string, { key: string; [k: string]: any }> =>
typeof property.key === 'string',
)
.map(
(property) =>
[
`${property.key.split('|')[0]}`,
getPropertyKeyValue.call(
this,
property,
property.key.split('|')[1] as string,
timezone,
version,
),
] as const,
)
.filter(([, value]) => value)
.reduce(
(obj, [key, value]) =>
Object.assign(obj, {
[key]: value,
}),
{},
);
}
export function mapSorting(data: SortData[]) {
return data.map((sort) => {
return {
direction: sort.direction,
[sort.timestamp ? 'timestamp' : 'property']: sort.key.split('|')[0],
};
});
}
export function mapFilters(filtersList: IDataObject[], timezone: string) {
return filtersList.reduce((obj, value: { [key: string]: any }) => {
let key = getNameAndType(value.key as string).type;
let valuePropertyName =
key === 'last_edited_time' ? value[camelCase(key)] : value[`${camelCase(key)}Value`];
if (['is_empty', 'is_not_empty'].includes(value.condition as string)) {
valuePropertyName = true;
} else if (
['past_week', 'past_month', 'past_year', 'next_week', 'next_month', 'next_year'].includes(
value.condition as string,
)
) {
valuePropertyName = {};
}
if (key === 'rich_text' || key === 'text') {
key = 'text';
} else if (key === 'phone_number') {
key = 'phone';
} else if (
key === 'date' &&
!['is_empty', 'is_not_empty'].includes(value.condition as string)
) {
valuePropertyName = value.date === '' ? {} : moment.tz(value.date, timezone).utc().format();
} else if (key === 'boolean') {
key = 'checkbox';
}
if (value.type === 'formula') {
if (['is_empty', 'is_not_empty'].includes(value.condition as string)) {
key = value.returnType;
} else {
const vpropertyName = value[`${camelCase(value.returnType as string)}Value`];
return Object.assign(obj, {
['property']: getNameAndType(value.key as string).name,
[key]: { [value.returnType]: { [`${value.condition}`]: vpropertyName } },
});
}
}
return Object.assign(obj, {
['property']: getNameAndType(value.key as string).name,
[key]: { [`${value.condition}`]: valuePropertyName },
});
}, {});
}
function simplifyProperty(property: any) {
let result: any;
const type = (property as IDataObject).type as string;
if (['text'].includes(property.type as string)) {
result = property.plain_text;
} else if (['rich_text', 'title'].includes(property.type as string)) {
if (Array.isArray(property[type]) && property[type].length !== 0) {
result = property[type].map((text: any) => simplifyProperty(text) as string).join('');
} else {
result = '';
}
} else if (
[
'url',
'created_time',
'checkbox',
'number',
'last_edited_time',
'email',
'phone_number',
'date',
].includes(property.type as string)
) {
result = property[type];
} else if (['created_by', 'last_edited_by', 'select'].includes(property.type as string)) {
result = property[type] ? property[type].name : null;
} else if (['people'].includes(property.type as string)) {
if (Array.isArray(property[type])) {
result = property[type].map((person: any) => person.person?.email || {});
} else {
result = property[type];
}
} else if (['multi_select'].includes(property.type as string)) {
if (Array.isArray(property[type])) {
result = property[type].map((e: IDataObject) => e.name || {});
} else {
result = property[type].options.map((e: IDataObject) => e.name || {});
}
} else if (['relation'].includes(property.type as string)) {
if (Array.isArray(property[type])) {
result = property[type].map((e: IDataObject) => e.id || {});
} else {
result = property[type].database_id;
}
} else if (['formula'].includes(property.type as string)) {
result = property[type][property[type].type];
} else if (['rollup'].includes(property.type as string)) {
const rollupFunction = property[type].function as string;
if (rollupFunction.startsWith('count') || rollupFunction.includes('empty')) {
result = property[type].number;
if (rollupFunction.includes('percent')) {
result = result * 100;
}
} else if (rollupFunction.startsWith('show') && property[type].type === 'array') {
const elements = property[type].array.map(simplifyProperty).flat();
result = rollupFunction === 'show_unique' ? [...new Set(elements as string)] : elements;
}
} else if (['files'].includes(property.type as string)) {
result = property[type].map(
(file: { type: string; [key: string]: any }) => file[file.type].url,
);
} else if (['status'].includes(property.type as string)) {
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
result = property[type].name;
}
return result;
}
export function simplifyProperties(properties: any) {
const results: any = {};
for (const key of Object.keys(properties as IDataObject)) {
results[`${key}`] = simplifyProperty(properties[key]);
}
return results;
}
export function getPropertyTitle(properties: { [key: string]: any }) {
return (
Object.values(properties).filter((property) => property.type === 'title')[0].title[0]
?.plain_text || ''
);
}
function prepend(stringKey: string, properties: { [key: string]: any }) {
for (const key of Object.keys(properties)) {
properties[`${stringKey}_${snakeCase(key)}`] = properties[key];
delete properties[key];
}
return properties;
}
export function simplifyObjects(objects: any, download = false, version = 2) {
if (!Array.isArray(objects)) {
objects = [objects];
}
const results: IDataObject[] = [];
for (const { object, id, properties, parent, title, json, binary, url } of objects) {
if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) {
results.push({
id,
name: properties.title.title[0].plain_text,
...(version === 2 ? { url } : {}),
});
} else if (object === 'page' && parent.type === 'database_id') {
results.push({
id,
...(version === 2 ? { name: getPropertyTitle(properties as IDataObject) } : {}),
...(version === 2 ? { url } : {}),
...(version === 2
? { ...prepend('property', simplifyProperties(properties) as IDataObject) }
: { ...simplifyProperties(properties) }),
} as IDataObject);
} else if (download && json.object === 'page' && json.parent.type === 'database_id') {
results.push({
json: {
id: json.id,
...(version === 2 ? { name: getPropertyTitle(json.properties as IDataObject) } : {}),
...(version === 2 ? { url: json.url } : {}),
...(version === 2
? { ...prepend('property', simplifyProperties(json.properties) as IDataObject) }
: { ...simplifyProperties(json.properties) }),
},
binary,
});
} else if (object === 'database') {
results.push({
id,
...(version === 2
? { name: title[0]?.plain_text || '' }
: { title: title[0]?.plain_text || '' }),
...(version === 2 ? { url } : {}),
});
}
}
return results;
}
export function getFormattedChildren(children: IDataObject[]) {
const results: IDataObject[] = [];
for (const child of children) {
const type = child.type;
results.push({ [`${type}`]: child, object: 'block', type });
}
return results;
}
export function getConditions() {
const elements: INodeProperties[] = [];
const types: { [key: string]: string } = {
title: 'rich_text',
rich_text: 'rich_text',
number: 'number',
checkbox: 'checkbox',
select: 'select',
multi_select: 'multi_select',
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
status: 'status',
date: 'date',
people: 'people',
files: 'files',
url: 'rich_text',
email: 'rich_text',
phone_number: 'rich_text',
relation: 'relation',
//formula: 'formula',
created_by: 'people',
created_time: 'date',
last_edited_by: 'people',
last_edited_time: 'date',
};
const typeConditions: { [key: string]: string[] } = {
rich_text: [
'equals',
'does_not_equal',
'contains',
'does_not_contain',
'starts_with',
'ends_with',
'is_empty',
'is_not_empty',
],
number: [
'equals',
'does_not_equal',
'grater_than',
'less_than',
'greater_than_or_equal_to',
'less_than_or_equal_to',
'is_empty',
'is_not_empty',
],
checkbox: ['equals', 'does_not_equal'],
select: ['equals', 'does_not_equal', 'is_empty', 'is_not_empty'],
multi_select: ['contains', 'does_not_equal', 'is_empty', 'is_not_empty'],
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
status: ['equals', 'does_not_equal'],
date: [
'equals',
'before',
'after',
'on_or_before',
'is_empty',
'is_not_empty',
'on_or_after',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
people: ['contains', 'does_not_contain', 'is_empty', 'is_not_empty'],
files: ['is_empty', 'is_not_empty'],
relation: ['contains', 'does_not_contain', 'is_empty', 'is_not_empty'],
};
const formula: { [key: string]: string[] } = {
text: [...typeConditions.rich_text],
checkbox: [...typeConditions.checkbox],
number: [...typeConditions.number],
date: [...typeConditions.date],
};
for (const type of Object.keys(types)) {
elements.push({
displayName: 'Condition',
name: 'condition',
type: 'options',
displayOptions: {
show: {
type: [type],
},
} as IDisplayOptions,
options: typeConditions[types[type]].map((entry: string) => ({
name: capitalCase(entry),
value: entry,
})),
default: '',
description: 'The value of the property to filter by',
} as INodeProperties);
}
elements.push({
displayName: 'Return Type',
name: 'returnType',
type: 'options',
displayOptions: {
show: {
type: ['formula'],
},
} as IDisplayOptions,
options: Object.keys(formula).map((key: string) => ({ name: capitalCase(key), value: key })),
default: '',
description: 'The formula return type',
} as INodeProperties);
for (const key of Object.keys(formula)) {
elements.push({
displayName: 'Condition',
name: 'condition',
type: 'options',
displayOptions: {
show: {
type: ['formula'],
returnType: [key],
},
} as IDisplayOptions,
options: formula[key].map((entry: string) => ({ name: capitalCase(entry), value: entry })),
default: '',
description: 'The value of the property to filter by',
} as INodeProperties);
}
return elements;
}
export type FileRecord = {
properties: {
[key: string]:
| any
| {
id: string;
type: string;
files: [{ external: { url: string } } | { file: { url: string } }];
};
};
};
export async function downloadFiles(
this: IExecuteFunctions | IPollFunctions,
records: FileRecord[],
pairedItem?: IPairedItemData[],
): Promise<INodeExecutionData[]> {
const elements: INodeExecutionData[] = [];
for (const record of records) {
const element: INodeExecutionData = { json: {}, binary: {} };
element.json = record as unknown as IDataObject;
if (pairedItem) {
element.pairedItems = pairedItem;
}
for (const key of Object.keys(record.properties)) {
if (record.properties[key].type === 'files') {
if (record.properties[key].files.length) {
for (const [index, file] of record.properties[key].files.entries()) {
const data = await notionApiRequest.call(
this,
'GET',
'',
{},
{},
(file?.file?.url as string) || (file?.external?.url as string),
{ json: false, encoding: null },
);
element.binary![`${key}_${index}`] = await this.helpers.prepareBinaryData(
data as Buffer,
);
}
}
}
}
if (Object.keys(element.binary as IBinaryKeyData).length === 0) {
delete element.binary;
}
elements.push(element);
}
return elements;
}
export function extractPageId(page = '') {
if (page.includes('p=')) {
return page.split('p=')[1];
} else if (page.includes('-') && page.includes('https')) {
return page.split('-')[page.split('-').length - 1];
}
return page;
}
export function extractDatabaseId(database: string) {
if (database.includes('?v=')) {
const data = database.split('?v=')[0].split('/');
const index = data.length - 1;
return data[index];
} else if (database.includes('/')) {
const index = database.split('/').length - 1;
return database.split('/')[index];
} else {
return database;
}
}
export function getSearchFilters(resource: string) {
return [
{
displayName: 'Filter',
name: 'filterType',
type: 'options',
options: [
{
name: 'None',
value: 'none',
},
{
name: 'Build Manually',
value: 'manual',
},
{
name: 'JSON',
value: 'json',
},
],
displayOptions: {
show: {
resource: [resource],
operation: ['getAll'],
},
hide: {
'@version': [1],
},
},
default: 'none',
},
{
displayName: 'Must Match',
name: 'matchType',
type: 'options',
options: [
{
name: 'Any filter',
value: 'anyFilter',
},
{
name: 'All Filters',
value: 'allFilters',
},
],
displayOptions: {
show: {
resource: [resource],
operation: ['getAll'],
filterType: ['manual'],
},
hide: {
'@version': [1],
},
},
default: 'anyFilter',
},
{
displayName: 'Filters',
name: 'filters',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: [resource],
operation: ['getAll'],
filterType: ['manual'],
},
hide: {
'@version': [1],
},
},
refactor: Apply `eslint-plugin-n8n-nodes-base` autofixable rules (#3174) * :zap: Initial setup * :shirt: Update `.eslintignore` * :shirt: Autofix node-param-default-missing (#3173) * :fire: Remove duplicate key * :shirt: Add exceptions * :package: Update package-lock.json * :shirt: Apply `node-class-description-inputs-wrong-trigger-node` (#3176) * :shirt: Apply `node-class-description-inputs-wrong-regular-node` (#3177) * :shirt: Apply `node-class-description-outputs-wrong` (#3178) * :shirt: Apply `node-execute-block-double-assertion-for-items` (#3179) * :shirt: Apply `node-param-default-wrong-for-collection` (#3180) * :shirt: Apply node-param-default-wrong-for-boolean (#3181) * Autofixed default missing * Autofixed booleans, worked well * :zap: Fix params * :rewind: Undo exempted autofixes * :package: Update package-lock.json * :shirt: Apply node-class-description-missing-subtitle (#3182) * :zap: Fix missing comma * :shirt: Apply `node-param-default-wrong-for-fixed-collection` (#3184) * :shirt: Add exception for `node-class-description-missing-subtitle` * :shirt: Apply `node-param-default-wrong-for-multi-options` (#3185) * :shirt: Apply `node-param-collection-type-unsorted-items` (#3186) * Missing coma * :shirt: Apply `node-param-default-wrong-for-simplify` (#3187) * :shirt: Apply `node-param-description-comma-separated-hyphen` (#3190) * :shirt: Apply `node-param-description-empty-string` (#3189) * :shirt: Apply `node-param-description-excess-inner-whitespace` (#3191) * Rule looks good * Add whitespace rule in eslint config * :zao: fix * :shirt: Apply `node-param-description-identical-to-display-name` (#3193) * :shirt: Apply `node-param-description-missing-for-ignore-ssl-issues` (#3195) * :rewind: Revert ":zao: fix" This reverts commit ef8a76f3dfedffd1bdccf3178af8a8d90cf5a55c. * :shirt: Apply `node-param-description-missing-for-simplify` (#3196) * :shirt: Apply `node-param-description-missing-final-period` (#3194) * Rule working as intended * Add rule to eslint * :shirt: Apply node-param-description-missing-for-return-all (#3197) * :zap: Restore `lintfix` command Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: agobrech <ael.gobrecht@gmail.com> Co-authored-by: Michael Kret <michael.k@radency.com>
2022-04-22 09:29:51 -07:00
default: {},
placeholder: 'Add Condition',
options: [
{
displayName: 'Conditions',
name: 'conditions',
values: [...filters(getConditions())],
},
],
},
{
displayName:
'See <a href="https://developers.notion.com/reference/post-database-query#post-database-query-filter" target="_blank">Notion guide</a> to creating filters',
name: 'jsonNotice',
type: 'notice',
displayOptions: {
show: {
resource: [resource],
operation: ['getAll'],
filterType: ['json'],
},
hide: {
'@version': [1],
},
},
default: '',
},
{
displayName: 'Filters (JSON)',
name: 'filterJson',
type: 'string',
displayOptions: {
show: {
resource: [resource],
operation: ['getAll'],
filterType: ['json'],
},
hide: {
'@version': [1],
},
},
default: '',
},
];
}
export function validateJSON(json: string | undefined): any {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
/**
* Manually extract a richtext's database mention RLC parameter.
* @param blockValues the blockUi.blockValues node parameter.
*/
export function extractDatabaseMentionRLC(blockValues: IDataObject[]) {
blockValues.forEach((bv) => {
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
if (bv.richText && bv.text) {
const texts = (
bv.text as {
text: [
{
textType: string;
mentionType: string;
database: string | { value: string; mode: string; __rl: boolean; __regex: string };
},
];
}
).text;
texts.forEach((txt) => {
feat(Notion (Beta) Node): Use resource locator component for database and page parameters (#4340) * use resource locator component for database -> get (Notion V1/V2) * getDatabases search function for V1/V2 with url * updated database get list placeholder * get database RLC by url - regex support optional workspace domain names * fixed linting error * listSearch getDatabases support filter query * support extractValue in getCurrentNodeParameter for RLC * RLC for database page create/getAll operation * RLC for database get operation support "By ID" with optional v param. * use RLC in append blocks operation * use RLC in NotionTrigger.nodes.ts * removed unused loadOptions getDatabases * support database RLC in createPage/createDbPage operation * page create operation use RLC for parent page param * page archive operation use RLC for page param * removed unused imports * fixed missing extractPageId in NotionV1.node.ts * database page get operation use RLC for page param * database page update operation use RLC for page param * block getAll children operation use RLC for page param * block append operation use RLC for block param * support databaseId with optional '-' characters * support blockId with optional '-' characters * support pageId with optional '-' characters * improved RLC descriptions and hints * NotionTrigger node support databseId with optional '-' characters * stricter RLC by ID regex rules for uuids * stricter RLC by URL regex rules for uuids * stricter RLC by ID regex rules for uuids (support max length) * RLC regex from URL allow both http and https * RLC by ID only allow uuid v4 with optional dash * removed RLC from URL hint "Use Notion's copy link..." * RLC from URL only allow uuid v4 * DB Status Column: Support Simplify Properties * Notion Credentials: Support custom Notion-Version header Use latest Notion-Version 2022-02-22 if not set * DB Status Column: Support DB Page Create/Update * DB Status Column: Support DB Page GetMany Filters * removed unused paginationToken args * Database Get: RLC by URL improve validation error message
2022-11-11 04:37:52 -08:00
if (txt.textType === 'mention' && txt.mentionType === 'database') {
if (typeof txt.database === 'object' && txt.database.__rl) {
if (txt.database.__regex) {
const regex = new RegExp(txt.database.__regex);
const extracted = regex.exec(txt.database.value);
txt.database = extracted![1];
} else {
txt.database = txt.database.value;
}
}
}
});
}
});
}
export function simplifyBlocksOutput(blocks: IDataObject[], rootId: string) {
for (const block of blocks) {
const type = block.type as string;
block.root_id = rootId;
['created_time', 'last_edited_time', 'created_by'].forEach((key) => {
delete block[key];
});
try {
if (['code'].includes(type)) {
const text = (block[type] as IDataObject).text as IDataObject[];
if (text && Array.isArray(text)) {
const content = text.map((entry) => entry.plain_text || '').join('');
block.content = content;
delete block[type];
}
continue;
}
if (['child_page', 'child_database'].includes(type)) {
const content = (block[type] as IDataObject).title as string;
block.content = content;
delete block[type];
continue;
}
const text = (block[type] as IDataObject)?.text as IDataObject[];
if (text && Array.isArray(text)) {
const content = text.map((entry) => entry.plain_text || '').join('');
block.content = content;
delete block[type];
}
} catch (e) {
continue;
}
}
return blocks;
}
export function extractBlockId(this: IExecuteFunctions, nodeVersion: number, itemIndex: number) {
let blockId: string;
if (nodeVersion < 2.2) {
blockId = extractPageId(
this.getNodeParameter('blockId', itemIndex, '', { extractValue: true }) as string,
);
} else {
const blockIdRLCData = this.getNodeParameter('blockId', itemIndex, {}) as IDataObject;
if (blockIdRLCData.mode === 'id') {
blockId = blockIdRLCData.value as string;
} else {
const blockRegex = /https:\/\/www\.notion\.so\/.+\?pvs=[0-9]+#([a-f0-9]{2,})/;
const match = (blockIdRLCData.value as string).match(blockRegex);
if (match === null) {
const pageRegex = new RegExp(blockUrlExtractionRegexp);
const pageMatch = (blockIdRLCData.value as string).match(pageRegex);
if (pageMatch === null) {
throw new NodeOperationError(
this.getNode(),
'Invalid URL, could not find block ID or page ID',
{
itemIndex,
},
);
} else {
blockId = extractPageId(pageMatch[1]);
}
} else {
blockId = match[1];
}
}
}
return blockId;
}
export const prepareNotionError = (node: INode, error: Error, itemIndex: number) => {
if (error instanceof NodeApiError) {
set(error, 'context.itemIndex', itemIndex);
return error;
}
if (error instanceof NodeOperationError && error?.context?.itemIndex === undefined) {
set(error, 'context.itemIndex', itemIndex);
return error;
}
return new NodeOperationError(node, error, { itemIndex });
};