nestjs-paginate
Version:
Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.
1,157 lines • 255 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");
const global_config_1 = require("./global-config");
// 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,
}),
]);
// Link cats via two parallel to-one relations for polymorphic (~) sort tests.
// Saved as fresh objects so the shared `cats` fixture is not mutated. Some cats
// get a bestFriend, others only a nemesis, so COALESCE(bestFriend.age, nemesis.age)
// exercises the fallback from the first source to the second.
await catRepo.save([
catRepo.create({ id: cats[0].id, bestFriend: cats[2] }), // Milo -> 4 (Shadow)
catRepo.create({ id: cats[1].id, nemesis: cats[0] }), // Garfield -> 6 (Milo)
catRepo.create({ id: cats[2].id, bestFriend: cats[3] }), // Shadow -> 3 (George)
catRepo.create({ id: cats[3].id, nemesis: cats[5] }), // George -> 0 (Baby)
catRepo.create({ id: cats[4].id, bestFriend: cats[1] }), // Leche -> 5 (Garfield)
catRepo.create({ id: cats[5].id, nemesis: cats[2] }), // Baby -> 4 (Shadow)
catRepo.create({ id: cats[6].id, bestFriend: cats[0] }), // Adam -> 6 (Milo)
]);
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,
config: { theme: 'dark', fontSize: 14 },
}),
catHomeRepo.create({
name: 'House',
cat: cats[1],
street: 'Mainstreet',
naptimePillow: null,
config: { theme: 'light', fontSize: 12 },
}),
catHomeRepo.create({
name: 'Mansion',
cat: cats[2],
street: 'Boulevard Avenue',
naptimePillow,
config: { theme: 'dark', fontSize: 16, nested: { level: 2, tag: 'vip' } },
}),
]);
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 },
metadataJson: { length: 5, thickness: 1 },
}),
catHairRepo.create({
name: 'long',
colors: ['white', 'brown'],
metadata: { length: 20, thickness: 5 },
metadataJson: { length: 20, thickness: 5 },
}),
catHairRepo.create({
name: 'buzzed',
colors: ['white'],
metadata: { length: 0.5, thickness: 10 },
metadataJson: { 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 and use empty string as default origin in config, even if global provided', async () => {
(0, global_config_1.updateGlobalConfig)({
defaultOrigin: 'http://localhost:8081',
});
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
defaultLimit: 1,
origin: '',
};
const query = {
path: 'http://localhost:8080/cat',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result).toBeInstanceOf(paginate_1.Paginated);
expect(result.links.current).toStrictEqual('/cat?page=1&limit=1&sortBy=id:ASC');
(0, global_config_1.updateGlobalConfig)({
defaultOrigin: undefined,
});
});
it('should use default origin from global config if provided, over the one from request', async () => {
(0, global_config_1.updateGlobalConfig)({
defaultOrigin: 'http://localhost:8081',
});
const config = {
sortableColumns: ['id'],
defaultSortBy: [['id', 'ASC']],
defaultLimit: 1,
};
const query = {
path: 'http://localhost:8080/cat',
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result).toBeInstanceOf(paginate_1.Paginated);
expect(result.links.current).toStrictEqual('http://localhost:8081/cat?page=1&limit=1&sortBy=id:ASC');
(0, global_config_1.updateGlobalConfig)({
defaultOrigin: undefined,
});
});
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, global_config_1.default.defaultLimit));
});
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);
});
describe('polymorphic sort (~)', () => {
// Mirrors SQL COALESCE(bestFriend.age, nemesis.age) for the links set up in beforeAll.
const coalescedAge = (cat) => { var _a, _b, _c, _d; return (_d = (_b = (_a = cat.bestFriend) === null || _a === void 0 ? void 0 : _a.age) !== null && _b !== void 0 ? _b : (_c = cat.nemesis) === null || _c === void 0 ? void 0 : _c.age) !== null && _d !== void 0 ? _d : null; };
it('should sort by a polymorphic column group using COALESCE (ASC)', async () => {
const config = {
sortableColumns: ['id', 'bestFriend.age', 'nemesis.age'],
relations: ['bestFriend', 'nemesis'],
};
const query = {
path: '',
sortBy: [[['bestFriend.age', 'nemesis.age'], 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
expect(result.meta.sortBy).toStrictEqual([[['bestFriend.age', 'nemesis.age'], 'ASC']]);
const values = result.data.map(coalescedAge);
// Every cat resolves to a non-null age through one of the two relations.
expect(values.every((v) => typeof v === 'number')).toBe(true);
// The COALESCE result is ordered ascending.
expect(values).toStrictEqual([...values].sort((a, b) => a - b));
// The fallback actually fires: some rows resolve via nemesis (no bestFriend).
expect(result.data.some((cat) => !cat.bestFriend && !!cat.nemesis)).toBe(true);
});
it('should sort by a polymorphic column group using COALESCE (DESC)', async () => {
const config = {
sortableColumns: ['id', 'bestFriend.age', 'nemesis.age'],
relations: ['bestFriend', 'nemesis'],
};
const query = {
path: '',
sortBy: [[['bestFriend.age', 'nemesis.age'], 'DESC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
const values = result.data.map(coalescedAge);
expect(values).toStrictEqual([...values].sort((a, b) => b - a));
});
it('should ignore a polymorphic group when one of its columns is not sortable', async () => {
const config = {
sortableColumns: ['id', 'bestFriend.age'], // nemesis.age intentionally omitted
relations: ['bestFriend', 'nemesis'],
defaultSortBy: [['id', 'ASC']],
};
const query = {
path: '',
sortBy: [[['bestFriend.age', 'nemesis.age'], 'ASC']],
};
const result = await (0, paginate_1.paginate)(query, catRepo, config);
// The group is dropped during validation, so the default sort applies.
expect(result.meta.sortBy).toStrictEqual([['id', 'ASC']]);
});
it('should reject a polymorphic group with cursor pagination', async () => {
const config = {
sortableColumns: ['id', 'bestFriend.age', 'nemesis.age'],
relations: ['bestFriend', 'nemesis'],
paginationType: paginate_1.PaginationType.CURSOR,
};
const query = {
path: '',
sortBy: [[['bestFriend.age', 'nemesis.age'], 'ASC']],
};
await expect((0, paginate_1.paginate)(query, catRepo, config)).rejects.toThrow('Polymorphic sort groups (using "~") are not supported with cursor pagination.');
});
it('should reject embedded columns inside a polymorphic group', async () => {
const config = {
sortableColumns: ['id', 'size.height', 'age'],
};
const query = {
path: '',
sortBy: [[['size.height', 'age'], 'ASC']],
};
await expect((0, paginate_1.paginate)(query, catRepo, config)).rejects.toThrow('Polymorphic sort groups (using "~") support only plain and relation columns');
});
});
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: {
to