Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cubejs-api-gateway/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@ const base = require('../../jest.base.config');
module.exports = {
...base,
rootDir: '.',
// Tests run from compiled output: there is no transform, so the sources under
// test/ cannot execute. Run `yarn tsc` first, and name a suite without its
// extension — `jest date-parser` — since the runtime path is
// dist/test/date-parser.test.js. `.spec.` is included because jest's default
// testMatch accepted it, and pinning this must not quietly drop a suite.
// test/test-collection.test.ts enforces that dist is what gets collected.
testMatch: ['<rootDir>/dist/test/**/*.{test,spec}.js'],
snapshotResolver: '<rootDir>/test/snapshotResolver.js',
};
1 change: 1 addition & 0 deletions packages/cubejs-api-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@types/supertest": "^2.0.10",
"@types/uuid": "^8.3.1",
"jest": "^29",
"micromatch": "^4.0.8",
"mysql": "^2.18.1",
"should": "^13.2.3",
"supertest": "^4.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/* globals describe,test,expect,jest */

import { dateParser } from '../src/date-parser';

describe('dateParser', () => {
// Restoring inside each test body only runs when the assertion passes, so one
// genuine failure would leave `Date.now` mocked for every test after it — and
// this file interleaves clock-mocking tests with ones that read the real
// clock, so a single red test would cascade into unrelated ones.
afterEach(() => {
jest.restoreAllMocks();
});

test('custom daily ranges returns day aligned dateRange', () => {
expect(dateParser('from 1 days ago to now', 'UTC')).toStrictEqual(
[dateParser('yesterday', 'UTC')[0], dateParser('today', 'UTC')[1]]
Expand Down Expand Up @@ -60,7 +66,7 @@ describe('dateParser', () => {

test('from 1 hour ago to now LA', () => {
// 'Z' stands for Zulu time, which is also GMT and UTC.
const now = '2020-09-22T13:03:20.518Z';
const now = new Date('2020-09-22T13:03:20.518Z');
// LA is GMT-0700, 7 hours diff
const tz = 'America/Los_Angeles';

Expand All @@ -74,13 +80,11 @@ describe('dateParser', () => {

test('from 1 quarter ago to now', () => {
const now = new Date(2021, 4, 3, 12, 0, 0, 0);
Date.now = jest.fn().mockReturnValue(now);
jest.spyOn(Date, 'now').mockReturnValue(now.getTime());

expect(dateParser('from 1 quarter ago to now', 'UTC', now)).toStrictEqual(
['2021-02-03T00:00:00.000', '2021-05-03T23:59:59.999']
);

Date.now.mockRestore();
});

test('from 7 days ago to now', () => {
Expand All @@ -103,70 +107,58 @@ describe('dateParser', () => {

test('last 2 quarters', () => {
const now = new Date(2021, 1, 15, 13, 0, 0, 0);
Date.now = jest.fn().mockReturnValue(now);
jest.spyOn(Date, 'now').mockReturnValue(now.getTime());

expect(dateParser('last 2 quarters', 'UTC', now)).toStrictEqual([
'2020-07-01T00:00:00.000',
'2020-12-31T23:59:59.999',
]);

Date.now.mockRestore();
});

test('last 6 months from month with less days than previous month', () => {
Date.now = jest.fn().mockReturnValue(new Date(2021, 1, 15, 13, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(new Date(2021, 1, 15, 13, 0, 0, 0).getTime());

expect(dateParser('last 6 months', 'UTC', new Date(2021, 1, 15, 13, 0, 0, 0))).toStrictEqual([
'2020-08-01T00:00:00.000',
'2021-01-31T23:59:59.999',
]);

Date.now.mockRestore();
});

test('last 6 months from month with more days than previous month', () => {
Date.now = jest.fn().mockReturnValue(new Date(2021, 2, 15, 13, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(new Date(2021, 2, 15, 13, 0, 0, 0).getTime());

expect(dateParser('last 6 months', 'UTC', new Date(2021, 1, 15, 13, 0, 0, 0))).toStrictEqual([
'2020-09-01T00:00:00.000',
'2021-02-28T23:59:59.999',
]);

Date.now.mockRestore();
});

test('next 6 months', () => {
Date.now = jest.fn().mockReturnValue(new Date(2021, 1, 20, 13, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(new Date(2021, 1, 20, 13, 0, 0, 0).getTime());
expect(dateParser('next 6 months', 'UTC', new Date(2021, 1, 20, 13, 0, 0, 0))).toStrictEqual([
'2021-03-01T00:00:00.000',
'2021-08-31T23:59:59.999',
]);

Date.now.mockRestore();
});

test('next month', () => {
Date.now = jest.fn().mockReturnValue(new Date(2021, 2, 5, 13, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(new Date(2021, 2, 5, 13, 0, 0, 0).getTime());
expect(dateParser('next month', 'UTC', new Date(2021, 2, 5, 13, 0, 0, 0))).toStrictEqual(
[
'2021-04-01T00:00:00.000',
'2021-04-30T23:59:59.999'
]
);

Date.now.mockRestore();
});

test('next 5 days', () => {
Date.now = jest.fn().mockReturnValue(new Date(2021, 2, 5, 13, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(new Date(2021, 2, 5, 13, 0, 0, 0).getTime());
expect(dateParser('next 5 days', 'UTC', new Date(2021, 2, 5, 13, 0, 0, 0))).toStrictEqual(
[
'2021-03-06T00:00:00.000',
'2021-03-10T23:59:59.999'
]
);

Date.now.mockRestore();
});

test('throws error on from invalid date to date', () => {
Expand All @@ -182,14 +174,12 @@ describe('dateParser', () => {
});

test('from 12AM till now by hour', () => {
Date.now = jest.fn().mockReturnValue(new Date(2021, 2, 5, 13, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(new Date(2021, 2, 5, 13, 0, 0, 0).getTime());
expect(dateParser('2 weeks ago by hour', 'UTC', new Date(Date.UTC(2021, 2, 5, 13, 0, 0, 0)))).toStrictEqual(
[
'2021-02-19T13:00:00.000',
'2021-02-19T13:59:59.999'
]
);

Date.now.mockRestore();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
/* globals describe,test,expect,jest,beforeEach,afterEach */

import { normalizeQuery, normalizeDateFilterValues, resolveDateRange } from '../src/query';

// The tests below walk the normalized filter tree, including the OR/AND group
// nodes that `NormalizedQuery.filters` (typed as a flat leaf array) does not
// model. This mirrors what the untyped resolver in src/query.js really returns.
// Fields are non-optional on purpose: a test that reaches for `.or` on a node
// that has none should fail on the assertion, not be nudged into a guard here.
type FilterNode = {
values: any[];
or: FilterNode[];
and: FilterNode[];
};

type NormalizedResult = {
filters: FilterNode[];
timeDimensions: { dateRange: string[] }[];
};

function normalized(query: any, persistent = false): NormalizedResult {
return normalizeQuery(query, persistent) as unknown as NormalizedResult;
}

// Tests for filter-leaf date-range resolution at the gateway. This mirrors
// what `timeDimensions.dateRange` has always done: a single relative string
// (e.g. "last 2 weeks") resolves to an absolute [start, end] pair before the
Expand Down Expand Up @@ -285,7 +303,7 @@ describe('normalizeQuery: date-range filter resolution', () => {
test('top-level inDateRange filter with relative string is resolved', () => {
// Why: even without an OR wrapper, a filter leaf with a relative date
// value must be resolved at the gateway.
const result = normalizeQuery({
const result = normalized({
...baseQuery,
filters: [
{ member: 'Orders.createdAt', operator: 'inDateRange', values: ['last 2 weeks'] },
Expand All @@ -301,7 +319,7 @@ describe('normalizeQuery: date-range filter resolution', () => {
test('inDateRange leaf nested inside OR is resolved (the actual feature)', () => {
// Why: this is the whole point. The recursive walker must reach leaves
// inside groups and apply the helper there too.
const result = normalizeQuery({
const result = normalized({
...baseQuery,
filters: [{
or: [
Expand All @@ -319,7 +337,7 @@ describe('normalizeQuery: date-range filter resolution', () => {

test('inDateRange leaf nested inside AND is resolved', () => {
// Why: AND must work symmetrically with OR.
const result = normalizeQuery({
const result = normalized({
...baseQuery,
filters: [{
and: [
Expand All @@ -334,7 +352,7 @@ describe('normalizeQuery: date-range filter resolution', () => {

test('deeply nested date filter (OR inside AND) is resolved', () => {
// Why: the walker must recurse to arbitrary depth, not just one level.
const result = normalizeQuery({
const result = normalized({
...baseQuery,
filters: [{
and: [
Expand All @@ -358,7 +376,7 @@ describe('normalizeQuery: date-range filter resolution', () => {
// Why: invariant — existing queries that only use top-level timeDimensions
// must produce the same shape they always did. Both paths share
// resolveDateRange so they cannot diverge.
const result = normalizeQuery({
const result = normalized({
...baseQuery,
timeDimensions: [
{ dimension: 'Orders.createdAt', dateRange: 'last 2 weeks' },
Expand All @@ -374,7 +392,7 @@ describe('normalizeQuery: date-range filter resolution', () => {
test('invalid relative date inside OR raises UserError at gateway', () => {
// Why: a malformed relative date must fail at the API boundary with a
// clear message, not deep in the SQL planner.
expect(() => normalizeQuery({
expect(() => normalized({
...baseQuery,
filters: [{
or: [
Expand All @@ -391,20 +409,20 @@ describe('normalizeQuery: date-range filter resolution', () => {
const dayBoundaryNow = new Date(Date.UTC(2026, 5, 25, 2, 0, 0, 0));
jest.spyOn(Date, 'now').mockReturnValue(dayBoundaryNow.getTime());

const utc = normalizeQuery({
const utc = normalized({
...baseQuery,
timezone: 'UTC',
filters: [{ or: [
{ member: 'Orders.createdAt', operator: 'inDateRange', values: ['today'] },
]}],
] }],
}, false);

const la = normalizeQuery({
const la = normalized({
...baseQuery,
timezone: 'America/Los_Angeles',
filters: [{ or: [
{ member: 'Orders.createdAt', operator: 'inDateRange', values: ['today'] },
]}],
] }],
}, false);

expect(utc.filters[0].or[0].values[0]).toMatch(/^2026-06-25T/);
Expand All @@ -413,7 +431,7 @@ describe('normalizeQuery: date-range filter resolution', () => {

test('non-date filters inside OR are untouched', () => {
// Why: regression guard — equals/contains/etc. must not be modified.
const result = normalizeQuery({
const result = normalized({
...baseQuery,
filters: [{
or: [
Expand Down
Loading
Loading