mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 00:24:07 -08:00
7ce7285f7a
* Changes to types so that credentials can be always loaded from DB This first commit changes all return types from the execute functions and calls to get credentials to be async so we can use await. This is a first step as previously credentials were loaded in memory and always available. We will now be loading them from the DB which requires turning the whole call chain async. * Fix updated files * Removed unnecessary credential loading to improve performance * Fix typo * ⚡ Fix issue * Updated new nodes to load credentials async * ⚡ Remove not needed comment Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
578 lines
14 KiB
TypeScript
578 lines
14 KiB
TypeScript
import {
|
|
OptionsWithUri,
|
|
} from 'request';
|
|
|
|
import {
|
|
IExecuteFunctions,
|
|
IExecuteSingleFunctions,
|
|
IHookFunctions,
|
|
ILoadOptionsFunctions,
|
|
} from 'n8n-core';
|
|
|
|
import {
|
|
IDataObject,
|
|
IDisplayOptions,
|
|
INodeProperties,
|
|
IPollFunctions,
|
|
NodeApiError,
|
|
} from 'n8n-workflow';
|
|
|
|
import {
|
|
camelCase,
|
|
capitalCase,
|
|
} from 'change-case';
|
|
|
|
import * as moment from 'moment-timezone';
|
|
|
|
import { validate as uuidValidate } from 'uuid';
|
|
|
|
export async function notionApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
|
|
|
try {
|
|
let options: OptionsWithUri = {
|
|
headers: {
|
|
'Notion-Version': '2021-05-13',
|
|
},
|
|
method,
|
|
qs,
|
|
body,
|
|
uri: uri || `https://api.notion.com/v1${resource}`,
|
|
json: true,
|
|
};
|
|
|
|
options = Object.assign({}, options, option);
|
|
const credentials = await this.getCredentials('notionApi') as IDataObject;
|
|
options!.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
|
|
return this.helpers.request!(options);
|
|
|
|
} catch (error) {
|
|
throw new NodeApiError(this.getNode(), error);
|
|
}
|
|
}
|
|
|
|
export async function notionApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
|
|
|
const resource = this.getNodeParameter('resource', 0) as string;
|
|
|
|
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]);
|
|
if (query.limit && query.limit <= returnData.length) {
|
|
return returnData;
|
|
}
|
|
} while (
|
|
responseData.has_more !== false
|
|
);
|
|
|
|
return returnData;
|
|
}
|
|
|
|
export function getBlockTypes() {
|
|
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',
|
|
},
|
|
];
|
|
}
|
|
|
|
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 === true && text.textLink !== '') {
|
|
return {
|
|
link: {
|
|
url: text.textLink,
|
|
},
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function getTexts(texts: [{ textType: string, text: string, isLink: boolean, range: boolean, textLink: string, mentionType: string, dateStart: string, dateEnd: string, date: string, annotationUi: IDataObject, expression: string }]) {
|
|
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 === true)
|
|
? { start: text.dateStart, end: text.dateEnd }
|
|
: { start: text.date, end: null },
|
|
},
|
|
annotations: text.annotationUi,
|
|
});
|
|
} else {
|
|
//@ts-ignore
|
|
results.push({
|
|
type: 'mention',
|
|
mention: {
|
|
type: text.mentionType,
|
|
//@ts-ignore
|
|
[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;
|
|
}
|
|
|
|
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 } : { checked: false },
|
|
//@ts-expect-error
|
|
// tslint:disable-next-line: no-any
|
|
text: (block.richText === false) ? formatText(block.textContent).text : getTexts(block.text.text as any || []),
|
|
},
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// tslint:disable-next-line: no-any
|
|
function getPropertyKeyValue(value: any, type: string, timezone: string) {
|
|
let result = {};
|
|
switch (type) {
|
|
case 'rich_text':
|
|
if (value.richText === false) {
|
|
result = { rich_text: [{ text: { content: value.textContent } }] };
|
|
} else {
|
|
result = { rich_text: getTexts(value.text.text) };
|
|
}
|
|
break;
|
|
case 'title':
|
|
result = { title: [{ text: { content: value.title } }] };
|
|
break;
|
|
case 'number':
|
|
result = { type: 'number', number: value.numberValue };
|
|
break;
|
|
case 'url':
|
|
result = { type: 'url', url: value.urlValue };
|
|
break;
|
|
case 'checkbox':
|
|
result = { type: 'checkbox', checkbox: value.checkboxValue };
|
|
break;
|
|
case 'relation':
|
|
result = {
|
|
// tslint:disable-next-line: no-any
|
|
type: 'relation', relation: (value.relationValue).reduce((acc: [], cur: any) => {
|
|
return acc.concat(cur.split(',').map((relation: string) => ({ id: relation })));
|
|
}, []),
|
|
};
|
|
break;
|
|
case 'multi_select':
|
|
const multiSelectValue = value.multiSelectValue;
|
|
result = {
|
|
type: 'multi_select',
|
|
// tslint:disable-next-line: no-any
|
|
multi_select: (Array.isArray(multiSelectValue) ? multiSelectValue : multiSelectValue.split(',').map((v: string) => v.trim()))
|
|
// tslint:disable-next-line: no-any
|
|
.filter((value: any) => value !== null)
|
|
.map((option: string) =>
|
|
((!uuidValidate(option)) ? { name: option } : { id: option })),
|
|
};
|
|
break;
|
|
case 'email':
|
|
result = {
|
|
type: 'email', email: value.emailValue,
|
|
};
|
|
break;
|
|
case 'people':
|
|
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':
|
|
result = {
|
|
type: 'select', select: { id: value.selectValue },
|
|
};
|
|
break;
|
|
case 'date':
|
|
const format = getDateFormat(value.includeTime);
|
|
const timezoneValue = (value.timezone === 'default') ? timezone : value.timezone;
|
|
if (value.range === true) {
|
|
result = {
|
|
type: 'date',
|
|
date: {
|
|
start: moment.tz(value.dateStart, timezoneValue).format(format),
|
|
end: moment.tz(value.dateEnd, timezoneValue).format(format),
|
|
},
|
|
};
|
|
} else {
|
|
result = {
|
|
type: 'date',
|
|
date: {
|
|
start: moment.tz(value.date, timezoneValue).format(format),
|
|
end: null,
|
|
},
|
|
};
|
|
}
|
|
break;
|
|
default:
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getDateFormat(includeTime: boolean) {
|
|
if (includeTime === false) {
|
|
return 'yyyy-MM-DD';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function getNameAndType(key: string) {
|
|
const [name, type] = key.split('|');
|
|
return {
|
|
name,
|
|
type,
|
|
};
|
|
}
|
|
|
|
export function mapProperties(properties: IDataObject[], timezone: string) {
|
|
return properties.reduce((obj, value) => Object.assign(obj, {
|
|
[`${(value.key as string).split('|')[0]}`]: getPropertyKeyValue(value, (value.key as string).split('|')[1], timezone),
|
|
}), {});
|
|
}
|
|
|
|
export function mapSorting(data: [{ key: string, type: string, direction: string, timestamp: boolean }]) {
|
|
return data.map((sort) => {
|
|
return {
|
|
direction: sort.direction,
|
|
[(sort.timestamp) ? 'timestamp' : 'property']: sort.key.split('|')[0],
|
|
};
|
|
});
|
|
}
|
|
|
|
export function mapFilters(filters: IDataObject[], timezone: string) {
|
|
// tslint:disable-next-line: no-any
|
|
return filters.reduce((obj, value: { [key: string]: any }) => {
|
|
let key = getNameAndType(value.key).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';
|
|
} else if (key === 'phone_number') {
|
|
key = 'phone';
|
|
} else if (key === 'date' && !['is_empty', 'is_not_empty'].includes(value.condition as string)) {
|
|
valuePropertyName = (valuePropertyName !== undefined && !Object.keys(valuePropertyName).length) ? {} : moment.tz(value.date, timezone).utc().format();
|
|
}
|
|
|
|
return Object.assign(obj, {
|
|
['property']: getNameAndType(value.key).name,
|
|
[key]: { [`${value.condition}`]: valuePropertyName },
|
|
});
|
|
}, {});
|
|
}
|
|
|
|
// tslint:disable-next-line: no-any
|
|
export function simplifyProperties(properties: any) {
|
|
// tslint:disable-next-line: no-any
|
|
const results: any = {};
|
|
for (const key of Object.keys(properties)) {
|
|
const type = (properties[key] as IDataObject).type as string;
|
|
if (['text'].includes(properties[key].type)) {
|
|
const texts = properties[key].text.map((e: { plain_text: string }) => e.plain_text || {}).join('');
|
|
results[`${key}`] = texts;
|
|
} else if (['url', 'created_time', 'checkbox', 'number', 'last_edited_time', 'email', 'phone_number', 'date'].includes(properties[key].type)) {
|
|
// tslint:disable-next-line: no-any
|
|
results[`${key}`] = properties[key][type] as any;
|
|
} else if (['title'].includes(properties[key].type)) {
|
|
if (Array.isArray(properties[key][type]) && properties[key][type].length !== 0) {
|
|
results[`${key}`] = properties[key][type][0].plain_text;
|
|
} else {
|
|
results[`${key}`] = '';
|
|
}
|
|
} else if (['created_by', 'last_edited_by', 'select'].includes(properties[key].type)) {
|
|
results[`${key}`] = properties[key][type].name;
|
|
} else if (['people'].includes(properties[key].type)) {
|
|
if (Array.isArray(properties[key][type])) {
|
|
// tslint:disable-next-line: no-any
|
|
results[`${key}`] = properties[key][type].map((person: any) => person.person.email || {});
|
|
} else {
|
|
results[`${key}`] = properties[key][type];
|
|
}
|
|
} else if (['multi_select'].includes(properties[key].type)) {
|
|
if (Array.isArray(properties[key][type])) {
|
|
results[`${key}`] = properties[key][type].map((e: IDataObject) => e.name || {});
|
|
} else {
|
|
results[`${key}`] = properties[key][type].options.map((e: IDataObject) => e.name || {});
|
|
}
|
|
} else if (['relation'].includes(properties[key].type)) {
|
|
if (Array.isArray(properties[key][type])) {
|
|
results[`${key}`] = properties[key][type].map((e: IDataObject) => e.id || {});
|
|
} else {
|
|
results[`${key}`] = properties[key][type].database_id;
|
|
}
|
|
} else if (['formula'].includes(properties[key].type)) {
|
|
results[`${key}`] = properties[key][type][properties[key][type].type];
|
|
|
|
} else if (['rollup'].includes(properties[key].type)) {
|
|
//TODO figure how to resolve rollup field type
|
|
// results[`${key}`] = properties[key][type][properties[key][type].type];
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// tslint:disable-next-line: no-any
|
|
export function simplifyObjects(objects: any) {
|
|
if (!Array.isArray(objects)) {
|
|
objects = [objects];
|
|
}
|
|
const results: IDataObject[] = [];
|
|
for (const { object, id, properties, parent, title } of objects) {
|
|
if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) {
|
|
results.push({
|
|
id,
|
|
title: properties.title.title[0].plain_text,
|
|
});
|
|
} else if (object === 'page' && parent.type === 'database_id') {
|
|
results.push({
|
|
id,
|
|
...simplifyProperties(properties),
|
|
});
|
|
} else if (object === 'database') {
|
|
results.push({
|
|
id,
|
|
title: title[0].plain_text,
|
|
});
|
|
}
|
|
}
|
|
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',
|
|
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',
|
|
],
|
|
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',
|
|
],
|
|
formula: [
|
|
'contains',
|
|
'does_not_contain',
|
|
'is_empty',
|
|
'is_not_empty',
|
|
],
|
|
};
|
|
|
|
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]] as string[]).map((type: string) => ({ name: capitalCase(type), value: type })),
|
|
default: '',
|
|
description: 'The value of the property to filter by.',
|
|
} as INodeProperties,
|
|
);
|
|
}
|
|
return elements;
|
|
}
|