Browse Source

Merge pull request #7438 from nocodb/nc-fix/webhook-filters

Nc fix/webhook filters
pull/7443/head
Raju Udava 10 months ago committed by GitHub
parent
commit
0c37f1d4be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 357
      packages/nocodb/src/helpers/webhookHelpers.ts

357
packages/nocodb/src/helpers/webhookHelpers.ts

@ -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;

Loading…
Cancel
Save