UNPKG

nestjs-paginate

Version:

Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.

1,187 lines (1,186 loc) 160 kB
"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 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);