mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-02 02:57:23 +00:00
fix datetime gb eq to use minute scale
This commit is contained in:
@@ -135,7 +135,525 @@ export class DateTimeGeneralHandler extends GenericFieldHandler {
|
||||
};
|
||||
}): Promise<{ value: any }> {
|
||||
return {
|
||||
value: this.parseDateTime(params).value.format('YYYY-MM-DD HH:mm:ssZ'),
|
||||
value: this.parseDateTime(params).value?.format('YYYY-MM-DD HH:mm:ssZ'),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTimezone(
|
||||
_knex: CustomKnex,
|
||||
filter: Filter,
|
||||
column: Column,
|
||||
options: FilterOptions,
|
||||
) {
|
||||
const { context } = options;
|
||||
|
||||
return getNodejsTimezone(
|
||||
parseProp(filter.meta).timezone,
|
||||
parseProp(column.meta).timezone,
|
||||
context.timezone,
|
||||
);
|
||||
}
|
||||
|
||||
protected parseFilterValue(
|
||||
value: string,
|
||||
_knex: CustomKnex,
|
||||
filter: Filter,
|
||||
column: Column,
|
||||
options: FilterOptions,
|
||||
) {
|
||||
// if the time provided has timezone, return as is
|
||||
if (isDateTimeStringHasTimezone(value)) {
|
||||
return dayjs(value).tz(this.getTimezone(_knex, filter, column, options));
|
||||
}
|
||||
// assume local
|
||||
else {
|
||||
return dayjs.tz(value, this.getTimezone(_knex, filter, column, options));
|
||||
}
|
||||
}
|
||||
|
||||
protected getNow(
|
||||
_knex: CustomKnex,
|
||||
filter: Filter & { groupby?: boolean },
|
||||
column: Column,
|
||||
options: FilterOptions,
|
||||
) {
|
||||
const now = dayjs.tz(
|
||||
new Date(),
|
||||
this.getTimezone(_knex, filter, column, options),
|
||||
);
|
||||
if (filter.groupby) {
|
||||
return now.startOf('minute');
|
||||
}
|
||||
// the val will be start of day in timezone
|
||||
return now.startOf('day');
|
||||
}
|
||||
|
||||
protected comparisonBetween(
|
||||
{
|
||||
sourceField,
|
||||
anchorDate,
|
||||
rangeDate,
|
||||
qb,
|
||||
}: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
anchorDate: dayjs.Dayjs;
|
||||
rangeDate: dayjs.Dayjs;
|
||||
qb: Knex.QueryBuilder;
|
||||
},
|
||||
{ knex }: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
) {
|
||||
qb.where(
|
||||
knex.raw('?? between ? and ?', [
|
||||
sourceField,
|
||||
anchorDate.utc().format(this.dateValueFormat),
|
||||
rangeDate.utc().format(this.dateValueFormat),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
protected comparisonOp(
|
||||
{
|
||||
sourceField,
|
||||
val,
|
||||
qb,
|
||||
comparisonOp,
|
||||
}: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: dayjs.Dayjs;
|
||||
qb: Knex.QueryBuilder;
|
||||
comparisonOp: '<' | '<=' | '>' | '>=';
|
||||
},
|
||||
{ knex }: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
) {
|
||||
qb.where(
|
||||
knex.raw(`?? ${comparisonOp} ?`, [
|
||||
sourceField,
|
||||
val.utc().format(this.dateValueFormat),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
override async filter(
|
||||
knex: CustomKnex,
|
||||
filter: Filter & { groupby?: boolean },
|
||||
column: Column,
|
||||
options: FilterOptions,
|
||||
) {
|
||||
const { alias } = options;
|
||||
const field =
|
||||
options.customWhereClause ??
|
||||
(alias ? `${alias}.${column.column_name}` : column.column_name);
|
||||
|
||||
const now = this.getNow(knex, filter, column, options);
|
||||
let anchorDate: dayjs.Dayjs;
|
||||
const emptyResult = { clause: () => {} } as FilterOperationResult;
|
||||
|
||||
// handle sub operation
|
||||
switch (filter.comparison_sub_op) {
|
||||
case 'today':
|
||||
anchorDate = now;
|
||||
break;
|
||||
case 'tomorrow':
|
||||
anchorDate = now.add(1, 'day');
|
||||
break;
|
||||
case 'yesterday':
|
||||
anchorDate = now.add(-1, 'day');
|
||||
break;
|
||||
case 'oneWeekAgo':
|
||||
anchorDate = now.add(-1, 'week');
|
||||
break;
|
||||
case 'oneWeekFromNow':
|
||||
anchorDate = now.add(1, 'week');
|
||||
break;
|
||||
case 'oneMonthAgo':
|
||||
anchorDate = now.add(-1, 'month');
|
||||
break;
|
||||
case 'oneMonthFromNow':
|
||||
anchorDate = now.add(1, 'month');
|
||||
break;
|
||||
case 'daysAgo':
|
||||
if (!filter.value) return emptyResult;
|
||||
anchorDate = now.add(-filter.value, 'day');
|
||||
break;
|
||||
case 'daysFromNow':
|
||||
if (!filter.value) return emptyResult;
|
||||
anchorDate = now.add(Number(filter.value), 'day');
|
||||
break;
|
||||
case 'exactDate':
|
||||
if (!filter.value) return emptyResult;
|
||||
anchorDate = this.parseFilterValue(
|
||||
filter.value,
|
||||
knex,
|
||||
filter,
|
||||
column,
|
||||
options,
|
||||
);
|
||||
anchorDate = filter.groupby ? anchorDate : anchorDate.startOf('day');
|
||||
break;
|
||||
// sub-ops for `isWithin` comparison
|
||||
case 'pastWeek':
|
||||
anchorDate = now.add(-1, 'week');
|
||||
break;
|
||||
case 'pastMonth':
|
||||
anchorDate = now.add(-1, 'month');
|
||||
break;
|
||||
case 'pastYear':
|
||||
anchorDate = now.add(-1, 'year');
|
||||
break;
|
||||
case 'nextWeek':
|
||||
anchorDate = now.add(1, 'week');
|
||||
break;
|
||||
case 'nextMonth':
|
||||
anchorDate = now.add(1, 'month');
|
||||
break;
|
||||
case 'nextYear':
|
||||
anchorDate = now.add(1, 'year');
|
||||
break;
|
||||
case 'pastNumberOfDays':
|
||||
if (!filter.value) return emptyResult;
|
||||
anchorDate = now.add(-filter.value, 'day');
|
||||
break;
|
||||
case 'nextNumberOfDays':
|
||||
if (!filter.value) return emptyResult;
|
||||
anchorDate = now.add(Number(filter.value), 'day');
|
||||
break;
|
||||
}
|
||||
// for straight date value without sub op
|
||||
if (!filter.comparison_sub_op && filter.value) {
|
||||
anchorDate = this.parseFilterValue(
|
||||
filter.value,
|
||||
knex,
|
||||
filter,
|
||||
column,
|
||||
options,
|
||||
);
|
||||
anchorDate = filter.groupby ? anchorDate : anchorDate.startOf('day');
|
||||
if (!anchorDate.isValid()) {
|
||||
return emptyResult;
|
||||
}
|
||||
}
|
||||
if (filter.comparison_op === 'isWithin') {
|
||||
return await this.filterIsWithin(
|
||||
{ val: anchorDate.valueOf(), sourceField: field },
|
||||
{ knex, filter, column },
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.handleFilter(
|
||||
{ val: anchorDate?.valueOf(), sourceField: field },
|
||||
{ knex, filter, column },
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
override async filterEq(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: {
|
||||
knex: CustomKnex;
|
||||
filter: Filter & { groupby?: boolean };
|
||||
column: Column;
|
||||
},
|
||||
_options: FilterOptions,
|
||||
) {
|
||||
const { knex, filter, column } = rootArgs;
|
||||
const anchorDate = dayjs.tz(
|
||||
args.val,
|
||||
this.getTimezone(knex, filter, column, _options),
|
||||
);
|
||||
const rangeDate = filter.groupby
|
||||
? anchorDate.add(1, 'minute').add(-1, 'milliseconds')
|
||||
: anchorDate.add(24, 'hours').add(-1, 'milliseconds');
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonBetween(
|
||||
{ ...args, anchorDate, rangeDate, qb: nestedQb },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterNeq(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
) {
|
||||
const anchorDate = dayjs(args.val);
|
||||
const rangeDate = anchorDate.add(24, 'hours').add(-1, 'milliseconds');
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
// is earlier than anchor date
|
||||
// or later than range date
|
||||
// or null
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonOp(
|
||||
{ ...args, val: anchorDate, qb: nestedQb, comparisonOp: '<' },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
nestedQb.orWhere((nestedQb2) => {
|
||||
this.comparisonOp(
|
||||
{ ...args, val: rangeDate, qb: nestedQb2, comparisonOp: '>' },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
nestedQb.orWhereNull(args.sourceField as any);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterGt(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
): Promise<{ rootApply: any; clause: (qb: Knex.QueryBuilder) => void }> {
|
||||
const anchorDate = dayjs(args.val);
|
||||
let rangeDate = anchorDate.add(24, 'hours').add(-1, 'milliseconds');
|
||||
|
||||
// when the given filter value has time component,
|
||||
// we use it raw as comparison
|
||||
if (rootArgs.filter.value?.replace('T', ' ').split(' ')[1]) {
|
||||
rangeDate = this.parseFilterValue(
|
||||
rootArgs.filter.value,
|
||||
rootArgs.knex,
|
||||
rootArgs.filter,
|
||||
rootArgs.column,
|
||||
_options,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonOp(
|
||||
{ ...args, val: rangeDate, qb: nestedQb, comparisonOp: '>' },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterGte(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
): Promise<{ rootApply: any; clause: (qb: Knex.QueryBuilder) => void }> {
|
||||
const anchorDate = dayjs(args.val);
|
||||
let rangeDate = anchorDate;
|
||||
|
||||
// when the given filter value has time component,
|
||||
// we use it raw as comparison
|
||||
if (rootArgs.filter.value?.replace('T', ' ').split(' ')[1]) {
|
||||
rangeDate = this.parseFilterValue(
|
||||
rootArgs.filter.value,
|
||||
rootArgs.knex,
|
||||
rootArgs.filter,
|
||||
rootArgs.column,
|
||||
_options,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonOp(
|
||||
{ ...args, val: rangeDate, qb: nestedQb, comparisonOp: '>=' },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterLte(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
): Promise<{ rootApply: any; clause: (qb: Knex.QueryBuilder) => void }> {
|
||||
const anchorDate = dayjs(args.val);
|
||||
let rangeDate = anchorDate.add(24, 'hours').add(-1, 'milliseconds');
|
||||
|
||||
// when the given filter value has time component,
|
||||
// we use it raw as comparison
|
||||
if (rootArgs.filter.value?.replace('T', ' ').split(' ')[1]) {
|
||||
rangeDate = this.parseFilterValue(
|
||||
rootArgs.filter.value,
|
||||
rootArgs.knex,
|
||||
rootArgs.filter,
|
||||
rootArgs.column,
|
||||
_options,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonOp(
|
||||
{ ...args, val: rangeDate, qb: nestedQb, comparisonOp: '<=' },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterLt(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
): Promise<{ rootApply: any; clause: (qb: Knex.QueryBuilder) => void }> {
|
||||
const anchorDate = dayjs(args.val);
|
||||
let rangeDate = anchorDate;
|
||||
|
||||
// when the given filter value has time component,
|
||||
// we use it raw as comparison
|
||||
if (rootArgs.filter.value?.replace('T', ' ').split(' ')[1]) {
|
||||
rangeDate = this.parseFilterValue(
|
||||
rootArgs.filter.value,
|
||||
rootArgs.knex,
|
||||
rootArgs.filter,
|
||||
rootArgs.column,
|
||||
_options,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonOp(
|
||||
{ ...args, val: rangeDate, qb: nestedQb, comparisonOp: '<' },
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterBlank(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
_rootArgs: {
|
||||
knex: CustomKnex;
|
||||
filter: Filter;
|
||||
column: Column;
|
||||
},
|
||||
_options: FilterOptions,
|
||||
) {
|
||||
const { sourceField } = args;
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
nestedQb.whereNull(sourceField as any);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async filterNotblank(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
_rootArgs: {
|
||||
knex: CustomKnex;
|
||||
filter: Filter;
|
||||
column: Column;
|
||||
},
|
||||
_options: FilterOptions,
|
||||
) {
|
||||
const { sourceField } = args;
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
nestedQb.whereNotNull(sourceField as any);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async filterIsWithin(
|
||||
args: {
|
||||
sourceField: string | Knex.QueryBuilder | Knex.RawBuilder;
|
||||
val: any;
|
||||
},
|
||||
rootArgs: { knex: CustomKnex; filter: Filter; column: Column },
|
||||
_options: FilterOptions,
|
||||
): Promise<{ rootApply: any; clause: (qb: Knex.QueryBuilder) => void }> {
|
||||
const { knex, filter, column } = rootArgs;
|
||||
const anchorDate = dayjs(args.val);
|
||||
const now = this.getNow(knex, filter, column, _options);
|
||||
let firstArg: dayjs.Dayjs;
|
||||
let secondArg: dayjs.Dayjs;
|
||||
if (now.isBefore(anchorDate)) {
|
||||
firstArg = now.startOf('day');
|
||||
secondArg = anchorDate.add(24, 'hours').add(-1, 'millisecond');
|
||||
} else {
|
||||
firstArg = anchorDate;
|
||||
secondArg = now.startOf('day').add(24, 'hours').add(-1, 'millisecond');
|
||||
}
|
||||
|
||||
return {
|
||||
rootApply: undefined,
|
||||
clause: (qb: Knex.QueryBuilder) => {
|
||||
qb.where((nestedQb) => {
|
||||
this.comparisonBetween(
|
||||
{
|
||||
...args,
|
||||
anchorDate: firstArg,
|
||||
rangeDate: secondArg,
|
||||
qb: nestedQb,
|
||||
},
|
||||
rootArgs,
|
||||
_options,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +611,25 @@ describe('dataApiV3', () => {
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it('Date based- Group by eq', async function () {
|
||||
// list 10 records
|
||||
const rsp = await ncAxiosGet({
|
||||
url: `${urlPrefix}/${table.id}/records`,
|
||||
query: {
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
const record0 = rsp.body.records[0];
|
||||
// will filter record per minute scale
|
||||
const filteredRsp = await ncAxiosGet({
|
||||
url: `${urlPrefix}/${table.id}/records`,
|
||||
query: {
|
||||
filter: `(DateTime,gb_eq,exactDate,"${record0.fields.DateTime}")`,
|
||||
},
|
||||
});
|
||||
expect(filteredRsp.body.records.length).to.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Link based', () => {
|
||||
|
||||
Reference in New Issue
Block a user