2022-08-17 08:50:24 -07:00
|
|
|
import { OptionsWithUri } from 'request';
|
2021-05-20 14:31:23 -07:00
|
|
|
|
|
|
|
import {
|
|
|
|
IExecuteFunctions,
|
|
|
|
IExecuteSingleFunctions,
|
|
|
|
IHookFunctions,
|
|
|
|
ILoadOptionsFunctions,
|
|
|
|
} from 'n8n-core';
|
|
|
|
|
|
|
|
import {
|
2021-12-29 14:23:22 -08:00
|
|
|
IBinaryKeyData,
|
2021-05-20 14:31:23 -07:00
|
|
|
IDataObject,
|
|
|
|
IDisplayOptions,
|
2021-12-29 14:23:22 -08:00
|
|
|
INodeExecutionData,
|
2021-05-20 14:31:23 -07:00
|
|
|
INodeProperties,
|
|
|
|
IPollFunctions,
|
|
|
|
NodeApiError,
|
|
|
|
} from 'n8n-workflow';
|
|
|
|
|
2022-12-02 12:54:28 -08:00
|
|
|
import { camelCase, capitalCase, snakeCase } from 'change-case';
|
2021-05-20 14:31:23 -07:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
import { filters } from './Filters';
|
2021-12-29 14:23:22 -08:00
|
|
|
|
2022-04-08 14:32:08 -07:00
|
|
|
import moment from 'moment-timezone';
|
2021-05-20 14:31:23 -07:00
|
|
|
|
2021-06-13 10:17:39 -07:00
|
|
|
import { validate as uuidValidate } from 'uuid';
|
|
|
|
|
2021-12-29 14:23:22 -08:00
|
|
|
const apiVersion: { [key: number]: string } = {
|
|
|
|
1: '2021-05-13',
|
|
|
|
2: '2021-08-16',
|
|
|
|
};
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export async function notionApiRequest(
|
|
|
|
this:
|
|
|
|
| IHookFunctions
|
|
|
|
| IExecuteFunctions
|
|
|
|
| IExecuteSingleFunctions
|
|
|
|
| ILoadOptionsFunctions
|
|
|
|
| IPollFunctions,
|
|
|
|
method: string,
|
|
|
|
resource: string,
|
2022-12-02 06:25:21 -08:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
body: any = {},
|
|
|
|
qs: IDataObject = {},
|
|
|
|
uri?: string,
|
|
|
|
option: IDataObject = {},
|
|
|
|
): Promise<any> {
|
2021-05-20 14:31:23 -07:00
|
|
|
try {
|
|
|
|
let options: OptionsWithUri = {
|
|
|
|
headers: {
|
2021-12-29 14:23:22 -08:00
|
|
|
'Notion-Version': apiVersion[this.getNode().typeVersion],
|
2021-05-20 14:31:23 -07:00
|
|
|
},
|
|
|
|
method,
|
|
|
|
qs,
|
|
|
|
body,
|
2023-01-19 04:37:19 -08:00
|
|
|
uri: uri || `https://api.notion.com/v1${resource}`,
|
2021-05-20 14:31:23 -07:00
|
|
|
json: true,
|
|
|
|
};
|
|
|
|
options = Object.assign({}, options, option);
|
2021-12-29 14:23:22 -08:00
|
|
|
if (Object.keys(body).length === 0) {
|
|
|
|
delete options.body;
|
|
|
|
}
|
2022-07-10 03:32:19 -07:00
|
|
|
if (!uri) {
|
2022-12-23 10:09:52 -08:00
|
|
|
return await this.helpers.requestWithAuthentication.call(this, 'notionApi', options);
|
2022-07-10 03:32:19 -07:00
|
|
|
}
|
2022-12-23 10:09:52 -08:00
|
|
|
return await this.helpers.request(options);
|
2021-05-20 14:31:23 -07:00
|
|
|
} catch (error) {
|
|
|
|
throw new NodeApiError(this.getNode(), error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export async function notionApiRequestAllItems(
|
|
|
|
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
|
|
|
propertyName: string,
|
|
|
|
method: string,
|
|
|
|
endpoint: string,
|
2022-12-02 06:25:21 -08:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
body: any = {},
|
|
|
|
query: IDataObject = {},
|
|
|
|
): Promise<any> {
|
2022-12-02 03:53:59 -08:00
|
|
|
const resource = this.getNodeParameter('resource', 0);
|
2021-06-22 14:59:04 -07:00
|
|
|
|
2021-05-20 14:31:23 -07:00
|
|
|
const returnData: IDataObject[] = [];
|
|
|
|
|
|
|
|
let responseData;
|
|
|
|
|
|
|
|
do {
|
|
|
|
responseData = await notionApiRequest.call(this, method, endpoint, body, query);
|
|
|
|
const { next_cursor } = responseData;
|
2021-06-22 14:59:04 -07:00
|
|
|
if (resource === 'block' || resource === 'user') {
|
2022-12-02 12:54:28 -08:00
|
|
|
query.start_cursor = next_cursor;
|
2021-06-22 14:59:04 -07:00
|
|
|
} else {
|
2022-12-02 12:54:28 -08:00
|
|
|
body.start_cursor = next_cursor;
|
2021-06-22 14:59:04 -07:00
|
|
|
}
|
2021-05-20 14:31:23 -07:00
|
|
|
returnData.push.apply(returnData, responseData[propertyName]);
|
|
|
|
if (query.limit && query.limit <= returnData.length) {
|
|
|
|
return returnData;
|
|
|
|
}
|
2022-08-17 08:50:24 -07:00
|
|
|
} while (responseData.has_more !== false);
|
2021-05-20 14:31:23 -07:00
|
|
|
|
|
|
|
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 {
|
2022-08-17 08:50:24 -07:00
|
|
|
title: [textContent(content)],
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function formatText(content: string) {
|
|
|
|
return {
|
2022-08-17 08:50:24 -07:00
|
|
|
text: [textContent(content)],
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
function getLink(text: { textLink: string; isLink: boolean }) {
|
2022-12-02 12:54:28 -08:00
|
|
|
if (text.isLink && text.textLink !== '') {
|
2021-05-20 14:31:23 -07:00
|
|
|
return {
|
|
|
|
link: {
|
|
|
|
url: text.textLink,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
],
|
|
|
|
) {
|
2021-05-20 14:31:23 -07:00
|
|
|
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,
|
2022-12-02 12:54:28 -08:00
|
|
|
[text.mentionType]: text.range
|
|
|
|
? { start: text.dateStart, end: text.dateEnd }
|
|
|
|
: { start: text.date, end: null },
|
2021-05-20 14:31:23 -07:00
|
|
|
},
|
|
|
|
annotations: text.annotationUi,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
results.push({
|
|
|
|
type: 'mention',
|
|
|
|
mention: {
|
|
|
|
type: text.mentionType,
|
2022-11-09 02:26:13 -08:00
|
|
|
//@ts-expect-error any
|
2021-05-20 14:31:23 -07:00
|
|
|
[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]: {
|
2022-08-17 08:50:24 -07:00
|
|
|
...(block.type === 'to_do' ? { checked: block.checked } : {}),
|
|
|
|
// prettier-ignore
|
2022-12-02 06:25:21 -08:00
|
|
|
|
2022-11-09 02:26:13 -08:00
|
|
|
text: (block.richText === false) ? formatText(block.textContent as string).text : getTexts((block.text as IDataObject).text as any || []),
|
2021-05-20 14:31:23 -07:00
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2023-01-13 09:11:56 -08:00
|
|
|
function getDateFormat(includeTime: boolean) {
|
|
|
|
if (!includeTime) {
|
|
|
|
return 'yyyy-MM-DD';
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2021-12-29 14:23:22 -08:00
|
|
|
function getPropertyKeyValue(value: any, type: string, timezone: string, version = 1) {
|
2022-08-17 08:50:24 -07:00
|
|
|
const ignoreIfEmpty = <T>(v: T, cb: (v: T) => any) =>
|
|
|
|
!v && value.ignoreIfEmpty ? undefined : cb(v);
|
2022-11-09 02:26:13 -08:00
|
|
|
let result: IDataObject = {};
|
2022-07-20 04:34:52 -07:00
|
|
|
|
2021-05-20 14:31:23 -07:00
|
|
|
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':
|
2022-08-17 08:50:24 -07:00
|
|
|
result = ignoreIfEmpty(value.urlValue, (url) => ({ type: 'url', url }));
|
2021-05-20 14:31:23 -07:00
|
|
|
break;
|
|
|
|
case 'checkbox':
|
|
|
|
result = { type: 'checkbox', checkbox: value.checkboxValue };
|
|
|
|
break;
|
|
|
|
case 'relation':
|
|
|
|
result = {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: 'relation',
|
2022-12-02 06:25:21 -08:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
relation: value.relationValue.reduce((acc: [], cur: any) => {
|
2022-02-24 14:27:06 -08:00
|
|
|
return acc.concat(cur.split(',').map((relation: string) => ({ id: relation.trim() })));
|
2021-05-20 14:31:23 -07:00
|
|
|
}, []),
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'multi_select':
|
2021-06-13 10:17:39 -07:00
|
|
|
const multiSelectValue = value.multiSelectValue;
|
2021-05-20 14:31:23 -07:00
|
|
|
result = {
|
2021-06-13 10:17:39 -07:00
|
|
|
type: 'multi_select',
|
2022-08-17 08:50:24 -07:00
|
|
|
multi_select: (Array.isArray(multiSelectValue)
|
|
|
|
? multiSelectValue
|
|
|
|
: multiSelectValue.split(',').map((v: string) => v.trim())
|
|
|
|
)
|
2022-12-02 12:54:28 -08:00
|
|
|
.filter((entry: any) => entry !== null)
|
2022-08-17 08:50:24 -07:00
|
|
|
.map((option: string) => (!uuidValidate(option) ? { name: option } : { id: option })),
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'email':
|
|
|
|
result = {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: 'email',
|
|
|
|
email: value.emailValue,
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'people':
|
2021-12-29 14:23:22 -08:00
|
|
|
//if expression it's a single value, make it an array
|
|
|
|
if (!Array.isArray(value.peopleValue)) {
|
|
|
|
value.peopleValue = [value.peopleValue];
|
|
|
|
}
|
|
|
|
|
2021-05-20 14:31:23 -07:00
|
|
|
result = {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: 'people',
|
|
|
|
people: value.peopleValue.map((option: string) => ({ id: option })),
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'phone_number':
|
|
|
|
result = {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: 'phone_number',
|
|
|
|
phone_number: value.phoneValue,
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'select':
|
|
|
|
result = {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: 'select',
|
|
|
|
select: version === 1 ? { id: value.selectValue } : { name: value.selectValue },
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
break;
|
2022-11-11 04:37:52 -08:00
|
|
|
case 'status':
|
|
|
|
result = {
|
|
|
|
type: 'status',
|
|
|
|
status: { name: value.statusValue },
|
|
|
|
};
|
|
|
|
break;
|
2021-05-20 14:31:23 -07:00
|
|
|
case 'date':
|
2021-06-12 09:06:47 -07:00
|
|
|
const format = getDateFormat(value.includeTime);
|
2022-08-17 08:50:24 -07:00
|
|
|
const timezoneValue = value.timezone === 'default' ? timezone : value.timezone;
|
2021-05-20 14:31:23 -07:00
|
|
|
if (value.range === true) {
|
|
|
|
result = {
|
2021-08-13 11:57:18 -07:00
|
|
|
type: 'date',
|
|
|
|
date: {
|
|
|
|
start: moment.tz(value.dateStart, timezoneValue).format(format),
|
|
|
|
end: moment.tz(value.dateEnd, timezoneValue).format(format),
|
|
|
|
},
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
result = {
|
2021-08-13 11:57:18 -07:00
|
|
|
type: 'date',
|
|
|
|
date: {
|
|
|
|
start: moment.tz(value.date, timezoneValue).format(format),
|
|
|
|
end: null,
|
|
|
|
},
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
}
|
2021-12-29 14:23:22 -08:00
|
|
|
|
|
|
|
//if the date was left empty, set it to null so it resets the value in notion
|
2022-08-17 08:50:24 -07:00
|
|
|
if (value.date === '' || (value.dateStart === '' && value.dateEnd === '')) {
|
2021-12-29 14:23:22 -08:00
|
|
|
result.date = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'files':
|
|
|
|
result = {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: 'files',
|
|
|
|
files: value.fileUrls.fileUrl.map((file: { name: string; url: string }) => ({
|
|
|
|
name: file.name,
|
|
|
|
type: 'external',
|
|
|
|
external: { url: file.url },
|
|
|
|
})),
|
2021-12-29 14:23:22 -08:00
|
|
|
};
|
2021-05-20 14:31:23 -07:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNameAndType(key: string) {
|
|
|
|
const [name, type] = key.split('|');
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
type,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-12-29 14:23:22 -08:00
|
|
|
export function mapProperties(properties: IDataObject[], timezone: string, version = 1) {
|
2022-07-20 04:34:52 -07:00
|
|
|
return properties
|
2022-08-17 08:50:24 -07:00
|
|
|
.filter(
|
|
|
|
(property): property is Record<string, { key: string; [k: string]: any }> =>
|
|
|
|
typeof property.key === 'string',
|
|
|
|
)
|
|
|
|
.map(
|
|
|
|
(property) =>
|
|
|
|
[
|
|
|
|
`${property.key.split('|')[0]}`,
|
|
|
|
getPropertyKeyValue(property, property.key.split('|')[1], timezone, version),
|
|
|
|
] as const,
|
|
|
|
)
|
2022-07-20 04:34:52 -07:00
|
|
|
.filter(([, value]) => value)
|
2022-08-17 08:50:24 -07:00
|
|
|
.reduce(
|
|
|
|
(obj, [key, value]) =>
|
|
|
|
Object.assign(obj, {
|
|
|
|
[key]: value,
|
|
|
|
}),
|
|
|
|
{},
|
|
|
|
);
|
2021-05-20 14:31:23 -07:00
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export function mapSorting(
|
|
|
|
data: [{ key: string; type: string; direction: string; timestamp: boolean }],
|
|
|
|
) {
|
2021-05-20 14:31:23 -07:00
|
|
|
return data.map((sort) => {
|
|
|
|
return {
|
|
|
|
direction: sort.direction,
|
2022-08-17 08:50:24 -07:00
|
|
|
[sort.timestamp ? 'timestamp' : 'property']: sort.key.split('|')[0],
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-12-02 12:54:28 -08:00
|
|
|
export function mapFilters(filtersList: IDataObject[], timezone: string) {
|
|
|
|
return filtersList.reduce((obj, value: { [key: string]: any }) => {
|
2021-05-20 14:31:23 -07:00
|
|
|
let key = getNameAndType(value.key).type;
|
2021-06-03 16:10:27 -07:00
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
let valuePropertyName =
|
|
|
|
key === 'last_edited_time' ? value[camelCase(key)] : value[`${camelCase(key)}Value`];
|
2021-06-03 16:10:27 -07:00
|
|
|
|
2021-05-20 14:31:23 -07:00
|
|
|
if (['is_empty', 'is_not_empty'].includes(value.condition as string)) {
|
|
|
|
valuePropertyName = true;
|
2022-08-17 08:50:24 -07:00
|
|
|
} else if (
|
|
|
|
['past_week', 'past_month', 'past_year', 'next_week', 'next_month', 'next_year'].includes(
|
|
|
|
value.condition as string,
|
|
|
|
)
|
|
|
|
) {
|
2021-05-20 14:31:23 -07:00
|
|
|
valuePropertyName = {};
|
|
|
|
}
|
2021-12-29 14:23:22 -08:00
|
|
|
if (key === 'rich_text' || key === 'text') {
|
2021-05-20 14:31:23 -07:00
|
|
|
key = 'text';
|
|
|
|
} else if (key === 'phone_number') {
|
|
|
|
key = 'phone';
|
2022-08-17 08:50:24 -07:00
|
|
|
} else if (
|
|
|
|
key === 'date' &&
|
|
|
|
!['is_empty', 'is_not_empty'].includes(value.condition as string)
|
|
|
|
) {
|
|
|
|
valuePropertyName = value.date === '' ? {} : moment.tz(value.date, timezone).utc().format();
|
2021-12-29 14:23:22 -08:00
|
|
|
} else if (key === 'boolean') {
|
|
|
|
key = 'checkbox';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value.type === 'formula') {
|
2022-12-02 12:54:28 -08:00
|
|
|
const vpropertyName = value[`${camelCase(value.returnType)}Value`];
|
2021-12-29 14:23:22 -08:00
|
|
|
|
|
|
|
return Object.assign(obj, {
|
|
|
|
['property']: getNameAndType(value.key).name,
|
2022-12-02 12:54:28 -08:00
|
|
|
[key]: { [value.returnType]: { [`${value.condition}`]: vpropertyName } },
|
2021-12-29 14:23:22 -08:00
|
|
|
});
|
2021-05-20 14:31:23 -07:00
|
|
|
}
|
2021-08-13 11:57:18 -07:00
|
|
|
|
2021-05-20 14:31:23 -07:00
|
|
|
return Object.assign(obj, {
|
|
|
|
['property']: getNameAndType(value.key).name,
|
|
|
|
[key]: { [`${value.condition}`]: valuePropertyName },
|
|
|
|
});
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
|
2022-02-24 04:47:47 -08:00
|
|
|
function simplifyProperty(property: any) {
|
|
|
|
let result: any;
|
|
|
|
const type = (property as IDataObject).type as string;
|
|
|
|
if (['text'].includes(property.type)) {
|
|
|
|
result = property.plain_text;
|
|
|
|
} else if (['rich_text', 'title'].includes(property.type)) {
|
|
|
|
if (Array.isArray(property[type]) && property[type].length !== 0) {
|
|
|
|
result = property[type].map((text: any) => simplifyProperty(text) as string).join('');
|
|
|
|
} else {
|
|
|
|
result = '';
|
|
|
|
}
|
2022-08-17 08:50:24 -07:00
|
|
|
} else if (
|
|
|
|
[
|
|
|
|
'url',
|
|
|
|
'created_time',
|
|
|
|
'checkbox',
|
|
|
|
'number',
|
|
|
|
'last_edited_time',
|
|
|
|
'email',
|
|
|
|
'phone_number',
|
|
|
|
'date',
|
|
|
|
].includes(property.type)
|
|
|
|
) {
|
2022-12-02 12:54:28 -08:00
|
|
|
result = property[type];
|
2022-02-24 04:47:47 -08:00
|
|
|
} else if (['created_by', 'last_edited_by', 'select'].includes(property.type)) {
|
2022-08-17 08:50:24 -07:00
|
|
|
result = property[type] ? property[type].name : null;
|
2022-02-24 04:47:47 -08:00
|
|
|
} else if (['people'].includes(property.type)) {
|
|
|
|
if (Array.isArray(property[type])) {
|
|
|
|
result = property[type].map((person: any) => person.person?.email || {});
|
|
|
|
} else {
|
|
|
|
result = property[type];
|
2021-05-20 14:31:23 -07:00
|
|
|
}
|
2022-02-24 04:47:47 -08:00
|
|
|
} else if (['multi_select'].includes(property.type)) {
|
|
|
|
if (Array.isArray(property[type])) {
|
2023-01-19 04:37:19 -08:00
|
|
|
result = property[type].map((e: IDataObject) => e.name || {});
|
2022-02-24 04:47:47 -08:00
|
|
|
} else {
|
2023-01-19 04:37:19 -08:00
|
|
|
result = property[type].options.map((e: IDataObject) => e.name || {});
|
2022-02-24 04:47:47 -08:00
|
|
|
}
|
|
|
|
} else if (['relation'].includes(property.type)) {
|
|
|
|
if (Array.isArray(property[type])) {
|
2023-01-19 04:37:19 -08:00
|
|
|
result = property[type].map((e: IDataObject) => e.id || {});
|
2022-02-24 04:47:47 -08:00
|
|
|
} else {
|
|
|
|
result = property[type].database_id;
|
|
|
|
}
|
|
|
|
} else if (['formula'].includes(property.type)) {
|
|
|
|
result = property[type][property[type].type];
|
|
|
|
} else if (['rollup'].includes(property.type)) {
|
|
|
|
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)] : elements;
|
|
|
|
}
|
|
|
|
} else if (['files'].includes(property.type)) {
|
2022-08-17 08:50:24 -07:00
|
|
|
result = property[type].map(
|
|
|
|
(file: { type: string; [key: string]: any }) => file[file.type].url,
|
|
|
|
);
|
2022-11-11 04:37:52 -08:00
|
|
|
} else if (['status'].includes(property.type)) {
|
|
|
|
result = property[type].name;
|
2022-02-24 04:47:47 -08:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function simplifyProperties(properties: any) {
|
|
|
|
const results: any = {};
|
|
|
|
for (const key of Object.keys(properties)) {
|
|
|
|
results[`${key}`] = simplifyProperty(properties[key]);
|
2021-05-20 14:31:23 -07:00
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2023-01-13 09:11:56 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-12-29 14:23:22 -08:00
|
|
|
export function simplifyObjects(objects: any, download = false, version = 2) {
|
2021-05-20 14:31:23 -07:00
|
|
|
if (!Array.isArray(objects)) {
|
|
|
|
objects = [objects];
|
|
|
|
}
|
|
|
|
const results: IDataObject[] = [];
|
2022-02-24 14:27:06 -08:00
|
|
|
for (const { object, id, properties, parent, title, json, binary, url } of objects) {
|
2021-05-20 14:31:23 -07:00
|
|
|
if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) {
|
|
|
|
results.push({
|
|
|
|
id,
|
2021-12-29 14:23:22 -08:00
|
|
|
name: properties.title.title[0].plain_text,
|
2022-08-17 08:50:24 -07:00
|
|
|
...(version === 2 ? { url } : {}),
|
2021-05-20 14:31:23 -07:00
|
|
|
});
|
|
|
|
} else if (object === 'page' && parent.type === 'database_id') {
|
|
|
|
results.push({
|
|
|
|
id,
|
2022-08-17 08:50:24 -07:00
|
|
|
...(version === 2 ? { name: getPropertyTitle(properties) } : {}),
|
|
|
|
...(version === 2 ? { url } : {}),
|
|
|
|
...(version === 2
|
|
|
|
? { ...prepend('property', simplifyProperties(properties)) }
|
|
|
|
: { ...simplifyProperties(properties) }),
|
2021-12-29 14:23:22 -08:00
|
|
|
});
|
|
|
|
} else if (download && json.object === 'page' && json.parent.type === 'database_id') {
|
|
|
|
results.push({
|
|
|
|
json: {
|
2022-02-24 14:27:06 -08:00
|
|
|
id: json.id,
|
2022-08-17 08:50:24 -07:00
|
|
|
...(version === 2 ? { name: getPropertyTitle(json.properties) } : {}),
|
|
|
|
...(version === 2 ? { url: json.url } : {}),
|
|
|
|
...(version === 2
|
|
|
|
? { ...prepend('property', simplifyProperties(json.properties)) }
|
|
|
|
: { ...simplifyProperties(json.properties) }),
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
binary,
|
2021-05-20 14:31:23 -07:00
|
|
|
});
|
|
|
|
} else if (object === 'database') {
|
|
|
|
results.push({
|
|
|
|
id,
|
2022-08-17 08:50:24 -07:00
|
|
|
...(version === 2
|
|
|
|
? { name: title[0]?.plain_text || '' }
|
|
|
|
: { title: title[0]?.plain_text || '' }),
|
|
|
|
...(version === 2 ? { url } : {}),
|
2021-05-20 14:31:23 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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',
|
2022-11-11 04:37:52 -08:00
|
|
|
status: 'status',
|
2021-05-20 14:31:23 -07:00
|
|
|
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',
|
|
|
|
],
|
2022-08-17 08:50:24 -07:00
|
|
|
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'],
|
2022-11-11 04:37:52 -08:00
|
|
|
status: ['equals', 'does_not_equal'],
|
2021-05-20 14:31:23 -07:00
|
|
|
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',
|
|
|
|
],
|
2022-08-17 08:50:24 -07:00
|
|
|
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'],
|
2021-12-29 14:23:22 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
const formula: { [key: string]: string[] } = {
|
2022-08-17 08:50:24 -07:00
|
|
|
text: [...typeConditions.rich_text],
|
|
|
|
checkbox: [...typeConditions.checkbox],
|
|
|
|
number: [...typeConditions.number],
|
|
|
|
date: [...typeConditions.date],
|
2021-05-20 14:31:23 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
for (const type of Object.keys(types)) {
|
2022-08-17 08:50:24 -07:00
|
|
|
elements.push({
|
|
|
|
displayName: 'Condition',
|
|
|
|
name: 'condition',
|
2021-12-29 14:23:22 -08:00
|
|
|
type: 'options',
|
|
|
|
displayOptions: {
|
|
|
|
show: {
|
2022-08-17 08:50:24 -07:00
|
|
|
type: [type],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
} as IDisplayOptions,
|
2022-12-02 12:54:28 -08:00
|
|
|
options: typeConditions[types[type]].map((entry: string) => ({
|
|
|
|
name: capitalCase(entry),
|
|
|
|
value: entry,
|
2022-08-17 08:50:24 -07:00
|
|
|
})),
|
2021-12-29 14:23:22 -08:00
|
|
|
default: '',
|
2022-08-17 08:50:24 -07:00
|
|
|
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);
|
2021-12-29 14:23:22 -08:00
|
|
|
|
|
|
|
for (const key of Object.keys(formula)) {
|
2022-08-17 08:50:24 -07:00
|
|
|
elements.push({
|
|
|
|
displayName: 'Condition',
|
|
|
|
name: 'condition',
|
|
|
|
type: 'options',
|
|
|
|
displayOptions: {
|
|
|
|
show: {
|
|
|
|
type: ['formula'],
|
|
|
|
returnType: [key],
|
|
|
|
},
|
|
|
|
} as IDisplayOptions,
|
2022-12-02 12:54:28 -08:00
|
|
|
options: formula[key].map((entry: string) => ({ name: capitalCase(entry), value: entry })),
|
2022-08-17 08:50:24 -07:00
|
|
|
default: '',
|
|
|
|
description: 'The value of the property to filter by',
|
|
|
|
} as INodeProperties);
|
2021-12-29 14:23:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return elements;
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
// prettier-ignore
|
2023-01-13 09:11:56 -08:00
|
|
|
export async function downloadFiles(this: IExecuteFunctions | IPollFunctions, records: [{ properties: { [key: string]: any | { id: string; type: string; files: [{ external: { url: string } } | { file: { url: string } }] } } }]): Promise<INodeExecutionData[]> {
|
2022-12-02 12:54:28 -08:00
|
|
|
|
2021-12-29 14:23:22 -08:00
|
|
|
const elements: INodeExecutionData[] = [];
|
|
|
|
for (const record of records) {
|
|
|
|
const element: INodeExecutionData = { json: {}, binary: {} };
|
|
|
|
element.json = record as unknown as IDataObject;
|
|
|
|
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()) {
|
2022-08-17 08:50:24 -07:00
|
|
|
const data = await notionApiRequest.call(
|
|
|
|
this,
|
|
|
|
'GET',
|
|
|
|
'',
|
|
|
|
{},
|
|
|
|
{},
|
|
|
|
file?.file?.url || file?.external?.url,
|
|
|
|
{ json: false, encoding: null },
|
|
|
|
);
|
2021-12-29 14:23:22 -08:00
|
|
|
element.binary![`${key}_${index}`] = await this.helpers.prepareBinaryData(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (Object.keys(element.binary as IBinaryKeyData).length === 0) {
|
|
|
|
delete element.binary;
|
|
|
|
}
|
|
|
|
elements.push(element);
|
|
|
|
}
|
2021-05-20 14:31:23 -07:00
|
|
|
return elements;
|
|
|
|
}
|
2021-12-29 14:23:22 -08:00
|
|
|
|
|
|
|
export function extractPageId(page: string) {
|
|
|
|
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: {
|
2022-08-17 08:50:24 -07:00
|
|
|
version: [2],
|
|
|
|
resource: [resource],
|
|
|
|
operation: ['getAll'],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
default: 'none',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
displayName: 'Must Match',
|
|
|
|
name: 'matchType',
|
|
|
|
type: 'options',
|
|
|
|
options: [
|
|
|
|
{
|
|
|
|
name: 'Any filter',
|
|
|
|
value: 'anyFilter',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'All Filters',
|
|
|
|
value: 'allFilters',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
displayOptions: {
|
|
|
|
show: {
|
2022-08-17 08:50:24 -07:00
|
|
|
version: [2],
|
|
|
|
resource: [resource],
|
|
|
|
operation: ['getAll'],
|
|
|
|
filterType: ['manual'],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
default: 'anyFilter',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
displayName: 'Filters',
|
|
|
|
name: 'filters',
|
|
|
|
type: 'fixedCollection',
|
|
|
|
typeOptions: {
|
|
|
|
multipleValues: true,
|
|
|
|
},
|
|
|
|
displayOptions: {
|
|
|
|
show: {
|
2022-08-17 08:50:24 -07:00
|
|
|
version: [2],
|
|
|
|
resource: [resource],
|
|
|
|
operation: ['getAll'],
|
|
|
|
filterType: ['manual'],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
},
|
2022-04-22 09:29:51 -07:00
|
|
|
default: {},
|
2021-12-29 14:23:22 -08:00
|
|
|
placeholder: 'Add Condition',
|
|
|
|
options: [
|
|
|
|
{
|
|
|
|
displayName: 'Conditions',
|
|
|
|
name: 'conditions',
|
2022-08-17 08:50:24 -07:00
|
|
|
values: [...filters(getConditions())],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
{
|
2022-08-17 08:50:24 -07:00
|
|
|
displayName:
|
|
|
|
'See <a href="https://developers.notion.com/reference/post-database-query#post-database-query-filter" target="_blank">Notion guide</a> to creating filters',
|
2021-12-29 14:23:22 -08:00
|
|
|
name: 'jsonNotice',
|
|
|
|
type: 'notice',
|
|
|
|
displayOptions: {
|
|
|
|
show: {
|
2022-08-17 08:50:24 -07:00
|
|
|
version: [2],
|
|
|
|
resource: [resource],
|
|
|
|
operation: ['getAll'],
|
|
|
|
filterType: ['json'],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
displayName: 'Filters (JSON)',
|
|
|
|
name: 'filterJson',
|
|
|
|
type: 'string',
|
|
|
|
displayOptions: {
|
|
|
|
show: {
|
2022-08-17 08:50:24 -07:00
|
|
|
version: [2],
|
|
|
|
resource: [resource],
|
|
|
|
operation: ['getAll'],
|
|
|
|
filterType: ['json'],
|
2021-12-29 14:23:22 -08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-08-17 08:50:24 -07:00
|
|
|
export function validateJSON(json: string | undefined): any {
|
2021-12-29 14:23:22 -08:00
|
|
|
let result;
|
|
|
|
try {
|
|
|
|
result = JSON.parse(json!);
|
|
|
|
} catch (exception) {
|
|
|
|
result = undefined;
|
|
|
|
}
|
|
|
|
return result;
|
2022-04-08 14:32:08 -07:00
|
|
|
}
|
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[]) {
|
2022-11-22 04:43:28 -08:00
|
|
|
blockValues.forEach((bv) => {
|
2022-11-11 04:37:52 -08:00
|
|
|
if (bv.richText && bv.text) {
|
2022-11-22 04:43:28 -08:00
|
|
|
const texts = (
|
|
|
|
bv.text as {
|
|
|
|
text: [
|
|
|
|
{
|
|
|
|
textType: string;
|
|
|
|
mentionType: string;
|
|
|
|
database: string | { value: string; mode: string; __rl: boolean; __regex: string };
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
).text;
|
|
|
|
texts.forEach((txt) => {
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|