nestjs-paginate
Version:
Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.
4,057 lines • 194 kB
JavaScript
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@nestjs/common");
const lodash_1 = require("lodash");
const process = require("process");
const typeorm_1 = require("typeorm");
const cat_hair_entity_1 = require("./__tests__/cat-hair.entity");
const cat_home_pillow_brand_entity_1 = require("./__tests__/cat-home-pillow-brand.entity");
const cat_home_pillow_entity_1 = require("./__tests__/cat-home-pillow.entity");
const cat_home_entity_1 = require("./__tests__/cat-home.entity");
const cat_toy_entity_1 = require("./__tests__/cat-toy.entity");
const cat_entity_1 = require("./__tests__/cat.entity");
const toy_shop_address_entity_1 = require("./__tests__/toy-shop-address.entity");
const toy_shop_entity_1 = require("./__tests__/toy-shop.entity");
const filter_1 = require("./filter");
const paginate_1 = require("./paginate");
// Disable debug logs during tests
beforeAll(() => {
jest.spyOn(common_1.Logger.prototype, 'debug').mockImplementation(() => { });
});
afterAll(() => {
jest.restoreAllMocks(); // Restore default logger behavior
});
const isoStringToDate = (isoString) => new Date(isoString);
describe('paginate', () => {
let dataSource;
let catRepo;
let catToyRepo;
let catHairRepo;
let toyShopRepo;
let toyShopAddressRepository;
let catHomeRepo;
let catHomePillowRepo;
let catHomePillowBrandRepo;
let cats;
let catToys;
let catToysWithoutShop;
let toyShopsAddresses;
let toysShops;
let catHomes;
let catHomePillows;
let naptimePillow;
let pillowBrand;
let catHairs = [];
let underCoats = [];
beforeAll(async () => {
const dbOptions = {
dropSchema: true,
synchronize: true,
logging: ['error'],
entities: [
cat_entity_1.CatEntity,
cat_toy_entity_1.CatToyEntity,
toy_shop_address_entity_1.ToyShopAddressEntity,
cat_home_entity_1.CatHomeEntity,
cat_home_pillow_entity_1.CatHomePillowEntity,
cat_home_pillow_brand_entity_1.CatHomePillowBrandEntity,
toy_shop_entity_1.ToyShopEntity,
process.env.DB === 'postgres' ? cat_hair_entity_1.CatHairEntity : undefined,
],
};
switch (process.env.DB) {
case 'postgres':
dataSource = new typeorm_1.DataSource(Object.assign(Object.assign({}, dbOptions), { type: 'postgres', host: process.env.DB_HOST || 'localhost', port: +process.env.POSTGRESS_DB_PORT || 5432, username: process.env.DB_USERNAME || 'root', password: process.env.DB_PASSWORD || 'pass', database: process.env.DB_DATABASE || 'test' }));
break;
case 'mariadb':
dataSource = new typeorm_1.DataSource(Object.assign(Object.assign({}, dbOptions), { type: 'mariadb', host: process.env.DB_HOST || 'localhost', port: +process.env.MARIA_DB_PORT || 3306, username: process.env.DB_USERNAME || 'root', password: process.env.DB_PASSWORD || 'pass', database: process.env.DB_DATABASE || 'test' }));
break;
case 'sqlite':
dataSource = new typeorm_1.DataSource(Object.assign(Object.assign({}, dbOptions), { type: 'sqlite', database: ':memory:' }));
break;
default:
throw new Error('Invalid DB');
}
await dataSource.initialize();
catRepo = dataSource.getRepository(cat_entity_1.CatEntity);
catToyRepo = dataSource.getRepository(cat_toy_entity_1.CatToyEntity);
catHomeRepo = dataSource.getRepository(cat_home_entity_1.CatHomeEntity);
catHomePillowRepo = dataSource.getRepository(cat_home_pillow_entity_1.CatHomePillowEntity);
catHomePillowBrandRepo = dataSource.getRepository(cat_home_pillow_brand_entity_1.CatHomePillowBrandEntity);
toyShopRepo = dataSource.getRepository(toy_shop_entity_1.ToyShopEntity);
toyShopAddressRepository = dataSource.getRepository(toy_shop_address_entity_1.ToyShopAddressEntity);
cats = await catRepo.save([
catRepo.create({
name: 'Milo',
color: 'brown',
age: 6,
cutenessLevel: cat_entity_1.CutenessLevel.HIGH,
lastVetVisit: isoStringToDate('2022-12-19T10:00:00.000Z'),
size: { height: 25, width: 10, length: 40 },
weightChange: -0.75,
}),
catRepo.create({
name: 'Garfield',
color: 'ginger',
age: 5,
cutenessLevel: cat_entity_1.CutenessLevel.MEDIUM,
lastVetVisit: isoStringToDate('2022-12-20T10:00:00.000Z'),
size: { height: 30, width: 15, length: 45 },
weightChange: 5.25,
}),
catRepo.create({
name: 'Shadow',
color: 'black',
age: 4,
cutenessLevel: cat_entity_1.CutenessLevel.HIGH,
lastVetVisit: isoStringToDate('2022-12-21T10:00:00.000Z'),
size: { height: 25, width: 10, length: 50 },
weightChange: -3,
}),
catRepo.create({
name: 'George',
color: 'white',
age: 3,
cutenessLevel: cat_entity_1.CutenessLevel.LOW,
lastVetVisit: null,
size: { height: 35, width: 12, length: 40 },
weightChange: 0,
}),
catRepo.create({
name: 'Leche',
color: 'white',
age: null,
cutenessLevel: cat_entity_1.CutenessLevel.HIGH,
lastVetVisit: null,
size: { height: 10, width: 5, length: 15 },
weightChange: -1.25,
}),
catRepo.create({
name: 'Baby',
color: 'brown',
age: 0,
cutenessLevel: cat_entity_1.CutenessLevel.HIGH,
lastVetVisit: null,
size: { height: 10, width: 5, length: 10 },
weightChange: 0.01,
}),
catRepo.create({
name: 'Adam',
color: 'black',
age: 4,
cutenessLevel: cat_entity_1.CutenessLevel.LOW,
lastVetVisit: isoStringToDate('2022-12-22T10:00:00.000Z'),
size: { height: 20, width: 15, length: 50 },
weightChange: 4.75,
}),
]);
toyShopsAddresses = await toyShopAddressRepository.save([
toyShopAddressRepository.create({ address: '123 Main St' }),
]);
toysShops = await toyShopRepo.save([
toyShopRepo.create({ shopName: 'Best Toys', address: toyShopsAddresses[0] }),
toyShopRepo.create({ shopName: 'Lovely Toys' }),
]);
catToys = await catToyRepo.save([
catToyRepo.create({ name: 'Fuzzy Thing', cat: cats[0], size: { height: 10, width: 10, length: 10 } }),
catToyRepo.create({
name: 'Stuffed Mouse',
shop: toysShops[0],
cat: cats[0],
size: { height: 5, width: 5, length: 12 },
}),
catToyRepo.create({
name: 'Mouse',
shop: toysShops[1],
cat: cats[0],
size: { height: 6, width: 4, length: 13 },
}),
catToyRepo.create({ name: 'String', cat: cats[1], size: { height: 1, width: 1, length: 50 } }),
]);
catToysWithoutShop = catToys.map((_a) => {
var { shop: _ } = _a, other = __rest(_a, ["shop"]);
const newInstance = new cat_toy_entity_1.CatToyEntity();
for (const otherKey in other) {
newInstance[otherKey] = other[otherKey];
}
return newInstance;
});
pillowBrand = await catHomePillowBrandRepo.save({ name: 'Purrfection', quality: null });
naptimePillow = await catHomePillowRepo.save({ color: 'black', brand: pillowBrand });
catHomes = await catHomeRepo.save([
catHomeRepo.create({ name: 'Box', cat: cats[0], street: null, naptimePillow: null }),
catHomeRepo.create({ name: 'House', cat: cats[1], street: 'Mainstreet', naptimePillow: null }),
catHomeRepo.create({ name: 'Mansion', cat: cats[2], street: 'Boulevard Avenue', naptimePillow }),
]);
catHomePillows = await catHomePillowRepo.save([
catHomePillowRepo.create({ color: 'red', home: catHomes[0] }),
catHomePillowRepo.create({ color: 'yellow', home: catHomes[0] }),
catHomePillowRepo.create({ color: 'blue', home: catHomes[0] }),
catHomePillowRepo.create({ color: 'pink', home: catHomes[1] }),
catHomePillowRepo.create({ color: 'purple', home: catHomes[1] }),
catHomePillowRepo.create({ color: 'teal', home: catHomes[1] }),
]);
// add friends to Milo
await catRepo.save(Object.assign(Object.assign({}, cats[0]), { friends: cats.slice(1) }));
catHairs = [];
underCoats = [];
if (process.env.DB === 'postgres') {
catHairRepo = dataSource.getRepository(cat_hair_entity_1.CatHairEntity);
catHairs = await catHairRepo.save([
catHairRepo.create({
name: 'short',
colors: ['white', 'brown', 'black'],
metadata: { length: 5, thickness: 1 },
}),
catHairRepo.create({
name: 'long',
colors: ['white', 'brown'],
metadata: { length: 20, thickness: 5 },
}),
catHairRepo.create({
name: 'buzzed',
colors: ['white'],
metadata: { length: 0.5, thickness: 10 },
}),
catHairRepo.create({ name: 'none' }),
]);
}
});
if (process.env.DB === 'postgres') {
afterAll(async () => {
const entities = dataSource.entityMetadatas;
const tableNames = entities.map((entity) => `"${entity.tableName}"`).join(', ');
await dataSource.query(`TRUNCATE ${tableNames} RESTART IDENTITY CASCADE;`);
});
}
it('should return an instance of Paginated', async () => {
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
defaultLimit: 1,
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result).toBeInstanceOf(paginate_1.Paginated);
expect(result.data).toStrictEqual(cats.slice(0, 1));
});
it('should accept a query builder', async () => {
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
defaultLimit: 1,
};
const query = {
path: '',
};
const queryBuilder = await catRepo.createQueryBuilder('cats');
const result = await (0, paginate_1.paginate)(query, queryBuilder, config);
expect(result.data).toStrictEqual(cats.slice(0, 1));
});
it('should accept a query builder with custom condition', async () => {
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
};
const query = {
path: '',
};
const queryBuilder = await dataSource
.createQueryBuilder()
.select('cats')
.from(cat_entity_1.CatEntity, 'cats')
.where('cats.color = :color', { color: 'white' });
const result = await (0, paginate_1.paginate)(query, queryBuilder, config);
expect(result.data).toStrictEqual(cats.slice(3, 5));
});
it('should accept query builder and work with query filter', async () => {
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
filterableColumns: {
'size.height': true,
},
};
const query = {
path: '',
filter: {
'size.height': '$gte:20',
},
};
const queryBuilder = await dataSource
.createQueryBuilder()
.select('cats')
.from(cat_entity_1.CatEntity, 'cats')
.where('cats.color = :color', { color: 'white' });
const result = await (0, paginate_1.paginate)(query, queryBuilder, config);
expect(result.data).toStrictEqual(cats.slice(3, 4));
});
it('should default to page 1, if negative page is given', async () => {
const config = {
sortableColumns: ['id'],
defaultLimit: 1,
};
const query = {
path: '',
page: -1,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.currentPage).toBe(1);
expect(result.data).toStrictEqual(cats.slice(0, 1));
});
it('should default to limit maxLimit, if maxLimit is not 0', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: 1,
defaultLimit: 1,
};
const query = {
path: '',
limit: paginate_1.PaginationLimit.NO_PAGINATION,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 1));
});
it('should return all cats', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: paginate_1.PaginationLimit.NO_PAGINATION,
defaultLimit: 1,
};
const query = {
path: '',
limit: paginate_1.PaginationLimit.NO_PAGINATION,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats);
});
it('should limit to query limit, even if maxLimit is set to NO_PAGINATION', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: paginate_1.PaginationLimit.NO_PAGINATION,
};
const query = {
path: '',
limit: 2,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.itemsPerPage).toBe(2);
});
it('should default to limit defaultLimit, if maxLimit is NO_PAGINATION', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: paginate_1.PaginationLimit.NO_PAGINATION,
defaultLimit: 1,
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 1));
});
it('should default to limit maxLimit, if more than maxLimit is given', async () => {
const config = {
sortableColumns: ['id'],
defaultLimit: 5,
maxLimit: 2,
};
const query = {
path: '',
page: 1,
limit: 20,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 2));
});
it('should limit cats by query', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: Number.MAX_SAFE_INTEGER,
defaultLimit: Number.MAX_SAFE_INTEGER,
};
const query = {
path: '',
limit: 2,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 2));
});
it('maxLimit should limit defaultLimit', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: 1,
defaultLimit: 2,
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 1));
});
it('limit should bypass defaultLimit', async () => {
const config = {
sortableColumns: ['id'],
defaultLimit: 1,
};
const query = {
path: '',
limit: 2,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 2));
});
it('DEFAULT_LIMIT should be used as the limit if limit is set to NO_PAGINATION and maxLimit is not specified.', async () => {
const config = {
sortableColumns: ['id'],
};
const query = {
path: '',
limit: paginate_1.PaginationLimit.NO_PAGINATION,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, paginate_1.PaginationLimit.DEFAULT_LIMIT));
});
it('should return the count without data ignoring maxLimit if limit is COUNTER_ONLY', async () => {
const config = {
sortableColumns: ['id'],
maxLimit: paginate_1.PaginationLimit.NO_PAGINATION,
};
const query = {
path: '',
limit: 0,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([]);
expect(result.meta.totalItems).toBe(7);
});
it('should return correct result for limited one-to-many relations', async () => {
const config = {
relations: ['toys'],
sortableColumns: ['id', 'toys.id'],
searchableColumns: ['name', 'toys.name'],
defaultLimit: 4,
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toStrictEqual(4);
});
it('should return correct links for some results', async () => {
const config = {
sortableColumns: ['id'],
};
const query = {
path: '',
page: 2,
limit: 2,
};
const { links } = await (0, paginate_1.paginate)(query, catRepo, config);
expect(links.first).toBe('?page=1&limit=2&sortBy=id:ASC');
expect(links.previous).toBe('?page=1&limit=2&sortBy=id:ASC');
expect(links.current).toBe('?page=2&limit=2&sortBy=id:ASC');
expect(links.next).toBe('?page=3&limit=2&sortBy=id:ASC');
expect(links.last).toBe('?page=4&limit=2&sortBy=id:ASC');
});
it('should return a relative path', async () => {
const config = {
sortableColumns: ['id'],
relativePath: true,
};
const query = {
path: 'http://localhost/cats',
page: 2,
limit: 2,
};
const { links } = await (0, paginate_1.paginate)(query, catRepo, config);
expect(links.first).toBe('/cats?page=1&limit=2&sortBy=id:ASC');
expect(links.previous).toBe('/cats?page=1&limit=2&sortBy=id:ASC');
expect(links.current).toBe('/cats?page=2&limit=2&sortBy=id:ASC');
expect(links.next).toBe('/cats?page=3&limit=2&sortBy=id:ASC');
expect(links.last).toBe('/cats?page=4&limit=2&sortBy=id:ASC');
});
it('should return an absolute path', async () => {
const config = {
sortableColumns: ['id'],
relativePath: false,
};
const query = {
path: 'http://localhost/cats',
page: 2,
limit: 2,
};
const { links } = await (0, paginate_1.paginate)(query, catRepo, config);
expect(links.first).toBe('http://localhost/cats?page=1&limit=2&sortBy=id:ASC');
expect(links.previous).toBe('http://localhost/cats?page=1&limit=2&sortBy=id:ASC');
expect(links.current).toBe('http://localhost/cats?page=2&limit=2&sortBy=id:ASC');
expect(links.next).toBe('http://localhost/cats?page=3&limit=2&sortBy=id:ASC');
expect(links.last).toBe('http://localhost/cats?page=4&limit=2&sortBy=id:ASC');
});
it('should return an absolute path with new origin', async () => {
const config = {
sortableColumns: ['id'],
relativePath: false,
origin: 'http://cats.example',
};
const query = {
path: 'http://localhost/cats',
page: 2,
limit: 2,
};
const { links } = await (0, paginate_1.paginate)(query, catRepo, config);
expect(links.first).toBe('http://cats.example/cats?page=1&limit=2&sortBy=id:ASC');
expect(links.previous).toBe('http://cats.example/cats?page=1&limit=2&sortBy=id:ASC');
expect(links.current).toBe('http://cats.example/cats?page=2&limit=2&sortBy=id:ASC');
expect(links.next).toBe('http://cats.example/cats?page=3&limit=2&sortBy=id:ASC');
expect(links.last).toBe('http://cats.example/cats?page=4&limit=2&sortBy=id:ASC');
});
it('should return only current link if zero results', async () => {
const config = {
sortableColumns: ['id'],
searchableColumns: ['name'],
};
const query = {
path: '',
page: 1,
limit: 2,
search: 'Pluto',
};
const { links } = await (0, paginate_1.paginate)(query, catRepo, config);
expect(links.first).toBe(undefined);
expect(links.previous).toBe(undefined);
expect(links.current).toBe('?page=1&limit=2&sortBy=id:ASC&search=Pluto');
expect(links.next).toBe(undefined);
expect(links.last).toBe(undefined);
});
it('should default to defaultSortBy if query sortBy does not exist', async () => {
const config = {
sortableColumns: ['id', 'createdAt'],
defaultSortBy: [['id', 'DESC']],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.sortBy).toStrictEqual([['id', 'DESC']]);
expect(result.data).toStrictEqual(cats.slice(0).reverse());
});
it('should put null values last when sorting', async () => {
const config = {
sortableColumns: ['age', 'createdAt'],
nullSort: 'last',
defaultSortBy: [['age', 'ASC']],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Extracting the indexes of non-null values and null values
const notNullIndexes = result.data
.map((cat, index) => (cat.age !== null ? index : -1))
.filter((index) => index !== -1);
const nullIndexes = result.data
.map((cat, index) => (cat.age === null ? index : -1))
.filter((index) => index !== -1);
expect(result.meta.sortBy).toStrictEqual([['age', 'ASC']]);
expect(Math.max(...notNullIndexes)).toBeLessThan(Math.min(...nullIndexes));
});
it('should put null values first when sorting', async () => {
const config = {
sortableColumns: ['age', 'createdAt'],
nullSort: 'first',
defaultSortBy: [['age', 'ASC']],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const nullIndexes = result.data
.map((cat, index) => (cat.age === null ? index : -1))
.filter((index) => index !== -1);
const notNullIndexes = result.data
.map((cat, index) => (cat.age !== null ? index : -1))
.filter((index) => index !== -1);
expect(result.meta.sortBy).toStrictEqual([['age', 'ASC']]);
expect(Math.max(...nullIndexes)).toBeLessThan(Math.min(...notNullIndexes));
});
it('should sort result by multiple columns', async () => {
const config = {
sortableColumns: ['name', 'color'],
};
const query = {
path: '',
sortBy: [
['color', 'DESC'],
['name', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const sortedCats = cats.slice(0).sort((a, b) => {
if (a.color === b.color) {
return a.name.localeCompare(b.name);
}
return b.color.localeCompare(a.color);
});
expect(result.meta.sortBy).toStrictEqual([
['color', 'DESC'],
['name', 'ASC'],
]);
expect(result.data).toStrictEqual(sortedCats);
});
it('should sort result by camelcase columns', async () => {
const config = {
sortableColumns: ['cutenessLevel', 'name'],
};
const query = {
path: '',
sortBy: [
['cutenessLevel', 'ASC'],
['name', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const sortedCats = cats.slice(0).sort((a, b) => {
if (a.cutenessLevel === b.cutenessLevel) {
return a.name.localeCompare(b.name);
}
return a.cutenessLevel.localeCompare(b.cutenessLevel);
});
expect(result.meta.sortBy).toStrictEqual([
['cutenessLevel', 'ASC'],
['name', 'ASC'],
]);
expect(result.data).toStrictEqual(sortedCats);
});
it('should return result based on search term', async () => {
const config = {
sortableColumns: ['id', 'name', 'color'],
searchableColumns: ['name', 'color'],
};
const query = {
path: '',
search: 'i',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.search).toStrictEqual('i');
expect(result.data).toStrictEqual([cats[0], cats[1], cats[3], cats[4]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=i');
});
it('should return result based on search term on a camelcase named column', async () => {
const config = {
sortableColumns: ['id', 'name', 'color'],
searchableColumns: ['cutenessLevel'],
};
const query = {
path: '',
search: 'hi',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expectedCats = cats.filter((cat) => cat.cutenessLevel === cat_entity_1.CutenessLevel.HIGH);
expect(result.meta.search).toStrictEqual('hi');
expect(result.data).toStrictEqual(expectedCats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=hi');
});
it('should not result in a sql syntax error when attempting a sql injection', async () => {
const config = {
sortableColumns: ['id', 'name', 'color'],
searchableColumns: ['name', 'color'],
};
const query = {
path: '',
search: "i UNION SELECT tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'",
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([]);
});
it('should return result based on search term on many-to-one relation', async () => {
const config = {
relations: ['cat'],
sortableColumns: ['id', 'name'],
searchableColumns: ['name', 'cat.name'],
};
const query = {
path: '',
search: 'Milo',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.search).toStrictEqual('Milo');
expect(result.data).toStrictEqual([catToysWithoutShop[0], catToysWithoutShop[1], catToysWithoutShop[2]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Milo');
});
it('should return result based on search term on one-to-many relation', async () => {
const config = {
relations: ['toys'],
sortableColumns: ['id', 'toys.id'],
searchableColumns: ['name', 'toys.name'],
};
const query = {
path: '',
search: 'Mouse',
sortBy: [
['id', 'ASC'],
['toys.id', 'DESC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.search).toStrictEqual('Mouse');
const toy = (0, lodash_1.clone)(catToysWithoutShop[1]);
delete toy.cat;
const toy2 = (0, lodash_1.clone)(catToysWithoutShop[2]);
delete toy2.cat;
expect(result.data).toStrictEqual([Object.assign((0, lodash_1.clone)(cats[0]), { toys: [toy2, toy] })]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&sortBy=toys.id:DESC&search=Mouse');
});
it('should return result based on search term on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name', 'cat.id'],
};
const query = {
path: '',
sortBy: [['cat.id', 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.sortBy).toStrictEqual([['cat.id', 'DESC']]);
const catHomesClone = (0, lodash_1.clone)([catHomes[0], catHomes[1], catHomes[2]]);
catHomesClone[0].countCat = cats.filter((cat) => cat.id === catHomesClone[0].cat.id).length;
catHomesClone[1].countCat = cats.filter((cat) => cat.id === catHomesClone[1].cat.id).length;
catHomesClone[2].countCat = cats.filter((cat) => cat.id === catHomesClone[2].cat.id).length;
expect(result.data).toStrictEqual(catHomesClone.sort((a, b) => b.cat.id - a.cat.id));
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.id:DESC');
});
it('should handle nullSort with relations properly', async () => {
const config = {
sortableColumns: ['id', 'age'],
nullSort: 'last',
defaultSortBy: [['age', 'ASC']],
relations: ['toys'],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Prepare expected result - cats ordered by age with null age last, including toys relation
const expectedResult = [...cats]
.sort((a, b) => {
if (a.age === null && b.age === null)
return 0;
if (a.age === null)
return 1;
if (b.age === null)
return -1;
return a.age - b.age;
})
.map((cat) => cat.id);
expect(result.meta.sortBy).toStrictEqual([['age', 'ASC']]);
expect(result.data.map((v) => v.id)).toStrictEqual(expectedResult);
});
it('should return result based on sort and search on many-to-one relation', async () => {
const config = {
relations: ['cat'],
sortableColumns: ['id', 'name', 'cat.id'],
searchableColumns: ['name', 'cat.name'],
};
const query = {
path: '',
sortBy: [['cat.id', 'DESC']],
search: 'Milo',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.search).toStrictEqual('Milo');
expect(result.data).toStrictEqual([catToysWithoutShop[0], catToysWithoutShop[1], catToysWithoutShop[2]].sort((a, b) => b.cat.id - a.cat.id));
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.id:DESC&search=Milo');
});
it('should return result based on sort on one-to-many relation', async () => {
const config = {
relations: ['toys', 'toys.shop', 'toys.shop.address'],
sortableColumns: ['id', 'name', 'toys.id'],
searchableColumns: ['name', 'toys.name'],
};
const query = {
path: '',
sortBy: [['toys.id', 'DESC']],
search: 'Mouse',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.search).toStrictEqual('Mouse');
const toy1 = (0, lodash_1.clone)(catToys[1]);
delete toy1.cat;
const toy2 = (0, lodash_1.clone)(catToys[2]);
delete toy2.cat;
delete result.data[0].toys[0].shop.address;
expect(result.data).toStrictEqual([Object.assign((0, lodash_1.clone)(cats[0]), { toys: [toy2, toy1] })]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=toys.id:DESC&search=Mouse');
});
it('should return result based on sort on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
searchableColumns: ['name', 'cat.name'],
};
const query = {
path: '',
search: 'Garfield',
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.search).toStrictEqual('Garfield');
const catHomesClone = (0, lodash_1.clone)(catHomes[1]);
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length;
expect(result.data).toStrictEqual([catHomesClone]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Garfield');
});
it('should load nested relations (object notation)', async () => {
const config = {
relations: { home: { pillows: true, naptimePillow: { brand: true } } },
sortableColumns: ['id', 'name'],
searchableColumns: ['name'],
};
const query = {
path: '',
search: 'Garfield',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const cat = (0, lodash_1.clone)(cats[1]);
const catHomesClone = (0, lodash_1.clone)(catHomes[1]);
const catHomePillowsClone3 = (0, lodash_1.clone)(catHomePillows[3]);
delete catHomePillowsClone3.home;
const catHomePillowsClone4 = (0, lodash_1.clone)(catHomePillows[4]);
delete catHomePillowsClone4.home;
const catHomePillowsClone5 = (0, lodash_1.clone)(catHomePillows[5]);
delete catHomePillowsClone5.home;
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length;
catHomesClone.pillows = [catHomePillowsClone3, catHomePillowsClone4, catHomePillowsClone5];
cat.home = catHomesClone;
delete cat.home.cat;
expect(result.meta.search).toStrictEqual('Garfield');
expect(result.data).toStrictEqual([cat]);
expect(result.data[0].home).toBeDefined();
expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows);
});
it('should load nested relations (array notation)', async () => {
const config = {
relations: ['home.pillows', 'home.naptimePillow.brand'],
sortableColumns: ['id', 'name'],
searchableColumns: ['name'],
};
const query = {
path: '',
search: 'Garfield',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const cat = (0, lodash_1.clone)(cats[1]);
const catHomesClone = (0, lodash_1.clone)(catHomes[1]);
const catHomePillowsClone3 = (0, lodash_1.clone)(catHomePillows[3]);
delete catHomePillowsClone3.home;
const catHomePillowsClone4 = (0, lodash_1.clone)(catHomePillows[4]);
delete catHomePillowsClone4.home;
const catHomePillowsClone5 = (0, lodash_1.clone)(catHomePillows[5]);
delete catHomePillowsClone5.home;
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length;
catHomesClone.pillows = [catHomePillowsClone3, catHomePillowsClone4, catHomePillowsClone5];
cat.home = catHomesClone;
delete cat.home.cat;
expect(result.meta.search).toStrictEqual('Garfield');
expect(result.data).toStrictEqual([cat]);
expect(result.data[0].home).toBeDefined();
expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows);
});
it('should throw an error when nonexistent relation loaded', async () => {
const config = {
relations: ['homee'],
sortableColumns: ['id'],
};
const query = {
path: '',
};
try {
await (0, paginate_1.paginate)(query, catRepo, config);
}
catch (err) {
expect(err).toBeInstanceOf(typeorm_1.TypeORMError);
}
});
it('should return result based on search term and searchBy columns', async () => {
const config = {
sortableColumns: ['id', 'name', 'color'],
searchableColumns: ['name', 'color'],
};
const searchTerm = 'white';
const expectedResultData = cats.filter((cat) => cat.color === searchTerm);
const query = {
path: '',
search: searchTerm,
searchBy: ['color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.search).toStrictEqual(searchTerm);
expect(result.meta.searchBy).toStrictEqual(['color']);
expect(result.data).toStrictEqual(expectedResultData);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=white&searchBy=color');
});
it('should return result based on where config and filter', async () => {
const config = {
sortableColumns: ['id'],
where: {
color: 'white',
},
filterableColumns: {
name: [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
name: '$not:Leche',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
name: '$not:Leche',
});
expect(result.data).toStrictEqual([cats[3]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche');
});
it('should return based on a nested many-to-one where condition', async () => {
const config = {
sortableColumns: ['id'],
relations: ['cat'],
where: {
cat: {
id: cats[0].id,
},
},
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.totalItems).toBe(3);
result.data.forEach((toy) => {
expect(toy.cat.id).toBe(cats[0].id);
});
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('should return valid data filtering by not id field many-to-one', async () => {
const config = {
sortableColumns: ['id', 'name'],
relations: ['cat'],
where: {
cat: {
name: cats[0].name,
},
},
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.totalItems).toBe(3);
result.data.forEach((toy) => {
expect(toy.cat.id).toBe(cats[0].id);
});
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('should return result based on where one-to-many relation', async () => {
const config = {
relations: ['toys'],
sortableColumns: ['id', 'name'],
where: {
toys: {
name: 'Stuffed Mouse',
},
},
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toBe(1);
expect(result.data[0].toys.length).toBe(1);
expect(result.data[0].toys[0].name).toBe('Stuffed Mouse');
});
it('should return all cats with a toys from the lovely shop', async () => {
const config = {
relations: ['toys', 'toys.shop'],
sortableColumns: ['id', 'name'],
where: {
toys: {
shop: {
shopName: 'Lovely Toys',
},
},
},
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toBe(1);
expect(result.data[0].toys.length).toBe(1);
expect(result.data[0].toys[0].shop.id).toStrictEqual(toysShops[1].id);
expect(result.data[0].toys[0].name).toBe('Mouse');
});
it('should return all cats from shop where street name like 123', async () => {
const config = {
relations: ['toys', 'toys.shop', 'toys.shop.address'],
sortableColumns: ['id', 'name'],
where: {
toys: {
shop: {
address: {
address: (0, typeorm_1.Like)('%123%'),
},
},
},
},
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toBe(1);
expect(result.data[0].toys.length).toBe(1);
expect(result.data[0].toys[0].shop).toStrictEqual(toysShops[0]);
expect(result.data[0].toys[0].name).toBe('Stuffed Mouse');
});
it('should return result based on filter on many-to-one relation', async () => {
const config = {
relations: ['cat'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.name': [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
'cat.name': '$not:Milo',
},
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.name': '$not:Milo',
});
expect(result.data).toStrictEqual([catToys[3]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.name=$not:Milo');
});
it('should be possible to filter by relation without loading it', async () => {
const config = {
relations: ['cat'],
sortableColumns: ['id'],
where: { cat: { toys: { name: catToys[0].name } } },
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toStrictEqual(3);
});
it('should be possible to filter by relation without loading it 4th level', async () => {
const config = {
relations: ['cat'],
sortableColumns: ['id'],
where: { cat: { toys: { shop: { address: { address: (0, typeorm_1.Like)('%123%') } } } } },
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toStrictEqual(3);
});
it('should be possible to filter by relation without loading it 4th level with load eager', async () => {
const config = {
loadEagerRelations: true,
sortableColumns: ['id'],
where: { cat: { toys: { shop: { address: { address: (0, typeorm_1.Like)('%123%') } } } } },
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toStrictEqual(3);
});
it('should be possible to filter by relation without including any relations', async () => {
const config = {
loadEagerRelations: false,
sortableColumns: ['id'],
where: { cat: { toys: { shop: { address: { address: (0, typeorm_1.Like)('%123%') } } } } },
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toStrictEqual(3);
});
it('should return result based on filter on one-to-many relation', async () => {
const config = {
relations: ['toys'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'toys.name': [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
'toys.name': '$not:Stuffed Mouse',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const cat1 = (0, lodash_1.clone)(cats[0]);
const cat2 = (0, lodash_1.clone)(cats[1]);
const catToys1 = (0, lodash_1.clone)(catToysWithoutShop[0]);
const catToys2 = (0, lodash_1.clone)(catToysWithoutShop[2]);
const catToys3 = (0, lodash_1.clone)(catToysWithoutShop[3]);
delete catToys1.cat;
delete catToys2.cat;
delete catToys3.cat;
cat1.toys = [catToys1, catToys2];
cat2.toys = [catToys3];
expect(result.meta.filter).toStrictEqual({
'toys.name': '$not:Stuffed Mouse',
});
expect(result.data).toStrictEqual([cat1, cat2]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.name=$not:Stuffed Mouse');
});
it('should return result based on filter on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.name': [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
'cat.name': '$not:Garfield',
},
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.name': '$not:Garfield',
});
const catHomesClones = [(0, lodash_1.clone)(catHomes[0]), (0, lodash_1.clone)(catHomes[2])];
catHomesClones[0].countCat = cats.filter((cat) => cat.id === catHomesClones[0].cat.id).length;
catHomesClones[1].countCat = cats.filter((cat) => cat.id === catHomesClones[1].cat.id).length;
expect(result.data).toStrictEqual(catHomesClones);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.name=$not:Garfield');
});
it('should return result based on $in filter on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.age': [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
filter: {
'cat.age': '$in:4,6',
},
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.age': '$in:4,6',
});
const catHomesClones = [(0, lodash_1.clone)(catHomes[0]), (0, lodash_1.clone)(catHomes[2])];
catHomesClones[0].countCat = cats.filter((cat) => cat.id === catHomesClones[0].cat.id).length;
catHomesClones[1].countCat = cats.filter((cat) => cat.id === catHomesClones[1].cat.id).length;
expect(result.data).toStrictEqual(catHomesClones);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.age=$in:4,6');
});
it('should return result based on $btw filter on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.age': [filter_1.FilterOperator.BTW],
},
};
const query = {
path: '',
filter: {
'cat.age': '$btw:6,10',
},
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.age': '$btw:6,10',
});
const catHomesClone = (0, lodash_1.clone)(catHomes[0]);
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length;
expect(result.data).toStrictEqual([catHomesClone]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.age=$btw:6,10');
});
it('should return result based on sort on embedded entity', async () => {
const config = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
searchableColumns: ['name'],
};
const query = {
path: '',
sortBy: [
['size.height', 'ASC'],
['size.length', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const orderedCats = [...cats].sort((a, b) => {
if (a.size.height !== b.size.height) {
return a.size.height - b.size.height;
}
return a.size.length - b.size.length;
});
expect(result.data).toStrictEqual(orderedCats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=size.height:ASC&sortBy=size.length:ASC');
});
it('should return result based on sort on embedded entity when other relations loaded', async () => {
const config = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width', 'toys.(size.height)'],
searchableColumns: ['name'],
relations: ['home', 'toys', 'home.naptimePillow.brand'],
};
const query = {
path: '',
sortBy: [
['size.height', 'DESC'],
['size.length', 'DESC'],
['toys.(size.height)', 'DESC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const copyCats = cats.map((cat) => {
const copy = (0, lodash_1.clone)(cat);
copy.home = null;
copy.toys = [];
return copy;
});
const copyHomes = catHomes.map((home) => {
const copy = (0, lodash_1.clone)(home);
copy.countCat = cats.filter((cat) => cat.id === copy.cat.id).length;
delete copy.cat;
return copy;
});
copyCats[0].home = copyHomes[0];
copyCats[1].home = copyHomes[1];
copyCats[2].home = copyHomes[2];
const copyToys = catToysWithoutShop.map((toy) => {
const copy = (0, lodash_1.clone)(toy);
delete copy.cat;
return copy;
});
copyCats[0].toys = [copyToys[0], copyToys[2], copyToys[1]];
copyCats[1].toys = [copyToys[3]];
const orderedCats = [...copyCats].sort((a, b) => {
if (b.size.height !== a.size.height) {
return b.size.height - a.size.height;
}
if (b.size.length !== a.size.length) {
return b.size.length - a.size.length;
}
const maxToyHeightA = a.toys.length > 0 ? Math.max(...a.toys.map((toy) => toy.size.height)) : -Infinity;
const maxToyHeightB = b.toys.length > 0 ? Math.max(...b.toys.map((toy) => toy.size.height)) : -Infinity;
return maxToyHeightB - maxToyHeightA;
});
expect(result.data).toStrictEqual(orderedCats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=size.height:DESC&sortBy=size.length:DESC&sortBy=toys.(size.height):DESC');
});
it('should return result based on sort on embedded entity on one-to-many relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'toys.(size.height)', 'toys.(size.length)', 'toys.(size.width)'],
searchableColumns: ['name'],
relations: ['toys'],
};
const query = {
path: '',
sortBy: [
['id', 'DESC'],
['toys.(size.height)', 'ASC'],
['toys.(size.length)', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const toy0 = (0, lodash_1.clone)(catToysWithoutShop[0]);
delete toy0.cat;
const toy1 = (0, lodash_1.clone)(catToysWithoutShop[1]);
delete toy1.cat;
const toy2 = (0, lodash_1.clone)(catToysWithoutShop[2]);
delete toy2.cat;
const toy3 = (0, lodash_1.clone)(catToysWithoutShop[3]);
delete toy3.cat;
const orderedCats = [
Object.assign((0, lodash_1.clone)(cats[6]), { toys: [] }),
Object.assign((0, lodash_1.clone)(cats[5]), { toys: [] }),
Object.assign((0, lodash_1.clone)(cats[4]), { toys: [] }),
Object.assign((0, lodash_1.clone)(cats[3]), { toys: [] }),
Object.assign((0, lodash_1.clone)(cats[2]), { toys: [] }),
Object.assign((0, lodash_1.clone)(cats[1]), { toys: [toy3] }),
Object.assign((0, lodash_1.clone)(cats[0]), { toys: [toy1, toy2, toy0] }),
];
expect(result.data).toStrictEqual(orderedCats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:DESC&sortBy=toys.(size.height):ASC&sortBy=toys.(size.length):ASC');
});
it('should return result based on sort on embedded entity on many-to-one relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['name'],
relations: ['cat'],
};
const query = {
path: '',
sortBy: [
['cat.(size.height)', 'DESC'],
['cat.(size.length)', 'DESC'],
['name', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
const orderedToys = [catToysWithoutShop[3], catToysWithoutShop[0], catToysWithoutShop[2], catToysWithoutShop[1]];
expect(result.data).toStrictEqual(orderedToys);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.(size.height):DESC&sortBy=cat.(size.length):DESC&sortBy=name:ASC');
});
it('should return result based on sort on embedded entity on one-to-one relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['name'],
relations: ['cat', 'naptimePillow.brand'],
};
const query = {
path: '',
sortBy: [['cat.(size.height)', 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
const orderedHomes = (0, lodash_1.clone)([catHomes[1], catHomes[0], catHomes[2]]);
orderedHomes[0].countCat = cats.filter((cat) => cat.id === orderedHomes[0].cat.id).length;
orderedHomes[1].countCat = cats.filter((cat) => cat.id === orderedHomes[1].cat.id).length;
orderedHomes[2].countCat = cats.filter((cat) => cat.id === orderedHomes[2].cat.id).length;
expect(result.data).toStrictEqual(orderedHomes);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.(size.height):DESC');
});
it('should return result based on search on embedded entity', async () => {
const config = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
searchableColumns: ['size.height'],
};
const query = {
path: '',
search: '10',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[4], cats[5]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=10');
});
it('should return result based on search term on embedded entity when other relations loaded', async () => {
const config = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
searchableColumns: ['size.height'],
relations: ['home', 'toys'],
};
const query = {
path: '',
search: '10',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.search).toStrictEqual('10');
const copyCat = (0, lodash_1.clone)(cats[4]);
copyCat.home = null;
copyCat.toys = [];
const copyCat2 = (0, lodash_1.clone)(cats[5]);
copyCat2.home = null;
copyCat2.toys = [];
expect(result.data).toStrictEqual([copyCat, copyCat2]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=10');
});
it('should return result based on search term on embedded entity on many-to-one relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['cat.(size.height)'],
relations: ['cat'],
};
const query = {
path: '',
search: '30',
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.search).toStrictEqual('30');
expect(result.data).toStrictEqual([catToys[3]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=30');
});
it('should return result based on search term on embedded entity on one-to-many relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'toys.(size.height)', 'toys.(size.length)', 'toys.(size.width)'],
searchableColumns: ['toys.(size.height)'],
relations: ['toys'],
};
const query = {
path: '',
search: '1',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const toy0 = (0, lodash_1.clone)(catToys[0]);
delete toy0.cat;
const toy3 = (0, lodash_1.clone)(catToys[3]);
delete toy3.cat;
expect(result.data).toStrictEqual([
Object.assign((0, lodash_1.clone)(cats[0]), { toys: [toy0] }),
Object.assign((0, lodash_1.clone)(cats[1]), { toys: [toy3] }),
]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=1');
});
it('should return result based on search term on embedded entity on one-to-one relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['cat.(size.height)'],
relations: ['cat', 'naptimePillow.brand'],
};
const query = {
path: '',
search: '30',
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
const catHomeClone = (0, lodash_1.clone)(catHomes[1]);
catHomeClone.countCat = cats.filter((cat) => cat.id === catHomeClone.cat.id).length;
expect(result.data).toStrictEqual([catHomeClone]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=30');
});
it('should return result based on sort and search on embedded many-to-one relation', async () => {
const config = {
sortableColumns: ['id', 'name', 'cat.(size.height)', 'cat.(size.length)', 'cat.(size.width)'],
searchableColumns: ['cat.(size.width)'],
relations: ['cat'],
};
const query = {
path: '',
search: '1',
sortBy: [['cat.(size.height)', 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.search).toStrictEqual('1');
expect(result.data).toStrictEqual([
catToysWithoutShop[3],
catToysWithoutShop[0],
catToysWithoutShop[1],
catToysWithoutShop[2],
]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.(size.height):DESC&search=1');
});
it('should return result based on filter on embedded entity', async () => {
const config = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
searchableColumns: ['size.height'],
filterableColumns: {
'size.height': [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
'size.height': '$not:25',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[1], cats[3], cats[4], cats[5], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.size.height=$not:25');
});
it('should return result based on filter on embedded entity when other relations loaded', async () => {
const config = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
searchableColumns: ['size.height'],
filterableColumns: {
'size.height': [filter_1.FilterSuffix.NOT],
},
relations: ['home', 'home.naptimePillow.brand'],
};
const query = {
path: '',
filter: {
'size.height': '$not:25',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const home = (0, lodash_1.clone)(catHomes[1]);
home.countCat = cats.filter((cat) => cat.id === home.cat.id).length;
delete home.cat;
const copyCats = [
Object.assign((0, lodash_1.clone)(cats[1]), { home: home }),
Object.assign((0, lodash_1.clone)(cats[3]), { home: null }),
Object.assign((0, lodash_1.clone)(cats[4]), { home: null }),
Object.assign((0, lodash_1.clone)(cats[5]), { home: null }),
Object.assign((0, lodash_1.clone)(cats[6]), { home: null }),
];
expect(result.data).toStrictEqual(copyCats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.size.height=$not:25');
});
it('should return result based on filter on embedded on many-to-one relation', async () => {
const config = {
relations: ['cat'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.(size.height)': [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
'cat.(size.height)': '$not:25',
},
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.(size.height)': '$not:25',
});
expect(result.data).toStrictEqual([catToys[3]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$not:25');
});
it('should return result based on filter on embedded on one-to-many relation', async () => {
const config = {
relations: ['toys'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'toys.(size.height)': [filter_1.FilterOperator.EQ],
},
};
const query = {
path: '',
filter: {
'toys.(size.height)': '1',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const cat2 = (0, lodash_1.clone)(cats[1]);
const catToys3 = (0, lodash_1.clone)(catToys[3]);
delete catToys3.cat;
cat2.toys = [catToys3];
expect(result.meta.filter).toStrictEqual({
'toys.(size.height)': '1',
});
expect(result.data).toStrictEqual([cat2]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.(size.height)=1');
});
it('should return result based on filter on embedded on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.(size.height)': [filter_1.FilterOperator.EQ],
},
};
const query = {
path: '',
filter: {
'cat.(size.height)': '$eq:30',
},
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.(size.height)': '$eq:30',
});
const catClone = (0, lodash_1.clone)(catHomes[1]);
catClone.countCat = cats.filter((cat) => cat.size.height === 30 && cat.id == catClone.cat.id).length;
expect(result.data).toStrictEqual([catClone]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$eq:30');
});
it('should return result based on $in filter on embedded on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.(size.height)': [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
filter: {
'cat.(size.height)': '$in:10,30,35',
},
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.(size.height)': '$in:10,30,35',
});
const catClone = (0, lodash_1.clone)(catHomes[1]);
catClone.countCat = cats.filter((cat) => (cat.size.height === 10 || cat.size.height === 30 || cat.size.height === 35) &&
cat.id == catClone.cat.id).length;
expect(result.data).toStrictEqual([catClone]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$in:10,30,35');
});
it('should return result based on $btw filter on embedded on one-to-one relation', async () => {
const config = {
relations: ['cat', 'naptimePillow.brand'],
sortableColumns: ['id', 'name'],
filterableColumns: {
'cat.(size.height)': [filter_1.FilterOperator.BTW],
},
};
const query = {
path: '',
filter: {
'cat.(size.height)': '$btw:18,33',
},
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.meta.filter).toStrictEqual({
'cat.(size.height)': '$btw:18,33',
});
const catHomeClones = (0, lodash_1.clone)(catHomes);
catHomeClones[0].countCat = cats.filter((cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClones[0].cat.id).length;
catHomeClones[1].countCat = cats.filter((cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClones[1].cat.id).length;
catHomeClones[2].countCat = cats.filter((cat) => cat.size.height >= 18 && cat.size.height <= 33 && cat.id == catHomeClones[1].cat.id).length;
expect(result.data).toStrictEqual(catHomeClones);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.(size.height)=$btw:18,33');
});
it('should return result based on where array and filter', async () => {
const config = {
sortableColumns: ['id'],
where: [
{
color: 'white',
},
{
age: 4,
},
],
filterableColumns: {
name: [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
name: '$not:Leche',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
name: '$not:Leche',
});
expect(result.data).toStrictEqual([cats[2], cats[3], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche');
});
it('should return result based on multiple filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: [filter_1.FilterSuffix.NOT],
color: [filter_1.FilterOperator.EQ],
},
};
const query = {
path: '',
filter: {
name: '$not:Leche',
color: 'white',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
name: '$not:Leche',
color: 'white',
});
expect(result.data).toStrictEqual([cats[3]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche&filter.color=white');
});
it('should return result based on $ilike filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: [filter_1.FilterOperator.ILIKE],
},
};
const query = {
path: '',
filter: {
name: '$ilike:arf',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
name: '$ilike:arf',
});
expect(result.data).toStrictEqual([cats[1]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$ilike:arf');
});
it('should return result based on $sw filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: [filter_1.FilterOperator.SW],
},
};
const query = {
path: '',
filter: {
name: '$sw:Ga',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
name: '$sw:Ga',
});
expect(result.data).toStrictEqual([cats[1]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$sw:Ga');
});
it('should return result based on filter and search term', async () => {
const config = {
sortableColumns: ['id'],
searchableColumns: ['name', 'color'],
filterableColumns: {
id: [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
search: 'white',
filter: {
id: '$not:$in:1,2,5',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.search).toStrictEqual('white');
expect(result.meta.filter).toStrictEqual({ id: '$not:$in:1,2,5' });
expect(result.data).toStrictEqual([cats[3]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=white&filter.id=$not:$in:1,2,5');
});
it('should return result based on filter and where config', async () => {
const config = {
sortableColumns: ['id'],
where: {
color: (0, typeorm_1.In)(['black', 'white']),
},
filterableColumns: {
id: [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
filter: {
id: '$not:$in:1,2,5',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[2], cats[3], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.id=$not:$in:1,2,5');
});
it('should return result based on range filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterOperator.GTE],
},
};
const query = {
path: '',
filter: {
age: '$gte:4',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$gte:4');
});
it('should return result based on between range filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterOperator.BTW],
},
};
const query = {
path: '',
filter: {
age: '$btw:4,5',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[1], cats[2], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$btw:4,5');
});
it('should return result based on is null query', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterOperator.NULL],
},
};
const query = {
path: '',
filter: {
age: '$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[4]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$null');
});
it('should return result based on not null query', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.NULL],
},
};
const query = {
path: '',
filter: {
age: '$not:$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
expect(result.data).toEqual(expect.arrayContaining(ageNotNullCats));
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null');
});
it('should return result based on not null query on relation', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'home.street': [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.NULL],
},
relations: ['home', 'home.naptimePillow.brand'],
};
const query = {
path: '',
filter: {
'home.street': '$not:$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expectedResult = [1, 2].map((i) => {
const ret = Object.assign((0, lodash_1.clone)(cats[i]), { home: (0, lodash_1.clone)(catHomes[i]) });
ret.home.countCat = 1;
delete ret.home.cat;
return ret;
});
expect(result.data).toStrictEqual(expectedResult);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.home.street=$not:$null');
});
it('should return result based on null query on relation', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'home.street': [filter_1.FilterOperator.NULL],
},
relations: ['home', 'home.naptimePillow.brand'],
};
const query = {
path: '',
filter: {
'home.street': '$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expectedResult = [0].map((i) => {
const ret = Object.assign((0, lodash_1.clone)(cats[i]), { home: Object.assign((0, lodash_1.clone)(catHomes[i])) });
ret.home.countCat = 1;
delete ret.home.cat;
return ret;
});
expect(result.data).toStrictEqual(expectedResult);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.home.street=$null');
});
it('should return result based on null query on nested relation', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'home.naptimePillow.brand.quality': [filter_1.FilterOperator.NULL],
},
};
const query = {
path: '',
filter: {
'home.naptimePillow.brand.quality': '$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expectedResult = [2].map((i) => {
const ret = Object.assign((0, lodash_1.clone)(cats[i]), { home: Object.assign((0, lodash_1.clone)(catHomes[i])) });
ret.home.countCat = 1;
delete ret.home.cat;
return ret;
});
expect(result.data).toStrictEqual(expectedResult);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.home.naptimePillow.brand.quality=$null');
});
it('should ignore filterable column which is not configured', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.NULL],
},
};
const query = {
path: '',
filter: {
age: '$not:$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null');
});
it('should ignore filter operator which is not configured', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: {
age: '$not:$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null');
});
it('should throw an error when no sortableColumns', async () => {
const config = {
sortableColumns: [],
};
const query = {
path: '',
};
try {
await (0, paginate_1.paginate)(query, catRepo, config);
}
catch (err) {
expect(err).toBeInstanceOf(common_1.HttpException);
}
});
it.each([
{ operator: '$eq', result: true },
{ operator: '$gte', result: true },
{ operator: '$gt', result: true },
{ operator: '$in', result: true },
{ operator: '$null', result: true },
{ operator: '$lt', result: true },
{ operator: '$lte', result: true },
{ operator: '$btw', result: true },
{ operator: '$ilike', result: true },
{ operator: '$fake', result: false },
])('should check operator "$operator" valid is $result', ({ operator, result }) => {
expect((0, filter_1.isOperator)(operator)).toStrictEqual(result);
});
it.each([{ suffix: '$not', result: true }])('should check suffix "$suffix" valid is $result', ({ suffix, result }) => {
expect((0, filter_1.isSuffix)(suffix)).toStrictEqual(result);
});
it.each([
{ operator: '$eq', name: 'Equal' },
{ operator: '$gt', name: 'MoreThan' },
{ operator: '$gte', name: 'MoreThanOrEqual' },
{ operator: '$in', name: 'In' },
{ operator: '$null', name: 'IsNull' },
{ operator: '$lt', name: 'LessThan' },
{ operator: '$lte', name: 'LessThanOrEqual' },
{ operator: '$btw', name: 'Between' },
{ operator: '$not', name: 'Not' },
{ operator: '$ilike', name: 'ILike' },
])('should get operator function $name for "$operator"', ({ operator, name }) => {
const func = filter_1.OperatorSymbolToFunction.get(operator);
expect(func.name).toStrictEqual(name);
});
for (const cc of [filter_1.FilterComparator.AND, filter_1.FilterComparator.OR, '']) {
const comparator = cc === '' ? filter_1.FilterComparator.AND : cc;
const cSrt = cc === '' ? cc : `${comparator}:`;
it.each([
{
string: cSrt + '$ilike:value',
tokens: { comparator, operator: '$ilike', suffix: undefined, value: 'value' },
},
{ string: cSrt + '$eq:value', tokens: { comparator, operator: '$eq', suffix: undefined, value: 'value' } },
{
string: cSrt + '$eq:val:ue',
tokens: { comparator, operator: '$eq', suffix: undefined, value: 'val:ue' },
},
{
string: cSrt + '$in:value1,value2,value3',
tokens: { comparator, operator: '$in', suffix: undefined, value: 'value1,value2,value3' },
},
{
string: cSrt + '$not:$in:value1:a,value2:b,value3:c',
tokens: { comparator, operator: '$in', suffix: '$not', value: 'value1:a,value2:b,value3:c' },
},
{ string: cSrt + 'value', tokens: { comparator, operator: '$eq', suffix: undefined, value: 'value' } },
{ string: cSrt + 'val:ue', tokens: { comparator, operator: '$eq', suffix: undefined, value: 'val:ue' } },
{ string: cSrt + '$not:value', tokens: { comparator, operator: '$eq', suffix: '$not', value: 'value' } },
{
string: cSrt + '$eq:$not:value',
tokens: { comparator, operator: '$eq', suffix: '$not', value: 'value' },
},
{
string: cSrt + '$eq:$null',
tokens: { comparator, operator: '$null', suffix: undefined, value: undefined },
},
{ string: cSrt + '$null', tokens: { comparator, operator: '$null', suffix: undefined, value: undefined } },
{ string: cSrt + '', tokens: { comparator, operator: '$eq', suffix: undefined, value: '' } },
{
string: cSrt + '$eq:$not:$in:value',
tokens: { comparator, operator: '$in', suffix: '$not', value: 'value' },
},
{
string: cSrt + '$eq:$not:value:$in',
tokens: { comparator, operator: '$eq', suffix: '$not', value: 'value:$in' },
},
{
string: cSrt + '$eq:$not:$null:value:$in',
tokens: { comparator, operator: '$null', suffix: '$not', value: undefined },
},
])('should get filter tokens for "$string"', ({ string, tokens }) => {
expect((0, filter_1.parseFilterToken)(string)).toStrictEqual(tokens);
});
}
it('should return result based on or between range filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterOperator.BTW],
},
};
const query = {
path: '',
filter: {
age: ['$btw:4,5', '$or:$btw:5,6'],
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$btw:4,5&filter.age=$or:$btw:5,6');
});
it('should return result based on or with all cats', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterOperator.BTW],
},
};
const query = {
path: '',
filter: {
age: ['$null', '$or:$not:$eq:$null'],
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([...cats]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$null&filter.age=$or:$not:$eq:$null');
});
it('should return result based on two ors and an and with two cats', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
age: [filter_1.FilterOperator.BTW],
name: true,
color: true,
},
};
const query = {
path: '',
filter: {
name: '$or:Milo',
color: '$or:white',
age: '$btw:1,10',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.filter((cat) => (cat.name === 'Milo' || cat.color === 'white') && cat.age));
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$or:Milo&filter.color=$or:white&filter.age=$btw:1,10');
});
it('should return result based on two multifilters chained together with and operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: true,
color: true,
},
};
const query = {
path: '',
filter: {
name: ['Milo', '$or:Garfield'],
color: ['brown', '$or:white'],
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expected = cats.filter((cat) => (cat.name === 'Milo' || cat.name === 'Garfield') && (cat.color === 'brown' || cat.color === 'white'));
expect(result.data).toStrictEqual(expected);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=Milo&filter.name=$or:Garfield&filter.color=brown&filter.color=$or:white');
});
it('should return result based on two multifilters chained together with or operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: true,
color: true,
},
};
const query = {
path: '',
filter: {
name: ['$or:Milo', '$or:Garfield'],
color: ['$or:brown', '$or:white'],
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expected = cats.filter((cat) => cat.name === 'Milo' || cat.name === 'Garfield' || cat.color === 'brown' || cat.color === 'white');
expect(result.data).toStrictEqual(expected);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$or:Milo&filter.name=$or:Garfield&filter.color=$or:brown&filter.color=$or:white');
});
it('should return result based on filters chained together with and operators and or operators', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
name: true,
color: true,
age: true,
cutenessLevel: true,
},
};
const query = {
path: '',
filter: {
name: ['$or:Milo', '$or:Garfield'],
age: '$or:$null',
color: ['brown', '$or:white'],
cutenessLevel: [cat_entity_1.CutenessLevel.HIGH, `$or:${cat_entity_1.CutenessLevel.LOW}`],
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expected = cats.filter((cat) => (cat.name === 'Milo' || cat.name === 'Garfield' || cat.age === null) &&
(cat.color === 'brown' || cat.color === 'white') &&
(cat.cutenessLevel === cat_entity_1.CutenessLevel.HIGH || cat.cutenessLevel === cat_entity_1.CutenessLevel.LOW));
expect(result.data).toStrictEqual(expected);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$or:Milo&filter.name=$or:Garfield&filter.age=$or:$null&filter.color=brown&filter.color=$or:white&filter.cutenessLevel=high&filter.cutenessLevel=$or:low');
});
it("should return primary columns if select doesn't contain all primary columns", async () => {
const config = {
sortableColumns: ['id', 'name'],
select: ['name'],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat, index) => {
expect(cat.id).toBe(cats[index].id);
expect(cat.name).toBe(cats[index].name);
});
expect(result.meta.select).toStrictEqual(undefined);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('should return all items even if deleted', async () => {
const config = {
sortableColumns: ['id'],
withDeleted: true,
};
const query = {
path: '',
};
await catRepo.softDelete({ id: cats[0].id });
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.totalItems).toBe(cats.length);
await catRepo.restore({ id: cats[0].id });
});
it('should return all relation items even if deleted', async () => {
const config = {
sortableColumns: ['id'],
withDeleted: true,
relations: ['cat'],
};
const query = {
path: '',
};
await catRepo.softDelete({ id: cats[0].id });
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.data[0].cat).not.toBeNull();
await catRepo.restore({ id: cats[0].id });
});
it('should return only undeleted items', async () => {
const config = {
sortableColumns: ['id'],
withDeleted: false,
};
const query = {
path: '',
};
await catRepo.softDelete({ id: cats[0].id });
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.totalItems).toBe(cats.length - 1);
await catRepo.restore({ id: cats[0].id });
});
it('should return the specified columns only', async () => {
const config = {
sortableColumns: ['id'],
select: ['id', 'name'],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
expect(cat.name).toBeDefined();
expect(cat.color).not.toBeDefined();
});
expect(result.meta.select).toBe(undefined);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('should ignore query select', async () => {
const config = {
sortableColumns: ['id'],
};
const query = {
path: '',
select: ['id', 'name'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
expect(cat.name).toBeDefined();
expect(cat.color).toBeDefined();
});
expect(result.meta.select).toEqual(undefined);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('should only query select columns which have been config selected', async () => {
const config = {
sortableColumns: ['id'],
select: ['id', 'name', 'color'],
};
const query = {
path: '',
select: ['id', 'color', 'age'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
expect(cat.name).not.toBeDefined();
expect(cat.color).toBeDefined();
expect(cat.age).not.toBeDefined();
});
expect(result.meta.select).toEqual(['id', 'color']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,color');
});
it('should return the specified relationship columns only', async () => {
const config = {
sortableColumns: ['name'],
select: ['id', 'name', 'toys.name'],
relations: ['toys'],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
expect(cat.name).toBeDefined();
expect(cat.color).not.toBeDefined();
cat.toys.map((toy) => {
expect(toy.name).toBeDefined();
expect(toy.id).not.toBeDefined();
});
});
expect(result.meta.select).toBe(undefined);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=name:ASC');
});
it('should return selected columns', async () => {
const config = {
sortableColumns: ['id', 'name'],
select: ['id', 'name', 'toys.name', 'toys.(size.height)', 'toys.(size.length)'],
relations: ['toys'],
};
const query = {
path: '',
select: ['id', 'toys.(size.height)'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
expect(cat.name).not.toBeDefined();
});
result.data.forEach((cat) => {
if (cat.id === 1 || cat.id === 2) {
const toy = cat.toys[0];
expect(toy.name).not.toBeDefined();
expect(toy.id).not.toBeDefined();
expect(toy.size.height).toBeDefined();
}
else {
expect(cat.toys).toHaveLength(0);
}
});
expect(result.meta.select).toStrictEqual(['id', 'toys.(size.height)']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,toys.(size.height)');
});
it('should only select columns via query which are selected in config', async () => {
const config = {
select: ['id', 'home.id', 'home.pillows.id'],
relations: { home: { pillows: true, naptimePillow: { brand: true } } },
sortableColumns: ['id', 'name'],
};
const query = {
path: '',
select: ['id', 'home.id', 'home.name'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
if (cat.id === 1 || cat.id === 2 || cat.id == 3) {
expect(cat.home.id).toBeDefined();
expect(cat.home.name).not.toBeDefined();
}
else {
expect(cat.home).toBeNull();
}
});
expect(result.meta.select).toStrictEqual(['id', 'home.id']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,home.id');
});
it('should return the specified nested relationship columns only', async () => {
const config = {
select: ['id', 'home.id', 'home.pillows.id', 'home.naptimePillow.brand'],
relations: { home: { pillows: true, naptimePillow: { brand: true } } },
sortableColumns: ['id', 'name'],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
result.data.forEach((cat) => {
expect(cat.id).toBeDefined();
expect(cat.name).not.toBeDefined();
if (cat.id === 1 || cat.id === 2 || cat.id == 3) {
expect(cat.home.id).toBeDefined();
expect(cat.home.name).not.toBeDefined();
expect(cat.home.countCat).not.toBeDefined();
cat.home.pillows.forEach((pillow) => {
expect(pillow.id).toBeDefined();
expect(pillow.color).not.toBeDefined();
});
}
else {
expect(cat.home).toBeNull();
}
});
expect(result.meta.select).toBe(undefined);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('should return the right amount of results if a many to many relation is involved', async () => {
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
relations: ['friends'],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.totalItems).toBe(7);
expect(result.data.length).toBe(7);
expect(result.data[0].friends.length).toBe(6);
});
it('should return eager relations when set the property `loadEagerRelations` as true', async () => {
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
loadEagerRelations: true,
searchableColumns: ['name'],
};
const query = {
path: '',
search: 'Garfield',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data[0].toys).toBeDefined();
expect(result.data[0].toys).toHaveLength(1);
});
it('should search nested relations', async () => {
const config = {
relations: { home: { pillows: true, naptimePillow: { brand: true } } },
sortableColumns: ['id', 'name'],
searchableColumns: ['name', 'home.pillows.color', 'home.naptimePillow.brand'],
};
const query = {
path: '',
search: 'pink',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const cat = (0, lodash_1.clone)(cats[1]);
const catHomesClone = (0, lodash_1.clone)(catHomes[1]);
const catHomePillowsClone = (0, lodash_1.clone)(catHomePillows[3]);
delete catHomePillowsClone.home;
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length;
catHomesClone.pillows = [catHomePillowsClone];
cat.home = catHomesClone;
delete cat.home.cat;
expect(result.meta.search).toStrictEqual('pink');
expect(result.data).toStrictEqual([cat]);
expect(result.data[0].home).toBeDefined();
expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows);
});
it('should filter nested relations', async () => {
const config = {
relations: { home: { pillows: true, naptimePillow: { brand: true } } },
sortableColumns: ['id', 'name'],
filterableColumns: { 'home.pillows.color': [filter_1.FilterOperator.EQ] },
};
const query = {
path: '',
filter: { 'home.pillows.color': 'pink' },
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const cat = (0, lodash_1.clone)(cats[1]);
const catHomesClone = (0, lodash_1.clone)(catHomes[1]);
const catHomePillowsClone = (0, lodash_1.clone)(catHomePillows[3]);
delete catHomePillowsClone.home;
catHomesClone.countCat = cats.filter((cat) => cat.id === catHomesClone.cat.id).length;
catHomesClone.pillows = [catHomePillowsClone];
cat.home = catHomesClone;
delete cat.home.cat;
expect(result.meta.filter['home.pillows.color']).toStrictEqual('pink');
expect(result.data).toStrictEqual([cat]);
expect(result.data[0].home).toBeDefined();
expect(result.data[0].home.pillows).toStrictEqual(cat.home.pillows);
});
it('should allow all filters on a field when passing boolean', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
id: true,
},
};
const query = {
path: '',
filter: {
id: '$not:$in:1,2,5',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[2], cats[3], cats[5], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.id=$not:$in:1,2,5');
});
it('should ignore all filters on a field when not passing anything', async () => {
const config = {
sortableColumns: ['id'],
};
const query = {
path: '',
filter: {
id: '$not:$in:1,2,5',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[3], cats[4], cats[5], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.id=$not:$in:1,2,5');
});
it('should use searchBy in query param when ignoreSearchByInQueryParam is not defined', async () => {
const config = {
sortableColumns: ['id'],
searchableColumns: ['name', 'color'],
};
const query = {
path: '',
search: 'Milo',
searchBy: ['color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toEqual(0);
expect(result.meta.searchBy).toStrictEqual(['color']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Milo&searchBy=color');
});
it('should use searchBy in query param when ignoreSearchByInQueryParam is set to false', async () => {
const config = {
sortableColumns: ['id'],
ignoreSearchByInQueryParam: false,
searchableColumns: ['name', 'color'],
};
const query = {
path: '',
search: 'Milo',
searchBy: ['color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toEqual(0);
expect(result.meta.searchBy).toStrictEqual(['color']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Milo&searchBy=color');
});
it('should ignore searchBy in query param when ignoreSearchByInQueryParam is set to true', async () => {
const config = {
sortableColumns: ['id'],
ignoreSearchByInQueryParam: true,
searchableColumns: ['name', 'color'],
};
const query = {
path: '',
search: 'Milo',
searchBy: ['color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data.length).toEqual(1);
expect(result.data).toStrictEqual([cats[0]]);
expect(result.meta.searchBy).toStrictEqual(['name', 'color']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Milo');
});
it('should use select in query param when ignoreSelectInQueryParam is not defined', async () => {
const config = {
sortableColumns: ['id'],
select: ['id', 'name', 'color'],
};
const query = {
path: '',
select: ['id', 'color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data[0]).toEqual({ id: cats[0].id, color: cats[0].color });
expect(result.meta.select).toStrictEqual(['id', 'color']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,color');
});
it('should use select in query param when ignoreSelectInQueryParam is set to false', async () => {
const config = {
sortableColumns: ['id'],
ignoreSelectInQueryParam: false,
select: ['id', 'name', 'color'],
};
const query = {
path: '',
select: ['id', 'color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data[0]).toEqual({ id: cats[0].id, color: cats[0].color });
expect(result.meta.select).toStrictEqual(['id', 'color']);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,color');
});
it('should ignore select in query param when ignoreSelectInQueryParam is set to true', async () => {
const config = {
sortableColumns: ['id'],
ignoreSelectInQueryParam: true,
select: ['id', 'name', 'color'],
};
const query = {
path: '',
select: ['id', 'color'],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data[0]).toEqual({ id: cats[0].id, color: cats[0].color, name: cats[0].name });
expect(result.meta.select).toEqual(undefined);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC');
});
it('uses custom count builder when provided', async () => {
const fakeQB = { getCount: jest.fn().mockResolvedValue(42) };
const config = {
sortableColumns: ['id'],
select: ['id', 'name', 'color'],
buildCountQuery: () => fakeQB,
};
const query = {
path: '',
select: ['id', 'color'],
};
const page = await (0, paginate_1.paginate)(query, catRepo, config);
expect(fakeQB.getCount).toHaveBeenCalledTimes(1);
expect(page.meta.totalItems).toBe(42);
});
describe('should return result based on date column filter', () => {
it('with $not and $null operators', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.NULL],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$not:$null',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$not:$null',
});
expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$not:$null');
});
it('with $lt operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.LT],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-20T10:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-20T10:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[0]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-20T10:00:00.000Z');
});
it('with $lte operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.LTE],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$lte:2022-12-20T10:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lte:2022-12-20T10:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[0], cats[1]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lte:2022-12-20T10:00:00.000Z');
});
it('with $btw operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.BTW],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$btw:2022-12-20T08:00:00.000Z,2022-12-20T12:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$btw:2022-12-20T08:00:00.000Z,2022-12-20T12:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[1]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$btw:2022-12-20T08:00:00.000Z,2022-12-20T12:00:00.000Z');
});
it('with $eq operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.EQ],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$eq:2022-12-21T10:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$eq:2022-12-21T10:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[2]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$eq:2022-12-21T10:00:00.000Z');
});
it('with $gte operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.GTE],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$gte:2022-12-20T10:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$gte:2022-12-20T10:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[1], cats[2], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$gte:2022-12-20T10:00:00.000Z');
});
it('with $gt operator', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.GT],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$gt:2022-12-20T10:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$gt:2022-12-20T10:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[2], cats[6]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$gt:2022-12-20T10:00:00.000Z');
});
it('with $lt operator and date only', async () => {
{
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.LT],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-20',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-20',
});
expect(result.data).toStrictEqual([cats[0]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-20');
}
{
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.LT],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-21',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-21',
});
expect(result.data).toStrictEqual([cats[0], cats[1]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-21');
}
});
});
describe('should correctly handle number column filter', () => {
it('with $eq operator and valid number', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.LT],
},
};
const query = {
path: '',
filter: {
lastVetVisit: '$lt:2022-12-20T10:00:00.000Z',
},
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.filter).toStrictEqual({
lastVetVisit: '$lt:2022-12-20T10:00:00.000Z',
});
expect(result.data).toStrictEqual([cats[0]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.lastVetVisit=$lt:2022-12-20T10:00:00.000Z');
});
});
if (process.env.DB === 'postgres') {
describe('should return results for an array column', () => {
it.each `
operator | data | expectedIndexes
${'$not:$null'} | ${undefined} | ${[0, 1, 2, 3]}
${'$lt'} | ${2} | ${[2, 3]}
${'$lte'} | ${2} | ${[1, 2, 3]}
${'$btw'} | ${'1,2'} | ${[1, 2]}
${'$gte'} | ${2} | ${[0, 1]}
${'$gt'} | ${2} | ${[0]}
${'$contains'} | ${'brown'} | ${[0, 1]}
`('with $operator operator', async ({ operator, data, expectedIndexes }) => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
colors: true,
},
};
const queryFilter = `${operator}${data ? `:${data}` : ''}`;
const query = {
path: '',
filter: {
colors: queryFilter,
},
};
const result = await (0, paginate_1.paginate)(query, catHairRepo, config);
expect(result.meta.filter).toStrictEqual({
colors: queryFilter,
});
expect(result.data).toStrictEqual(expectedIndexes.map((index) => catHairs[index]));
expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&filter.colors=${queryFilter}`);
});
it('should work with search', async () => {
const config = {
sortableColumns: ['id'],
searchableColumns: ['colors'],
};
const query = {
path: '',
search: 'brown',
};
const result = await (0, paginate_1.paginate)(query, catHairRepo, config);
expect(result.meta.search).toStrictEqual('brown');
expect(result.data).toStrictEqual([catHairs[0], catHairs[1]]);
expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&search=brown`);
});
});
}
if (process.env.DB === 'postgres') {
describe('should be able to filter on jsonb columns', () => {
beforeAll(async () => {
underCoats = await catHairRepo.save([
catHairRepo.create({
name: 'full',
colors: ['orange'],
metadata: { length: 50, thickness: 2 },
underCoat: catHairs[0],
}),
]);
});
it('should filter with single value', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'metadata.length': true,
},
};
const query = {
path: '',
filter: {
'metadata.length': '$eq:5',
},
};
const result = await (0, paginate_1.paginate)(query, catHairRepo, config);
expect(result.meta.filter).toStrictEqual({
'metadata.length': '$eq:5',
});
expect(result.data).toStrictEqual([catHairs[0]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.metadata.length=$eq:5');
});
it('should filter with multiple values', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'metadata.length': true,
'metadata.thickness': true,
},
};
const query = {
path: '',
filter: {
'metadata.length': '$eq:0.5',
'metadata.thickness': '$eq:10',
},
};
const result = await (0, paginate_1.paginate)(query, catHairRepo, config);
expect(result.meta.filter).toStrictEqual({
'metadata.length': '$eq:0.5',
'metadata.thickness': '$eq:10',
});
expect(result.data).toStrictEqual([catHairs[2]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.metadata.length=$eq:0.5&filter.metadata.thickness=$eq:10');
});
it('should filter on a nested property through a relation', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'underCoat.metadata.length': true,
},
relations: ['underCoat'],
};
const query = {
path: '',
filter: {
'underCoat.metadata.length': '$eq:50',
},
};
const result = await (0, paginate_1.paginate)(query, catHairRepo, config);
expect(result.meta.filter).toStrictEqual({
'underCoat.metadata.length': '$eq:50',
});
expect(result.data).toStrictEqual([underCoats[0]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.underCoat.metadata.length=$eq:50');
});
});
}
if (process.env.DB !== 'postgres') {
describe('should return result based on virtual column', () => {
it('should return result sorted and filter by a virtual column in main entity', async () => {
const config = {
sortableColumns: ['countCat'],
relations: ['cat', 'naptimePillow.brand'],
filterableColumns: {
countCat: [filter_1.FilterOperator.GT],
},
};
const query = {
path: '',
filter: {
countCat: '$gt:0',
},
sortBy: [['countCat', 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catHomeRepo, config);
expect(result.data).toStrictEqual([catHomes[0], catHomes[1], catHomes[2]]);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=countCat:ASC&filter.countCat=$gt:0');
});
it('should return result based on virtual column filter', async () => {
const config = {
sortableColumns: ['id'],
filterableColumns: {
'home.countCat': [filter_1.FilterOperator.GT],
},
relations: ['home', 'home.naptimePillow.brand'],
};
const query = {
path: '',
filter: {
'home.countCat': '$gt:0',
},
sortBy: [['id', 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expectedResult = [0, 1, 2].map((i) => {
const ret = Object.assign((0, lodash_1.clone)(cats[i]), { home: Object.assign((0, lodash_1.clone)(catHomes[i])) });
delete ret.home.cat;
return ret;
});
expect(result.data).toStrictEqual(expectedResult);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.home.countCat=$gt:0');
});
it('should return result sorted by a virtual column', async () => {
const config = {
sortableColumns: ['home.countCat'],
relations: ['home', 'home.naptimePillow.brand'],
};
const query = {
path: '',
sortBy: [['home.countCat', 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const expectedResult = [3, 4, 5, 6, 0, 1, 2].map((i) => {
const ret = (0, lodash_1.clone)(cats[i]);
if (i < 3) {
ret.home = (0, lodash_1.clone)(catHomes[i]);
ret.home.countCat = cats.filter((cat) => cat.id === ret.home.cat.id).length;
delete ret.home.cat;
}
else {
ret.home = null;
}
return ret;
});
expect(result.data).toStrictEqual(expectedResult);
expect(result.links.current).toBe('?page=1&limit=20&sortBy=home.countCat:ASC');
});
});
}
describe('cursor pagination', () => {
describe('sortBy: id', () => {
it('should paginate using cursor (sortableColumns[0], ASC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 2,
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 2));
expect(result.meta.itemsPerPage).toBe(2);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe('?limit=2&sortBy=id:DESC&cursor=V00000000001V0000'); // id=1, DESC (Milo) -> V + LPAD(1, 11, '0') + V + LPAD(0, 4, '0')
expect(result.links.next).toBe(`?limit=2&sortBy=id:ASC&cursor=V99999999998X0000`); // id=2, ASC (Garfield) -> V + 10^11 - 2 + X + LPAD(0, 4, '0')
});
it('should paginate using cursor (id, ASC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 2,
};
const query = {
path: '',
sortBy: [['id', 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(0, 2));
expect(result.meta.itemsPerPage).toBe(2);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe('?limit=2&sortBy=id:DESC&cursor=V00000000001V0000'); // id=1, DESC (Milo) -> V + LPAD(1, 11, '0') + V + LPAD(0, 4, '0')
expect(result.links.next).toBe(`?limit=2&sortBy=id:ASC&cursor=V99999999998X0000`); // id=2, ASC (Garfield) -> V + 10^11 - 2 + X + LPAD(0, 4, '0')
});
it('should paginate using cursor with specific cursor value (id, ASC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 2,
};
const query = {
path: '',
cursor: 'V99999999998X0000', // id=2, ASC (Garfield)
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(2, 4));
expect(result.meta.itemsPerPage).toBe(2);
expect(result.meta.cursor).toBe('V99999999998X0000');
expect(result.links.previous).toBe(`?limit=2&sortBy=id:DESC&cursor=V00000000003V0000`); // id=3, DESC (Shadow) -> V + LPAD(3, 11, '0') + V + LPAD(0, 4, '0')
expect(result.links.next).toBe(`?limit=2&sortBy=id:ASC&cursor=V99999999996X0000`); // id=4, ASC (George) -> V + 10^11 - 4 + X + LPAD(0, 4, '0')
});
it('should handle end of data with cursor pagination (id, ASC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 10,
};
const query = {
path: '',
cursor: 'V99999999993X0000', // id=7, ASC (Baby) -> V + 10^11 - 7 + X + LPAD(0, 4, '0')
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([]);
expect(result.meta.itemsPerPage).toBe(0);
expect(result.meta.cursor).toBe('V99999999993X0000');
expect(result.links.previous).toBeUndefined();
expect(result.links.next).toBeUndefined();
});
it('should paginate using cursor (id, DESC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 2,
};
const query = {
path: '',
sortBy: [['id', 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(5, 7).reverse());
expect(result.meta.itemsPerPage).toBe(2);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe(`?limit=2&sortBy=id:ASC&cursor=V99999999993X0000`); // id=7, ASC (Adam) -> V + 10^11 - 7 + X + LPAD(0, 4, '0')
expect(result.links.next).toBe('?limit=2&sortBy=id:DESC&cursor=V00000000006V0000'); // id=6, DESC (Baby) -> V + LPAD(6, 11, '0') + V + LPAD(0, 4, '0')
});
it('should paginate using cursor with specific cursor value (id, DESC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 2,
};
const query = {
path: '',
cursor: 'V00000000004V0000', // id=4, DESC (George) -> V + LPAD(4, 11, '0') + V + LPAD(0, 4, '0')
sortBy: [['id', 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual(cats.slice(1, 3).reverse());
expect(result.meta.itemsPerPage).toBe(2);
expect(result.meta.cursor).toBe('V00000000004V0000');
expect(result.links.previous).toBe(`?limit=2&sortBy=id:ASC&cursor=V99999999997X0000`); // id=3, ASC (Shadow) -> V + 10^11 - 3 + X + LPAD(0, 4, '0')
expect(result.links.next).toBe(`?limit=2&sortBy=id:DESC&cursor=V00000000002V0000`); // id=2, DESC (Garfield) -> V + LPAD(2, 11, '0') + V + LPAD(0, 4, '0')
});
it('should handle end of data with cursor pagination (id, DESC)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 10,
};
const query = {
path: '',
cursor: 'V00000000001V0000', // id=1, DESC (Milo) -> V + LPAD(1, 11, '0') + V + LPAD(0, 4, '0')
sortBy: [['id', 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([]);
expect(result.meta.itemsPerPage).toBe(0);
expect(result.meta.cursor).toBe('V00000000001V0000');
expect(result.links.previous).toBeUndefined();
expect(result.links.next).toBeUndefined();
});
});
describe('sortBy: lastVetVisit', () => {
it('should handle date type cursor column (lastVetVisit, ASC)', async () => {
const config = {
sortableColumns: ['id', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['lastVetVisit', 'ASC']],
defaultLimit: 2,
};
const query = {
path: '',
cursor: 'V998328556000000', // lastVetVisit=2022-12-19T10:00:00.000Z, ASC (Milo) -> V + 10^15 - 1671444000000
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[1], cats[2]]); // Garfield, Shadow
expect(result.links.previous).toBe(`?limit=2&sortBy=lastVetVisit:DESC&cursor=V001671530400000`); // lastVetVisit=2022-12-20T10:00:00.000Z, DESC (Garfield) -> V + LPAD(1671530400000, 15, '0')
expect(result.links.next).toBe(`?limit=2&sortBy=lastVetVisit:ASC&cursor=V998328383200000`); // lastVetVisit=2022-12-21T10:00:00.000Z, ASC (Shadow) -> V + 10^15 - 1671616800000
});
it('should handle date type cursor column with reverse order (lastVetVisit, DESC)', async () => {
const config = {
sortableColumns: ['id', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['lastVetVisit', 'DESC']],
defaultLimit: 2,
};
const query = {
path: '',
cursor: 'V001671616800000', // lastVetVisit=2022-12-21T10:00:00.000Z, DESC (Shadow) -> V + LPAD(1671616800000, 15, '0')
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[1], cats[0]]); // Garfield, Milo
expect(result.links.previous).toBe(`?limit=2&sortBy=lastVetVisit:ASC&cursor=V998328469600000`); // lastVetVisit=2022-12-20T10:00:00.000Z, ASC (Garfield) -> V + 10^15 - 1671530400000
expect(result.links.next).toBe(`?limit=2&sortBy=lastVetVisit:DESC&cursor=V001671444000000`); // lastVetVisit=2022-12-19T10:00:00.000Z, DESC (Milo) -> V + LPAD(1671444000000, 15, '0')
});
// The range of mysql timestamp is from 1970-01-01 00:00:01
if (process.env.DB !== 'mariadb') {
it('should handle date type cursor column with zero timestamp (lastVetVisit, ASC)', async () => {
// Create a new cat with lastVetVisit = new Date(0)
const zeroDateCat = await catRepo.save(catRepo.create({
name: 'ZeroCat',
color: 'grey',
age: 1,
cutenessLevel: cat_entity_1.CutenessLevel.LOW,
lastVetVisit: isoStringToDate('1970-01-01T00:00:00.000Z'), // new Date(0)
size: { height: 20, width: 10, length: 30 },
weightChange: 0,
}));
const config = {
sortableColumns: ['id', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['lastVetVisit', 'ASC']],
defaultLimit: 2,
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Should appear first as it has the earliest possible timestamp
expect(result.data[0]).toStrictEqual(zeroDateCat);
expect(result.links.previous).toBe(`?limit=2&sortBy=lastVetVisit:DESC&cursor=V000000000000000`); // lastVetVisit=1970-01-01T00:00:00.000Z, DESC (ZeroCat)
expect(result.links.next).toBe(`?limit=2&sortBy=lastVetVisit:ASC&cursor=V998328556000000`); // lastVetVisit=2022-12-19T10:00:00.000Z, ASC (Milo)
// Clean up
await catRepo.remove(zeroDateCat);
});
it('should handle date type cursor column with zero timestamp (lastVetVisit, DESC)', async () => {
// Create a new cat with lastVetVisit = new Date(0)
const zeroDateCat = await catRepo.save(catRepo.create({
name: 'ZeroCat',
color: 'grey',
age: 1,
cutenessLevel: cat_entity_1.CutenessLevel.LOW,
lastVetVisit: isoStringToDate('1970-01-01T00:00:00.000Z'), // new Date(0)
size: { height: 20, width: 10, length: 30 },
weightChange: 0,
}));
const config = {
sortableColumns: ['id', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['lastVetVisit', 'DESC']],
defaultLimit: 2,
filterableColumns: {
lastVetVisit: [filter_1.FilterOperator.NULL, filter_1.FilterSuffix.NOT],
},
};
const query = {
path: '',
filter: { lastVetVisit: '$not:$null' }, // to ensure null values are not included
cursor: 'V001671444000000', // lastVetVisit=2022-12-19T10:00:00.000Z, DESC (Milo)
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Should appear last as it has the earliest possible timestamp
expect(result.data[result.data.length - 1]).toStrictEqual(zeroDateCat);
expect(result.links.previous).toBe(`?limit=2&sortBy=lastVetVisit:ASC&filter.lastVetVisit=$not:$null&cursor=X000000000000000`); // lastVetVisit=1970-01-01T00:00:00.000Z, ASC (ZeroCat)
expect(result.links.next).toBe(`?limit=2&sortBy=lastVetVisit:DESC&filter.lastVetVisit=$not:$null&cursor=V000000000000000`); // lastVetVisit=1970-01-01T00:00:00.000Z, DESC (ZeroCat)
// Clean up
await catRepo.remove(zeroDateCat);
});
}
});
describe('sortBy: age, lastVetVisit', () => {
it('should handle multiple cursor columns checking sorted data (age:ASC, lastVetVisit:ASC)', async () => {
const config = {
sortableColumns: ['age', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
};
const query = {
path: '',
sortBy: [
['age', 'ASC'],
['lastVetVisit', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
const ageNullCats = cats.filter((cat) => cat.age === null);
const sortedAgeNotNullCats = ageNotNullCats.sort((a, b) => {
if (a.age === b.age) {
return a.lastVetVisit.getTime() - b.lastVetVisit.getTime();
}
return a.age - b.age;
});
const sortedAgeNullCats = ageNullCats.sort((a, b) => {
return a.lastVetVisit.getTime() - b.lastVetVisit.getTime();
});
expect(result.data).toStrictEqual([...sortedAgeNotNullCats, ...sortedAgeNullCats]);
expect(result.meta.cursor).toBeUndefined();
expect(result.meta.sortBy).toStrictEqual([
['age', 'ASC'],
['lastVetVisit', 'ASC'],
]);
expect(result.links.previous).toBe('?limit=20&sortBy=age:DESC&sortBy=lastVetVisit:DESC&cursor=N00000000000X0000A000000000000000'); // age=0, DESC + lastVetVisit=null, DESC (Baby) -> N + LPAD(0, 11, '0') + X + LPAD(0, 4, '0') + A + LPAD(0, 15, '0')
expect(result.links.next).toBe('?limit=20&sortBy=age:ASC&sortBy=lastVetVisit:ASC&cursor=A000000000000000A000000000000000'); // age=null, ASC + lastVetVisit=null, ASC (Leche) -> A + LPAD(0, 15, '0') + A + LPAD(0, 15, '0')
});
it('should handle multiple cursor columns checking combined cursor (age:ASC, lastVetVisit:ASC)', async () => {
const config = {
sortableColumns: ['age', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 4,
};
const query = {
path: '',
sortBy: [
['age', 'ASC'],
['lastVetVisit', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data).toStrictEqual([cats[5], cats[3], cats[2], cats[6]]); // Baby, George, Shadow, Adam
expect(result.meta.itemsPerPage).toBe(4);
expect(result.meta.cursor).toBeUndefined();
expect(result.meta.sortBy).toStrictEqual([
['age', 'ASC'],
['lastVetVisit', 'ASC'],
]);
expect(result.links.previous).toBe('?limit=4&sortBy=age:DESC&sortBy=lastVetVisit:DESC&cursor=N00000000000X0000A000000000000000'); // age=0, DESC + lastVetVisit=null, DESC (Baby) -> N + LPAD(0, 11, '0') + X + LPAD(0, 4, '0') + A + LPAD(0, 15, '0')
expect(result.links.next).toBe('?limit=4&sortBy=age:ASC&sortBy=lastVetVisit:ASC&cursor=V99999999996X0000V998328296800000'); // age=4, ASC + lastVetVisit=2022-12-22T10:00:00.000Z, ASC (Adam) -> V + 10^11 - 4 + X + LPAD(0, 4, '0') + V + 10^15 - 1671703200000
const result2 = await (0, paginate_1.paginate)(Object.assign(Object.assign({}, query), { cursor: 'V99999999996X0000V998328296800000' }), catRepo, config);
expect(result2.data).toStrictEqual([cats[1], cats[0], cats[4]]); // Garfield, Milo, Leche
expect(result2.meta.cursor).toBe('V99999999996X0000V998328296800000');
expect(result2.meta.sortBy).toStrictEqual([
['age', 'ASC'],
['lastVetVisit', 'ASC'],
]);
expect(result2.links.previous).toBe('?limit=4&sortBy=age:DESC&sortBy=lastVetVisit:DESC&cursor=V00000000005V0000V001671530400000'); // age=5, DESC + lastVetVisit=2022-12-20T10:00:00.000Z, DESC (Garfield) -> V + LPAD(5, 11, '0') + V + LPAD(0, 4, '0') + V + LPAD(1671530400000, 15, '0')
expect(result2.links.next).toBe(`?limit=4&sortBy=age:ASC&sortBy=lastVetVisit:ASC&cursor=A000000000000000A000000000000000`); // age=null, ASC + lastVetVisit=null, ASC (Leche) -> A + LPAD(0, 15, '0') + A + LPAD(0, 15, '0')
});
it('should handle multiple cursor columns with different directions (age:DESC, lastVetVisit:ASC)', async () => {
const config = {
sortableColumns: ['age', 'lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
};
const query = {
path: '',
sortBy: [
['age', 'DESC'],
['lastVetVisit', 'ASC'],
],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
const ageNullCats = cats.filter((cat) => cat.age === null);
const sortedAgeNotNullCats = ageNotNullCats.sort((a, b) => {
if (a.age === b.age) {
return a.lastVetVisit.getTime() - b.lastVetVisit.getTime();
}
return b.age - a.age;
});
const sortedAgeNullCats = ageNullCats.sort((a, b) => {
return a.lastVetVisit.getTime() - b.lastVetVisit.getTime();
});
expect(result.data).toStrictEqual([...sortedAgeNotNullCats, ...sortedAgeNullCats]); // Milo, Garfield, Shadow, Adam, George, Baby, Leche
expect(result.meta.cursor).toBeUndefined();
expect(result.meta.sortBy).toStrictEqual([
['age', 'DESC'],
['lastVetVisit', 'ASC'],
]);
expect(result.links.previous).toBe('?limit=20&sortBy=age:ASC&sortBy=lastVetVisit:DESC&cursor=V99999999994X0000V001671444000000'); // age=6, ASC + lastVetVisit=2022-12-19T10:00:00.000Z DESC (Milo) -> V + (10^11 - 6) + X + LPAD(0, 4, '0') + V + LPAD(1671444000000, 15, '0')
expect(result.links.next).toBe(`?limit=20&sortBy=age:DESC&sortBy=lastVetVisit:ASC&cursor=A000000000000000A000000000000000`); // age=null, DESC + lastVetVisit=null, ASC (Leche) -> A + LPAD(0, 15, '0') + A + LPAD(0, 15, '0')
});
});
describe('handling null and 0 values', () => {
it('should handle 0 and null distinctly (DESC)', async () => {
const config = {
sortableColumns: ['age'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['age', 'DESC']],
};
const result = await (0, paginate_1.paginate)({ path: '', limit: 6 }, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
expect(result.data).toEqual(expect.arrayContaining(ageNotNullCats)); // If there are multiple data with the same age value, sorting is not guaranteed among them
expect(result.meta.itemsPerPage).toBe(6);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe('?limit=6&sortBy=age:ASC&cursor=V99999999994X0000'); // age=6 ASC (Milo) -> V + 10^11 - 6 + X + LPAD(0, 4, '0')
expect(result.links.next).toBe('?limit=6&sortBy=age:DESC&cursor=N00000000000X0000'); // age=0 DESC (Baby) -> V + LPAD(0, 11, '0') + X + LPAD(0, 4, '0')
const result2 = await (0, paginate_1.paginate)({ path: '', cursor: 'N00000000000X0000', limit: 6 }, catRepo, config);
expect(result2.data).toStrictEqual([cats[4]]); // Leche
expect(result2.meta.itemsPerPage).toBe(1);
expect(result2.meta.cursor).toBe('N00000000000X0000');
expect(result2.links.previous).toBe('?limit=6&sortBy=age:ASC&cursor=A000000000000000'); // age=null ASC (Leche) -> A + LPAD(0, 15, '0')
expect(result2.links.current).toBe('?limit=6&sortBy=age:DESC&cursor=N00000000000X0000');
expect(result2.links.next).toBe('?limit=6&sortBy=age:DESC&cursor=A000000000000000'); // age=null DESC (Leche) -> A + LPAD(0, 15, '0')
});
it('should handle 0 and null distinctly (ASC)', async () => {
const config = {
sortableColumns: ['age'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['age', 'ASC']],
};
const result = await (0, paginate_1.paginate)({ path: '', limit: 1 }, catRepo, config);
expect(result.data).toStrictEqual([cats[5]]); // Baby
expect(result.meta.itemsPerPage).toBe(1);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe('?limit=1&sortBy=age:DESC&cursor=N00000000000X0000'); // age=0 DESC (Baby) -> N + LPAD(0, 11, '0') + X + LPAD(0, 4, '0')
expect(result.links.next).toBe('?limit=1&sortBy=age:ASC&cursor=X00000000000X0000'); // age=0 ASC (Baby) -> X + LPAD(0, 11, '0') + X + LPAD(0, 4, '0')
const result2 = await (0, paginate_1.paginate)({ path: '', cursor: 'X00000000000X0000' }, catRepo, config);
const catsExceptBaby = cats.filter((cat) => cat.name !== 'Baby');
expect(result2.data).toEqual(expect.arrayContaining(catsExceptBaby)); // cats except Baby
expect(result2.meta.cursor).toBe('X00000000000X0000');
expect(result2.links.previous).toBe('?limit=20&sortBy=age:DESC&cursor=V00000000003V0000'); // age=3 DESC (George) -> V + LPAD(3, 11, '0') + X + LPAD(0, 4, '0')
expect(result2.links.current).toBe('?limit=20&sortBy=age:ASC&cursor=X00000000000X0000');
expect(result2.links.next).toBe('?limit=20&sortBy=age:ASC&cursor=A000000000000000'); // age=null ASC (Leche) -> A + LPAD(0, 15, '0')
});
});
describe('with other options', () => {
it('should work with filter and cursor pagination (sortBy id + filter color)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultLimit: 2,
filterableColumns: {
color: [filter_1.FilterOperator.EQ],
},
};
const query = {
path: '',
filter: { color: 'white' },
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const whiteCats = cats.filter((cat) => cat.color === 'white');
expect(result.data).toStrictEqual(whiteCats.slice(0, 2));
expect(result.links.previous).toBe('?limit=2&sortBy=id:DESC&filter.color=white&cursor=V00000000004V0000');
expect(result.links.current).toBe('?limit=2&sortBy=id:ASC&filter.color=white');
expect(result.links.next).toBe(`?limit=2&sortBy=id:ASC&filter.color=white&cursor=V99999999995X0000`);
});
it('should work with filter and cursor pagination (sortBy id + filter age)', async () => {
const config = {
sortableColumns: ['id'],
paginationType: paginate_1.PaginationType.CURSOR,
filterableColumns: {
age: [filter_1.FilterSuffix.NOT, filter_1.FilterOperator.NULL],
},
};
const query = {
path: '',
sortBy: [['id', 'ASC']],
filter: { age: '$not:$null' },
limit: 20,
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
expect(result.data).toStrictEqual(ageNotNullCats.sort((a, b) => a.id - b.id));
expect(result.links.previous).toBe('?limit=20&sortBy=id:DESC&filter.age=$not:$null&cursor=V00000000001V0000');
expect(result.links.current).toBe('?limit=20&sortBy=id:ASC&filter.age=$not:$null');
expect(result.links.next).toBe('?limit=20&sortBy=id:ASC&filter.age=$not:$null&cursor=V99999999993X0000');
});
});
describe('handling data including decimal and negative numbers', () => {
it('should handle data including decimal and negative numbers (weightChange, ASC)', async () => {
const config = {
sortableColumns: ['weightChange'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['weightChange', 'ASC']],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const sortedCats = cats.sort((a, b) => a.weightChange - b.weightChange);
expect(result.data).toEqual(sortedCats);
expect(result.links.previous).toBe('?limit=20&sortBy=weightChange:DESC&cursor=M99999999997X0000'); // weightChange=-3.00 DESC (Shadow) -> (M + 10^11 - 3) + (X + PAD(0, 4, '0'))
expect(result.links.next).toBe('?limit=20&sortBy=weightChange:ASC&cursor=V99999999995V7500'); // weightChange=5.25 ASC (Garfield) -> (V + 10^11 - 5) + (V + 10^4 - 2500)
});
it('should handle data including decimal and negative numbers (weightChange, DESC)', async () => {
const config = {
sortableColumns: ['weightChange'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['weightChange', 'DESC']],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const sortedCats = cats.sort((a, b) => b.weightChange - a.weightChange);
expect(result.data).toEqual(sortedCats);
expect(result.links.previous).toBe('?limit=20&sortBy=weightChange:ASC&cursor=V99999999995V7500'); // weightChange=5.25 ASC (Garfield) -> (V + 10^11 - 5) + (V + 10^4 - 2500)
expect(result.links.next).toBe('?limit=20&sortBy=weightChange:DESC&cursor=M99999999997X0000'); // weightChange=-3.00 DESC (Shadow) -> (M + 10^11 - 3) + (X + LPAD(0, 4, '0'))
});
it('should put null values last', async () => {
const nullCat = await catRepo.save(catRepo.create({
name: 'nullCat',
color: 'black',
age: null,
weightChange: null,
cutenessLevel: cat_entity_1.CutenessLevel.LOW,
size: { height: 0, width: 0, length: 0 },
}));
const config = {
sortableColumns: ['weightChange'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [['weightChange', 'ASC']],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.data[result.data.length - 1]).toStrictEqual(nullCat);
await catRepo.remove(nullCat);
});
it('should handle multiple cursor columns checking sorted data (age:ASC, weightChange:ASC)', async () => {
const config = {
sortableColumns: ['age', 'weightChange'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [
['age', 'ASC'],
['weightChange', 'ASC'],
],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
const ageNullCats = cats.filter((cat) => cat.age === null);
const sortedAgeNotNullCats = ageNotNullCats.sort((a, b) => {
if (a.age === b.age) {
return a.weightChange - b.weightChange;
}
return a.age - b.age;
});
const sortedAgeNullCats = ageNullCats.sort((a, b) => {
return a.weightChange - b.weightChange;
});
expect(result.data).toEqual([...sortedAgeNotNullCats, ...sortedAgeNullCats]);
expect(result.meta.cursor).toBeUndefined();
expect(result.meta.sortBy).toStrictEqual([
['age', 'ASC'],
['weightChange', 'ASC'],
]);
expect(result.links.previous).toBe('?limit=20&sortBy=age:DESC&sortBy=weightChange:DESC&cursor=N00000000000X0000V00000000000V0100'); // age=0, DESC + weightChange=0.01 DESC (Baby) -> (V + LPAD(0, 11, '0')) + (X + LPAD(0, 4, '0')) + (V + LPAD(0, 11, '0')) + (V + LPAD(100, 4, '0'))
expect(result.links.next).toBe('?limit=20&sortBy=age:ASC&sortBy=weightChange:ASC&cursor=A000000000000000Y00000000001V2500'); // age=null, ASC + weightChange=-1.25 ASC (Leche) -> (A + LPAD(0, 15, '0')) + (Y + LPAD(1, 11, '0')) + (V + LPAD(2500, 4, '0'))
});
it('should handle multiple cursor columns with different directions (age:DESC, weightChange:ASC)', async () => {
const config = {
sortableColumns: ['age', 'weightChange'],
paginationType: paginate_1.PaginationType.CURSOR,
defaultSortBy: [
['age', 'DESC'],
['weightChange', 'ASC'],
],
};
const query = {
path: '',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const ageNotNullCats = cats.filter((cat) => cat.age !== null);
const ageNullCats = cats.filter((cat) => cat.age === null);
const sortedAgeNotNullCats = ageNotNullCats.sort((a, b) => {
if (a.age === b.age) {
return a.weightChange - b.weightChange;
}
return b.age - a.age;
});
const sortedAgeNullCats = ageNullCats.sort((a, b) => {
return a.weightChange - b.weightChange;
});
expect(result.data).toEqual([...sortedAgeNotNullCats, ...sortedAgeNullCats]);
expect(result.meta.cursor).toBeUndefined();
expect(result.meta.sortBy).toStrictEqual([
['age', 'DESC'],
['weightChange', 'ASC'],
]);
expect(result.links.previous).toBe('?limit=20&sortBy=age:ASC&sortBy=weightChange:DESC&cursor=V99999999994X0000N00000000000V2500'); // age=6, ASC + weightChange=-0.75 DESC (Milo) -> (V + 10^11 - 6) + (X + LPAD(0, 4, '0')) + (N + LPAD(0, 11, '0')) + (V + LPAD(10^4 - 7500), 4, '0'))
expect(result.links.next).toBe('?limit=20&sortBy=age:DESC&sortBy=weightChange:ASC&cursor=A000000000000000Y00000000001V2500'); // age=null, DESC + weightChange=-1.25 ASC (Leche) -> (A + LPAD(0, 15, '0')) + (Y + LPAD(1, 11, '0')) + (V + LPAD(2500, 4, '0'))
});
});
describe('handling relation column', () => {
let relationTestCats;
let relationTestToys;
let relationTestCatsIds;
let relationTestToysIds;
beforeAll(async () => {
relationTestCats = await catRepo.save([
catRepo.create({
name: 'TestCat1',
color: 'orange',
age: 2,
cutenessLevel: cat_entity_1.CutenessLevel.MEDIUM,
lastVetVisit: isoStringToDate('2022-11-15T10:00:00.000Z'),
size: { height: 22, width: 8, length: 35 },
weightChange: 1.5,
}),
catRepo.create({
name: 'TestCat2',
color: 'grey',
age: 3,
cutenessLevel: cat_entity_1.CutenessLevel.HIGH,
lastVetVisit: isoStringToDate('2022-11-20T10:00:00.000Z'),
size: { height: 28, width: 12, length: 42 },
weightChange: -0.8,
}),
catRepo.create({
name: 'TestCat3',
color: 'cream',
age: 4,
cutenessLevel: cat_entity_1.CutenessLevel.LOW,
lastVetVisit: isoStringToDate('2022-11-25T10:00:00.000Z'),
size: { height: 18, width: 9, length: 38 },
weightChange: 2.2,
}),
]);
relationTestCatsIds = relationTestCats.map((cat) => cat.id);
relationTestToys = await catToyRepo.save([
catToyRepo.create({
name: 'TestToy1',
cat: relationTestCats[0],
size: { height: 3, width: 3, length: 8 },
}),
catToyRepo.create({
name: 'TestToy2',
cat: relationTestCats[0],
size: { height: 6, width: 2, length: 7 },
}),
catToyRepo.create({
name: 'TestToy3',
cat: relationTestCats[1],
size: { height: 2, width: 2, length: 10 },
}),
catToyRepo.create({
name: 'TestToy4',
cat: relationTestCats[2],
size: { height: 5, width: 5, length: 5 },
}),
]);
relationTestToysIds = relationTestToys.map((toy) => toy.id);
});
afterAll(async () => {
if (relationTestToys === null || relationTestToys === void 0 ? void 0 : relationTestToys.length) {
await catToyRepo.remove(relationTestToys);
}
if (relationTestCats === null || relationTestCats === void 0 ? void 0 : relationTestCats.length) {
await catRepo.remove(relationTestCats);
}
});
it('should handle cursor pagination on many-to-one relation column (cat.age, ASC)', async () => {
const config = {
sortableColumns: ['cat.age'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
defaultLimit: 4,
};
const query = {
path: '',
sortBy: [['cat.age', 'ASC']],
filter: { id: `$in:${relationTestToysIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toBe(4);
expect(result.data[0].cat.age).toBeLessThanOrEqual(result.data[1].cat.age);
expect(result.data[1].cat.age).toBeLessThanOrEqual(result.data[2].cat.age);
expect(result.data[2].cat.age).toBeLessThanOrEqual(result.data[3].cat.age);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe(`?limit=4&sortBy=cat.age:DESC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V00000000002V0000`);
expect(result.links.next).toBe(`?limit=4&sortBy=cat.age:ASC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V99999999996X0000`);
});
it('should handle cursor pagination on many-to-one relation column with cursor (cat.age, ASC)', async () => {
const config = {
sortableColumns: ['cat.age'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
defaultLimit: 2,
};
// First, get the first page of results
const firstPageQuery = {
path: '',
sortBy: [['cat.age', 'ASC']],
};
const firstResult = await (0, paginate_1.paginate)(firstPageQuery, catToyRepo, config);
expect(firstResult.data.length).toBe(2);
expect(firstResult.links.previous).toBe('?limit=2&sortBy=cat.age:DESC&cursor=V00000000002V0000');
expect(firstResult.links.next).toBe('?limit=2&sortBy=cat.age:ASC&cursor=V99999999998X0000');
expect(firstResult.data[0].cat.age).toBeLessThanOrEqual(firstResult.data[1].cat.age);
// Extract cursor from the next link
const cursor = firstResult.links.next.split('cursor=')[1];
expect(cursor).toBeDefined();
// Use the cursor to get the next page
const secondPageQuery = {
path: '',
sortBy: [['cat.age', 'ASC']],
cursor: cursor,
};
const secondResult = await (0, paginate_1.paginate)(secondPageQuery, catToyRepo, config);
expect(secondResult.data.length).toBe(2);
expect(firstResult.data[1].cat.age).toBeLessThanOrEqual(secondResult.data[0].cat.age);
expect(secondResult.data[0].cat.age).toBeLessThanOrEqual(secondResult.data[1].cat.age);
expect(secondResult.meta.cursor).toBe(cursor);
expect(secondResult.links.previous).toBe('?limit=2&sortBy=cat.age:DESC&cursor=V00000000003V0000');
expect(secondResult.links.next).toBe('?limit=2&sortBy=cat.age:ASC&cursor=V99999999996X0000');
});
it('should handle cursor pagination on many-to-one relation column (cat.age, DESC)', async () => {
const config = {
sortableColumns: ['cat.age'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
sortBy: [['cat.age', 'DESC']],
filter: { id: `$in:${relationTestToysIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toBe(4);
expect(result.data[0].cat.age).toBeGreaterThanOrEqual(result.data[1].cat.age);
expect(result.data[1].cat.age).toBeGreaterThanOrEqual(result.data[2].cat.age);
expect(result.data[2].cat.age).toBeGreaterThanOrEqual(result.data[3].cat.age);
expect(result.meta.cursor).toBeUndefined();
expect(result.links.previous).toBe(`?limit=20&sortBy=cat.age:ASC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V99999999996X0000`);
expect(result.links.next).toBe(`?limit=20&sortBy=cat.age:DESC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V00000000002V0000`);
});
it('should handle cursor pagination on many-to-one relation column with cursor (cat.age, DESC)', async () => {
const config = {
sortableColumns: ['cat.age'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
defaultLimit: 2,
};
// First, get the first page of results
const firstPageQuery = {
path: '',
sortBy: [['cat.age', 'DESC']],
};
const firstResult = await (0, paginate_1.paginate)(firstPageQuery, catToyRepo, config);
expect(firstResult.data.length).toBe(2);
expect(firstResult.links.previous).toBe('?limit=2&sortBy=cat.age:ASC&cursor=V99999999994X0000');
expect(firstResult.links.next).toBe('?limit=2&sortBy=cat.age:DESC&cursor=V00000000006V0000');
expect(firstResult.data[0].cat.age).toBeGreaterThanOrEqual(firstResult.data[1].cat.age);
// Extract cursor from the next link
const cursor = firstResult.links.next.split('cursor=')[1];
expect(cursor).toBeDefined();
// Use the cursor to get the next page
const secondPageQuery = {
path: '',
sortBy: [['cat.age', 'DESC']],
cursor: cursor,
};
const secondResult = await (0, paginate_1.paginate)(secondPageQuery, catToyRepo, config);
expect(secondResult.data.length).toBe(2);
expect(firstResult.data[1].cat.age).toBeGreaterThanOrEqual(secondResult.data[0].cat.age);
expect(secondResult.data[0].cat.age).toBeGreaterThanOrEqual(secondResult.data[1].cat.age);
expect(secondResult.meta.cursor).toBe(cursor);
expect(secondResult.links.previous).toBe('?limit=2&sortBy=cat.age:ASC&cursor=V99999999995X0000');
expect(secondResult.links.next).toBe('?limit=2&sortBy=cat.age:DESC&cursor=V00000000004V0000');
});
it('should handle cursor pagination on one-to-many relation column & embedded entity (toys.(size.height), ASC)', async () => {
const config = {
sortableColumns: ['toys.(size.height)'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['toys'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
sortBy: [['toys.(size.height)', 'ASC']],
filter: { id: `$in:${relationTestCatsIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Verify sort order - toys.(size.height) ASC
// Former cat should have toys with shortest height, then latter cat
if (result.data[0].toys.length > 0 && result.data[1].toys.length > 0) {
const minHeight1 = Math.min(...result.data[0].toys.map((toy) => toy.size.height));
const minHeight2 = Math.min(...result.data[1].toys.map((toy) => toy.size.height));
expect(minHeight1).toBeLessThanOrEqual(minHeight2);
}
if (result.data[1].toys.length > 0 && result.data[2].toys.length > 0) {
const minHeight1 = Math.min(...result.data[1].toys.map((toy) => toy.size.height));
const minHeight2 = Math.min(...result.data[2].toys.map((toy) => toy.size.height));
expect(minHeight1).toBeLessThanOrEqual(minHeight2);
}
expect(result.links.previous).toBe(`?limit=20&sortBy=toys.(size.height):DESC&filter.id=$in:${relationTestCatsIds.join(',')}&cursor=V00000000002V0000`);
expect(result.links.next).toBe(`?limit=20&sortBy=toys.(size.height):ASC&filter.id=$in:${relationTestCatsIds.join(',')}&cursor=V99999999995X0000`);
});
it('should handle cursor pagination on one-to-many relation column & embedded entity (toys.(size.height), ASC) for a cat that has more than one toy', async () => {
const config = {
sortableColumns: ['toys.(size.height)'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['toys'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
sortBy: [['toys.(size.height)', 'ASC']],
cursor: 'V99999999996X0000',
filter: { id: `$in:${relationTestCatsIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Verify sort order - toys.(size.height) ASC
// Former cat should have toys with shortest height, then latter cat
if (result.data[0].toys.length > 0 && result.data[1].toys.length > 0) {
const minHeight1 = Math.min(...result.data[0].toys.map((toy) => toy.size.height));
const minHeight2 = Math.min(...result.data[1].toys.map((toy) => toy.size.height));
expect(minHeight1).toBeLessThanOrEqual(minHeight2);
}
expect(result.links.previous).toBe(`?limit=20&sortBy=toys.(size.height):DESC&filter.id=$in:${relationTestCatsIds.join(',')}&cursor=V00000000005V0000`);
expect(result.links.next).toBe(`?limit=20&sortBy=toys.(size.height):ASC&filter.id=$in:${relationTestCatsIds.join(',')}&cursor=V99999999994X0000`);
});
it('should handle cursor pagination on one-to-many relation column & embedded entity (toys.(size.height), DESC)', async () => {
const config = {
sortableColumns: ['toys.(size.height)'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['toys'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
sortBy: [['toys.(size.height)', 'DESC']],
filter: { id: `$in:${relationTestCatsIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Verify sort order - toys.(size.height) DESC
// Former cat should have toys with tallest height, then latter cat
if (result.data[0].toys.length > 0 && result.data[1].toys.length > 0) {
const maxHeight1 = Math.max(...result.data[0].toys.map((toy) => toy.size.height));
const maxHeight2 = Math.max(...result.data[1].toys.map((toy) => toy.size.height));
expect(maxHeight1).toBeGreaterThanOrEqual(maxHeight2);
}
if (result.data[1].toys.length > 0 && result.data[2].toys.length > 0) {
const maxHeight1 = Math.max(...result.data[1].toys.map((toy) => toy.size.height));
const maxHeight2 = Math.max(...result.data[2].toys.map((toy) => toy.size.height));
expect(maxHeight1).toBeGreaterThanOrEqual(maxHeight2);
}
expect(result.links.previous).toBe(`?limit=20&sortBy=toys.(size.height):ASC&filter.id=$in:${relationTestCatsIds.join(',')}&cursor=V99999999994X0000`);
expect(result.links.next).toBe(`?limit=20&sortBy=toys.(size.height):DESC&filter.id=$in:${relationTestCatsIds.join(',')}&cursor=V00000000002V0000`);
});
it('should handle cursor pagination on one-to-one relation column (cat.lastVetVisit, ASC)', async () => {
const config = {
sortableColumns: ['cat.lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
sortBy: [['cat.lastVetVisit', 'ASC']],
filter: { id: `$in:${relationTestToysIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toBe(4);
// Verify sort order - cat.lastVetVisit ASC
// Toys should be ordered by their cats' last vet visit date ASC
const date1 = new Date(result.data[0].cat.lastVetVisit).getTime();
const date2 = new Date(result.data[1].cat.lastVetVisit).getTime();
const date3 = new Date(result.data[2].cat.lastVetVisit).getTime();
const date4 = new Date(result.data[3].cat.lastVetVisit).getTime();
expect(date1).toBeLessThanOrEqual(date2);
expect(date2).toBeLessThanOrEqual(date3);
expect(date3).toBeLessThanOrEqual(date4);
expect(result.links.previous).toBe(`?limit=20&sortBy=cat.lastVetVisit:DESC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V001668506400000`);
expect(result.links.next).toBe(`?limit=20&sortBy=cat.lastVetVisit:ASC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V998330629600000`);
});
it('should handle cursor pagination on one-to-one relation column (cat.lastVetVisit, DESC)', async () => {
const config = {
sortableColumns: ['cat.lastVetVisit'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
filterableColumns: {
id: [filter_1.FilterOperator.IN],
},
};
const query = {
path: '',
sortBy: [['cat.lastVetVisit', 'DESC']],
filter: { id: `$in:${relationTestToysIds.join(',')}` }, // Filter by test toy IDs
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
expect(result.data.length).toBe(4);
// Verify sort order - cat.lastVetVisit DESC
// Toys should be ordered by their cats' last vet visit date DESC
const date1 = new Date(result.data[0].cat.lastVetVisit).getTime();
const date2 = new Date(result.data[1].cat.lastVetVisit).getTime();
const date3 = new Date(result.data[2].cat.lastVetVisit).getTime();
const date4 = new Date(result.data[3].cat.lastVetVisit).getTime();
expect(date1).toBeGreaterThanOrEqual(date2);
expect(date2).toBeGreaterThanOrEqual(date3);
expect(date3).toBeGreaterThanOrEqual(date4);
expect(result.links.previous).toBe(`?limit=20&sortBy=cat.lastVetVisit:ASC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V998330629600000`);
expect(result.links.next).toBe(`?limit=20&sortBy=cat.lastVetVisit:DESC&filter.id=$in:${relationTestToysIds.join(',')}&cursor=V001668506400000`);
});
it('should handle multiple cursor columns with relation (cat.age:ASC, id:DESC)', async () => {
// Configure pagination with multiple sorting criteria - cat.age ASC and id DESC
const config = {
sortableColumns: ['cat.age', 'id'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
};
const query = {
path: '',
sortBy: [
['cat.age', 'ASC'],
['id', 'DESC'],
],
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
// Verify results exist
expect(result.data.length).toBeGreaterThan(0);
// Verify sort order - first by cat.age ASC, then by id DESC
if (result.data.length >= 2) {
for (let i = 0; i < result.data.length - 1; i++) {
if (result.data[i].cat.age === result.data[i + 1].cat.age) {
// If ages are equal, ids should be in DESC order
expect(result.data[i].id).toBeGreaterThanOrEqual(result.data[i + 1].id);
}
else {
// Otherwise ages should be in ASC order
expect(result.data[i].cat.age).toBeLessThan(result.data[i + 1].cat.age);
}
}
}
expect(result.links.previous).toBe('?limit=20&sortBy=cat.age:DESC&sortBy=id:ASC&cursor=V00000000002V0000V99999999994X0000');
expect(result.links.next).toBe('?limit=20&sortBy=cat.age:ASC&sortBy=id:DESC&cursor=V99999999994X0000V00000000001V0000');
});
it('should handle cursor pagination with filter on relation column', async () => {
const config = {
sortableColumns: ['size.height'],
paginationType: paginate_1.PaginationType.CURSOR,
relations: ['cat'],
filterableColumns: {
'cat.age': [filter_1.FilterOperator.EQ],
},
defaultLimit: 4,
};
// Get target age from test data
const targetAge = relationTestCats[0].age;
const query = {
path: '',
filter: {
'cat.age': `${targetAge}`,
},
sortBy: [['size.height', 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catToyRepo, config);
// Verify all toys belong to cats with the target age
result.data.forEach((toy) => {
expect(toy.cat.age).toBe(targetAge);
});
// Verify sort order - size.height ASC
if (result.data.length >= 2) {
for (let i = 0; i < result.data.length - 1; i++) {
expect(result.data[i].size.height).toBeLessThanOrEqual(result.data[i + 1].size.height);
}
}
});
});
});
describe('Wildcard Select', () => {
it('should expand * wildcard to all main entity columns', async () => {
const query = {
page: 1,
limit: 10,
select: ['*'],
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, {
sortableColumns: ['id'],
select: ['*'],
});
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0]).toHaveProperty('color');
expect(result.data[0]).toHaveProperty('age');
expect(result.data[0]).toHaveProperty('cutenessLevel');
expect(result.data[0]).toHaveProperty('lastVetVisit');
expect(result.data[0]).toHaveProperty('createdAt');
expect(result.data[0]).toHaveProperty('deletedAt');
expect(result.data[0]).toHaveProperty('weightChange');
expect(result.data[0]).toHaveProperty('size');
expect(result.data[0]).toHaveProperty('size.height');
expect(result.data[0]).toHaveProperty('size.width');
expect(result.data[0]).toHaveProperty('size.length');
});
it('should expand relation.* wildcard to all relation columns', async () => {
const query = {
page: 1,
limit: 10,
select: ['id', 'name', 'toys.*'],
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, {
sortableColumns: ['id'],
select: ['id', 'name', 'toys.*'],
relations: ['toys'],
});
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0]).not.toHaveProperty('color');
expect(result.data[0]).not.toHaveProperty('age');
expect(result.data[0]).not.toHaveProperty('cutenessLevel');
expect(result.data[0]).not.toHaveProperty('lastVetVisit');
expect(result.data[0]).not.toHaveProperty('createdAt');
expect(result.data[0]).not.toHaveProperty('deletedAt');
expect(result.data[0]).not.toHaveProperty('weightChange');
expect(result.data[0].toys[0]).toHaveProperty('id');
expect(result.data[0].toys[0]).toHaveProperty('name');
expect(result.data[0].toys[0]).toHaveProperty('createdAt');
expect(result.data[0].toys[0]).toHaveProperty('size');
expect(result.data[0].toys[0]).toHaveProperty('size.height');
expect(result.data[0].toys[0]).toHaveProperty('size.width');
expect(result.data[0].toys[0]).toHaveProperty('size.length');
});
it('should handle both * and relation.* wildcards together', async () => {
const query = {
page: 1,
limit: 10,
select: ['*', 'toys.*'],
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, {
sortableColumns: ['id'],
select: ['*', 'toys.*'],
relations: ['toys'],
});
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0]).toHaveProperty('color');
expect(result.data[0]).toHaveProperty('age');
expect(result.data[0]).toHaveProperty('cutenessLevel');
expect(result.data[0]).toHaveProperty('lastVetVisit');
expect(result.data[0]).toHaveProperty('createdAt');
expect(result.data[0]).toHaveProperty('deletedAt');
expect(result.data[0]).toHaveProperty('weightChange');
expect(result.data[0].toys[0]).toHaveProperty('id');
expect(result.data[0].toys[0]).toHaveProperty('name');
expect(result.data[0].toys[0]).toHaveProperty('createdAt');
});
it('should handle non-existent relation wildcard gracefully', async () => {
const query = {
page: 1,
limit: 10,
select: ['id', 'name', 'nonExistentRelation.*'],
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, {
sortableColumns: ['id'],
select: ['id', 'name', 'nonExistentRelation.*'],
});
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0]).not.toHaveProperty('nonExistentRelation.*');
});
it('should handle nested relation wildcards correctly', async () => {
const query = {
page: 1,
limit: 10,
select: ['*', 'toys.*', 'toys.shop.*', 'toys.shop.address.*'],
sortBy: [
['id', 'ASC'],
['toys.id', 'ASC'],
],
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, {
sortableColumns: ['id', 'toys.id'],
select: ['*', 'toys.*', 'toys.shop.*', 'toys.shop.address.*'],
relations: ['toys', 'toys.shop', 'toys.shop.address'],
});
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0].toys[1]).toHaveProperty('id');
expect(result.data[0].toys[1]).toHaveProperty('name');
expect(result.data[0].toys[1].shop).toHaveProperty('id');
expect(result.data[0].toys[1].shop).toHaveProperty('shopName');
expect(result.data[0].toys[1].shop.address).toHaveProperty('id');
expect(result.data[0].toys[1].shop.address).toHaveProperty('address');
});
it('should restrict query.select to only fields allowed in config.select', async () => {
// Server-side config only allows id and name
const config = {
sortableColumns: ['id'],
select: ['id', 'name'],
};
// Client tries to request additional fields
const query = {
page: 1,
limit: 10,
select: ['id', 'name', 'color', 'age'], // color and age not in config.select
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Should only include fields that exist in both config.select and query.select
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0]).not.toHaveProperty('color');
expect(result.data[0]).not.toHaveProperty('age');
});
it('should restrict wildcard query.select to only fields allowed in config.select', async () => {
// Server-side config only allows id, name and toys.id
const config = {
sortableColumns: ['id'],
select: ['id', 'name', 'toys.id'],
relations: ['toys'],
};
// Client tries to request all fields with wildcards
const query = {
page: 1,
limit: 10,
select: ['*', 'toys.*'], // Requesting all fields with wildcards
path: '/cats',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// Should only include fields that exist in both expanded config.select and expanded query.select
expect(result.data[0]).toHaveProperty('id');
expect(result.data[0]).toHaveProperty('name');
expect(result.data[0]).not.toHaveProperty('color');
expect(result.data[0]).not.toHaveProperty('age');
// Should only have toys.id, not other toy properties
expect(result.data[0].toys[0]).toHaveProperty('id');
expect(result.data[0].toys[0]).not.toHaveProperty('name');
expect(result.data[0].toys[0]).not.toHaveProperty('createdAt');
});
});
});
//# sourceMappingURL=paginate.spec.js.map