|
|
|
@ -3,10 +3,19 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
import axios from 'axios'; |
|
|
|
|
import { useAgent } from 'request-filtering-agent'; |
|
|
|
|
import { Logger } from '@nestjs/common'; |
|
|
|
|
import dayjs from 'dayjs'; |
|
|
|
|
import { isDateMonthFormat, UITypes } from 'nocodb-sdk'; |
|
|
|
|
import isBetween from 'dayjs/plugin/isBetween'; |
|
|
|
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; |
|
|
|
|
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; |
|
|
|
|
import NcPluginMgrv2 from './NcPluginMgrv2'; |
|
|
|
|
import type { Column, FormView, Hook, Model, View } from '~/models'; |
|
|
|
|
import type { HookLogType } from 'nocodb-sdk'; |
|
|
|
|
import { Filter, HookLog } from '~/models'; |
|
|
|
|
import type { Column, FormView, Hook, Model, View } from '~/models'; |
|
|
|
|
import { Filter, HookLog, Source } from '~/models'; |
|
|
|
|
|
|
|
|
|
dayjs.extend(isBetween); |
|
|
|
|
dayjs.extend(isSameOrBefore); |
|
|
|
|
dayjs.extend(isSameOrAfter); |
|
|
|
|
|
|
|
|
|
Handlebars.registerHelper('json', function (context) { |
|
|
|
|
return JSON.stringify(context); |
|
|
|
@ -24,118 +33,275 @@ export function parseBody(template: string, data: any): string {
|
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function validateCondition(filters: Filter[], data: any) { |
|
|
|
|
export async function validateCondition( |
|
|
|
|
filters: Filter[], |
|
|
|
|
data: any, |
|
|
|
|
{ |
|
|
|
|
client, |
|
|
|
|
}: { |
|
|
|
|
client: string; |
|
|
|
|
}, |
|
|
|
|
) { |
|
|
|
|
if (!filters.length) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let isValid = true; |
|
|
|
|
let isValid = null; |
|
|
|
|
for (const _filter of filters) { |
|
|
|
|
const filter = _filter instanceof Filter ? _filter : new Filter(_filter); |
|
|
|
|
let res; |
|
|
|
|
const field = await filter.getColumn().then((c) => c.title); |
|
|
|
|
const column = await filter.getColumn(); |
|
|
|
|
const field = column.title; |
|
|
|
|
let val = data[field]; |
|
|
|
|
switch (typeof filter.value) { |
|
|
|
|
case 'boolean': |
|
|
|
|
val = !!data[field]; |
|
|
|
|
break; |
|
|
|
|
case 'number': |
|
|
|
|
val = +data[field]; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
switch (filter.comparison_op) { |
|
|
|
|
case 'eq': |
|
|
|
|
res = val == filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'neq': |
|
|
|
|
res = val != filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'like': |
|
|
|
|
res = |
|
|
|
|
data[field]?.toLowerCase()?.indexOf(filter.value?.toLowerCase()) > -1; |
|
|
|
|
break; |
|
|
|
|
case 'nlike': |
|
|
|
|
res = |
|
|
|
|
data[field]?.toLowerCase()?.indexOf(filter.value?.toLowerCase()) === |
|
|
|
|
-1; |
|
|
|
|
break; |
|
|
|
|
case 'empty': |
|
|
|
|
case 'blank': |
|
|
|
|
res = |
|
|
|
|
data[field] === '' || |
|
|
|
|
data[field] === null || |
|
|
|
|
data[field] === undefined; |
|
|
|
|
break; |
|
|
|
|
case 'notempty': |
|
|
|
|
case 'notblank': |
|
|
|
|
res = !( |
|
|
|
|
data[field] === '' || |
|
|
|
|
data[field] === null || |
|
|
|
|
data[field] === undefined |
|
|
|
|
); |
|
|
|
|
break; |
|
|
|
|
case 'checked': |
|
|
|
|
res = !!data[field]; |
|
|
|
|
break; |
|
|
|
|
case 'notchecked': |
|
|
|
|
res = !data[field]; |
|
|
|
|
break; |
|
|
|
|
case 'null': |
|
|
|
|
res = res = data[field] === null; |
|
|
|
|
break; |
|
|
|
|
case 'notnull': |
|
|
|
|
res = data[field] !== null; |
|
|
|
|
break; |
|
|
|
|
case 'allof': |
|
|
|
|
res = (filter.value?.split(',').map((item) => item.trim()) ?? []).every( |
|
|
|
|
(item) => (data[field]?.split(',') ?? []).includes(item), |
|
|
|
|
); |
|
|
|
|
break; |
|
|
|
|
case 'anyof': |
|
|
|
|
res = (filter.value?.split(',').map((item) => item.trim()) ?? []).some( |
|
|
|
|
(item) => (data[field]?.split(',') ?? []).includes(item), |
|
|
|
|
); |
|
|
|
|
break; |
|
|
|
|
case 'nallof': |
|
|
|
|
res = !( |
|
|
|
|
filter.value?.split(',').map((item) => item.trim()) ?? [] |
|
|
|
|
).every((item) => (data[field]?.split(',') ?? []).includes(item)); |
|
|
|
|
break; |
|
|
|
|
case 'nanyof': |
|
|
|
|
res = !(filter.value?.split(',').map((item) => item.trim()) ?? []).some( |
|
|
|
|
(item) => (data[field]?.split(',') ?? []).includes(item), |
|
|
|
|
); |
|
|
|
|
break; |
|
|
|
|
case 'lt': |
|
|
|
|
res = +data[field] < +filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'lte': |
|
|
|
|
case 'le': |
|
|
|
|
res = +data[field] <= +filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'gt': |
|
|
|
|
res = +data[field] > +filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'gte': |
|
|
|
|
case 'ge': |
|
|
|
|
res = +data[field] >= +filter.value; |
|
|
|
|
break; |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
[ |
|
|
|
|
UITypes.Date, |
|
|
|
|
UITypes.DateTime, |
|
|
|
|
UITypes.CreatedTime, |
|
|
|
|
UITypes.LastModifiedTime, |
|
|
|
|
].includes(column.uidt) && |
|
|
|
|
!['empty', 'blank', 'notempty', 'notblank'].includes(filter.comparison_op) |
|
|
|
|
) { |
|
|
|
|
const dateFormat = |
|
|
|
|
client === 'mysql2' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'; |
|
|
|
|
|
|
|
|
|
let now = dayjs(new Date()); |
|
|
|
|
const dateFormatFromMeta = column?.meta?.date_format; |
|
|
|
|
const dataVal: any = val; |
|
|
|
|
let filterVal: any = filter.value; |
|
|
|
|
if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) { |
|
|
|
|
// reset to 1st
|
|
|
|
|
now = dayjs(now).date(1); |
|
|
|
|
if (val) val = dayjs(val).date(1); |
|
|
|
|
} |
|
|
|
|
if (filterVal) res = dayjs(filterVal).isSame(dataVal, 'day'); |
|
|
|
|
|
|
|
|
|
// handle sub operation
|
|
|
|
|
switch (filter.comparison_sub_op) { |
|
|
|
|
case 'today': |
|
|
|
|
filterVal = now; |
|
|
|
|
break; |
|
|
|
|
case 'tomorrow': |
|
|
|
|
filterVal = now.add(1, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'yesterday': |
|
|
|
|
filterVal = now.add(-1, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'oneWeekAgo': |
|
|
|
|
filterVal = now.add(-1, 'week'); |
|
|
|
|
break; |
|
|
|
|
case 'oneWeekFromNow': |
|
|
|
|
filterVal = now.add(1, 'week'); |
|
|
|
|
break; |
|
|
|
|
case 'oneMonthAgo': |
|
|
|
|
filterVal = now.add(-1, 'month'); |
|
|
|
|
break; |
|
|
|
|
case 'oneMonthFromNow': |
|
|
|
|
filterVal = now.add(1, 'month'); |
|
|
|
|
break; |
|
|
|
|
case 'daysAgo': |
|
|
|
|
if (!filterVal) return; |
|
|
|
|
filterVal = now.add(-filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'daysFromNow': |
|
|
|
|
if (!filterVal) return; |
|
|
|
|
filterVal = now.add(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'exactDate': |
|
|
|
|
if (!filterVal) return; |
|
|
|
|
break; |
|
|
|
|
// sub-ops for `isWithin` comparison
|
|
|
|
|
case 'pastWeek': |
|
|
|
|
filterVal = now.add(-1, 'week'); |
|
|
|
|
break; |
|
|
|
|
case 'pastMonth': |
|
|
|
|
filterVal = now.add(-1, 'month'); |
|
|
|
|
break; |
|
|
|
|
case 'pastYear': |
|
|
|
|
filterVal = now.add(-1, 'year'); |
|
|
|
|
break; |
|
|
|
|
case 'nextWeek': |
|
|
|
|
filterVal = now.add(1, 'week'); |
|
|
|
|
break; |
|
|
|
|
case 'nextMonth': |
|
|
|
|
filterVal = now.add(1, 'month'); |
|
|
|
|
break; |
|
|
|
|
case 'nextYear': |
|
|
|
|
filterVal = now.add(1, 'year'); |
|
|
|
|
break; |
|
|
|
|
case 'pastNumberOfDays': |
|
|
|
|
if (!filterVal) return; |
|
|
|
|
filterVal = now.add(-filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'nextNumberOfDays': |
|
|
|
|
if (!filterVal) return; |
|
|
|
|
filterVal = now.add(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (dataVal) { |
|
|
|
|
switch (filter.comparison_op) { |
|
|
|
|
case 'eq': |
|
|
|
|
res = dayjs(dataVal).isSame(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'neq': |
|
|
|
|
res = !dayjs(dataVal).isSame(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'gt': |
|
|
|
|
res = dayjs(dataVal).isAfter(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'lt': |
|
|
|
|
res = dayjs(dataVal).isBefore(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'lte': |
|
|
|
|
case 'le': |
|
|
|
|
res = dayjs(dataVal).isSameOrBefore(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'gte': |
|
|
|
|
case 'ge': |
|
|
|
|
res = dayjs(dataVal).isSameOrAfter(filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'empty': |
|
|
|
|
case 'blank': |
|
|
|
|
res = dataVal === '' || dataVal === null || dataVal === undefined; |
|
|
|
|
break; |
|
|
|
|
case 'notempty': |
|
|
|
|
case 'notblank': |
|
|
|
|
res = !( |
|
|
|
|
dataVal === '' || |
|
|
|
|
dataVal === null || |
|
|
|
|
dataVal === undefined |
|
|
|
|
); |
|
|
|
|
break; |
|
|
|
|
case 'isWithin': { |
|
|
|
|
let now = dayjs(new Date()).format(dateFormat).toString(); |
|
|
|
|
now = column.uidt === UITypes.Date ? now.substring(0, 10) : now; |
|
|
|
|
switch (filter.comparison_sub_op) { |
|
|
|
|
case 'pastWeek': |
|
|
|
|
case 'pastMonth': |
|
|
|
|
case 'pastYear': |
|
|
|
|
case 'pastNumberOfDays': |
|
|
|
|
res = dayjs(dataVal).isBetween(filterVal, now, 'day'); |
|
|
|
|
break; |
|
|
|
|
case 'nextWeek': |
|
|
|
|
case 'nextMonth': |
|
|
|
|
case 'nextYear': |
|
|
|
|
case 'nextNumberOfDays': |
|
|
|
|
res = dayjs(dataVal).isBetween(now, filterVal, 'day'); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
switch (typeof filter.value) { |
|
|
|
|
case 'boolean': |
|
|
|
|
val = !!data[field]; |
|
|
|
|
break; |
|
|
|
|
case 'number': |
|
|
|
|
val = +data[field]; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
switch (filter.comparison_op) { |
|
|
|
|
case 'eq': |
|
|
|
|
res = val == filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'neq': |
|
|
|
|
res = val != filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'like': |
|
|
|
|
res = |
|
|
|
|
data[field] |
|
|
|
|
?.toString?.() |
|
|
|
|
?.toLowerCase() |
|
|
|
|
?.indexOf(filter.value?.toLowerCase()) > -1; |
|
|
|
|
break; |
|
|
|
|
case 'nlike': |
|
|
|
|
res = |
|
|
|
|
data[field] |
|
|
|
|
?.toString?.() |
|
|
|
|
?.toLowerCase() |
|
|
|
|
?.indexOf(filter.value?.toLowerCase()) === -1; |
|
|
|
|
break; |
|
|
|
|
case 'empty': |
|
|
|
|
case 'blank': |
|
|
|
|
res = |
|
|
|
|
data[field] === '' || |
|
|
|
|
data[field] === null || |
|
|
|
|
data[field] === undefined; |
|
|
|
|
break; |
|
|
|
|
case 'notempty': |
|
|
|
|
case 'notblank': |
|
|
|
|
res = !( |
|
|
|
|
data[field] === '' || |
|
|
|
|
data[field] === null || |
|
|
|
|
data[field] === undefined |
|
|
|
|
); |
|
|
|
|
break; |
|
|
|
|
case 'checked': |
|
|
|
|
res = !!data[field]; |
|
|
|
|
break; |
|
|
|
|
case 'notchecked': |
|
|
|
|
res = !data[field]; |
|
|
|
|
break; |
|
|
|
|
case 'null': |
|
|
|
|
res = res = data[field] === null; |
|
|
|
|
break; |
|
|
|
|
case 'notnull': |
|
|
|
|
res = data[field] !== null; |
|
|
|
|
break; |
|
|
|
|
case 'allof': |
|
|
|
|
res = ( |
|
|
|
|
filter.value?.split(',').map((item) => item.trim()) ?? [] |
|
|
|
|
).every((item) => (data[field]?.split(',') ?? []).includes(item)); |
|
|
|
|
break; |
|
|
|
|
case 'anyof': |
|
|
|
|
res = ( |
|
|
|
|
filter.value?.split(',').map((item) => item.trim()) ?? [] |
|
|
|
|
).some((item) => (data[field]?.split(',') ?? []).includes(item)); |
|
|
|
|
break; |
|
|
|
|
case 'nallof': |
|
|
|
|
res = !( |
|
|
|
|
filter.value?.split(',').map((item) => item.trim()) ?? [] |
|
|
|
|
).every((item) => (data[field]?.split(',') ?? []).includes(item)); |
|
|
|
|
break; |
|
|
|
|
case 'nanyof': |
|
|
|
|
res = !( |
|
|
|
|
filter.value?.split(',').map((item) => item.trim()) ?? [] |
|
|
|
|
).some((item) => (data[field]?.split(',') ?? []).includes(item)); |
|
|
|
|
break; |
|
|
|
|
case 'lt': |
|
|
|
|
res = +data[field] < +filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'lte': |
|
|
|
|
case 'le': |
|
|
|
|
res = +data[field] <= +filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'gt': |
|
|
|
|
res = +data[field] > +filter.value; |
|
|
|
|
break; |
|
|
|
|
case 'gte': |
|
|
|
|
case 'ge': |
|
|
|
|
res = +data[field] >= +filter.value; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
switch (filter.logical_op) { |
|
|
|
|
case 'or': |
|
|
|
|
isValid = isValid || res; |
|
|
|
|
isValid = isValid || !!res; |
|
|
|
|
break; |
|
|
|
|
case 'not': |
|
|
|
|
isValid = isValid && !res; |
|
|
|
|
break; |
|
|
|
|
case 'and': |
|
|
|
|
default: |
|
|
|
|
isValid = isValid && res; |
|
|
|
|
isValid = (isValid ?? true) && res; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return isValid; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -286,6 +452,7 @@ export async function invokeWebhook(
|
|
|
|
|
) { |
|
|
|
|
let hookLog: HookLogType; |
|
|
|
|
const startTime = process.hrtime(); |
|
|
|
|
const source = await Source.get(model.source_id); |
|
|
|
|
let notification; |
|
|
|
|
try { |
|
|
|
|
notification = |
|
|
|
@ -325,6 +492,7 @@ export async function invokeWebhook(
|
|
|
|
|
await validateCondition( |
|
|
|
|
testFilters || (await hook.getFilters()), |
|
|
|
|
data, |
|
|
|
|
{ client: source?.type }, |
|
|
|
|
) |
|
|
|
|
) { |
|
|
|
|
filteredData.push(data); |
|
|
|
@ -340,7 +508,7 @@ export async function invokeWebhook(
|
|
|
|
|
if ( |
|
|
|
|
prevData && |
|
|
|
|
filters.length && |
|
|
|
|
(await validateCondition(filters, prevData)) |
|
|
|
|
(await validateCondition(filters, prevData, { client: source?.type })) |
|
|
|
|
) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
@ -348,6 +516,7 @@ export async function invokeWebhook(
|
|
|
|
|
!(await validateCondition( |
|
|
|
|
testFilters || (await hook.getFilters()), |
|
|
|
|
newData, |
|
|
|
|
{ client: source?.type }, |
|
|
|
|
)) |
|
|
|
|
) { |
|
|
|
|
return; |
|
|
|
|