diff --git a/src/__tests__/cat-toy.entity.ts b/src/__tests__/cat-toy.entity.ts index 011ff33..4b71efe 100644 --- a/src/__tests__/cat-toy.entity.ts +++ b/src/__tests__/cat-toy.entity.ts @@ -1,5 +1,6 @@ import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' import { CatEntity } from './cat.entity' +import { SizeEmbed } from './size.embed' @Entity() export class CatToyEntity { @@ -9,6 +10,9 @@ export class CatToyEntity { @Column() name: string + @Column(() => SizeEmbed) + size: SizeEmbed + @ManyToOne(() => CatEntity, (cat) => cat.toys) @JoinColumn() cat: CatEntity diff --git a/src/__tests__/cat.entity.ts b/src/__tests__/cat.entity.ts index 0fb49f1..67afb7a 100644 --- a/src/__tests__/cat.entity.ts +++ b/src/__tests__/cat.entity.ts @@ -10,6 +10,7 @@ import { } from 'typeorm' import { CatToyEntity } from './cat-toy.entity' import { CatHomeEntity } from './cat-home.entity' +import { SizeEmbed } from './size.embed' @Entity() export class CatEntity { @@ -25,6 +26,9 @@ export class CatEntity { @Column({ nullable: true }) age: number | null + @Column(() => SizeEmbed) + size: SizeEmbed + @OneToMany(() => CatToyEntity, (catToy) => catToy.cat) toys: CatToyEntity[] diff --git a/src/__tests__/size.embed.ts b/src/__tests__/size.embed.ts new file mode 100644 index 0000000..7b8269c --- /dev/null +++ b/src/__tests__/size.embed.ts @@ -0,0 +1,12 @@ +import { Column } from 'typeorm' + +export class SizeEmbed { + @Column() + height: number + + @Column() + width: number + + @Column() + length: number +} diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index 2ec90da..2a09fb1 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -36,17 +36,17 @@ describe('paginate', () => { catToyRepo = connection.getRepository(CatToyEntity) catHomeRepo = connection.getRepository(CatHomeEntity) cats = await catRepo.save([ - catRepo.create({ name: 'Milo', color: 'brown', age: 6 }), - catRepo.create({ name: 'Garfield', color: 'ginger', age: 5 }), - catRepo.create({ name: 'Shadow', color: 'black', age: 4 }), - catRepo.create({ name: 'George', color: 'white', age: 3 }), - catRepo.create({ name: 'Leche', color: 'white', age: null }), + catRepo.create({ name: 'Milo', color: 'brown', age: 6, size: { height: 25, width: 10, length: 40 } }), + catRepo.create({ name: 'Garfield', color: 'ginger', age: 5, size: { height: 30, width: 15, length: 45 } }), + catRepo.create({ name: 'Shadow', color: 'black', age: 4, size: { height: 25, width: 10, length: 50 } }), + catRepo.create({ name: 'George', color: 'white', age: 3, size: { height: 35, width: 12, length: 40 } }), + catRepo.create({ name: 'Leche', color: 'white', age: null, size: { height: 10, width: 5, length: 15 } }), ]) catToys = await catToyRepo.save([ - catToyRepo.create({ name: 'Fuzzy Thing', cat: cats[0] }), - catToyRepo.create({ name: 'Stuffed Mouse', cat: cats[0] }), - catToyRepo.create({ name: 'Mouse', cat: cats[0] }), - catToyRepo.create({ name: 'String', cat: cats[1] }), + catToyRepo.create({ name: 'Fuzzy Thing', cat: cats[0], size: { height: 10, width: 10, length: 10 } }), + catToyRepo.create({ name: 'Stuffed Mouse', cat: cats[0], size: { height: 5, width: 5, length: 12 } }), + catToyRepo.create({ name: 'Mouse', cat: cats[0], size: { height: 6, width: 4, length: 13 } }), + catToyRepo.create({ name: 'String', cat: cats[1], size: { height: 1, width: 1, length: 50 } }), ]) catHomes = await catHomeRepo.save([ catHomeRepo.create({ name: 'Box', cat: cats[0] }), @@ -627,7 +627,7 @@ describe('paginate', () => { relations: ['cat'], sortableColumns: ['id', 'name'], filterableColumns: { - createdAt: [FilterOperator.BTW], + 'cat.age': [FilterOperator.BTW], }, } const query: PaginateQuery = { @@ -642,10 +642,453 @@ describe('paginate', () => { expect(result.meta.filter).toStrictEqual({ 'cat.age': '$btw:6,10', }) - expect(result.data).toStrictEqual([catHomes[0], catHomes[1]]) + expect(result.data).toStrictEqual([catHomes[0]]) 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], + searchableColumns: ['name'], + } + const query: PaginateQuery = { + path: '', + sortBy: [ + ['size.height', 'ASC'], + ['size.length', 'ASC'], + ], + } + + const result = await paginate(query, catRepo, config) + + const orderedCats = [cats[4], cats[0], cats[2], cats[1], cats[3]] + 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], + searchableColumns: ['name'], + relations: ['home', 'toys'], + } + const query: PaginateQuery = { + path: '', + sortBy: [ + ['size.height', 'DESC'], + ['size.length', 'DESC'], + ], + } + + const result = await paginate(query, catRepo, config) + + const copyCats = cats.map((cat: CatEntity) => { + const copy = clone(cat) + copy.home = null + copy.toys = [] + return copy + }) + + const copyHomes = catHomes.map((home: CatHomeEntity) => { + const copy = clone(home) + delete copy.cat + return copy + }) + copyCats[0].home = copyHomes[0] + copyCats[1].home = copyHomes[1] + + const copyToys = catToys.map((toy: CatToyEntity) => { + const copy = clone(toy) + delete copy.cat + return copy + }) + copyCats[0].toys = [copyToys[0], copyToys[1], copyToys[2]] + copyCats[1].toys = [copyToys[3]] + + const orderedCats = [copyCats[3], copyCats[1], copyCats[2], copyCats[0], copyCats[4]] + + expect(result.data).toStrictEqual(orderedCats) + expect(result.links.current).toBe('?page=1&limit=20&sortBy=size.height:DESC&sortBy=size.length:DESC') + }) + + it('should return result based on sort on embedded entity on one-to-many relation', async () => { + const config: PaginateConfig = { + sortableColumns: ['id', 'name', 'toys.size.height', 'toys.size.length', 'toys.size.width'], + searchableColumns: ['name'], + relations: ['toys'], + } + const query: PaginateQuery = { + path: '', + sortBy: [ + ['id', 'DESC'], + ['toys.size.height', 'ASC'], + ['toys.size.length', 'ASC'], + ], + } + + const result = await paginate(query, catRepo, config) + + const toy0 = clone(catToys[0]) + delete toy0.cat + + const toy1 = clone(catToys[1]) + delete toy1.cat + + const toy2 = clone(catToys[2]) + delete toy2.cat + + const toy3 = clone(catToys[3]) + delete toy3.cat + + const orderedCats = [ + Object.assign(clone(cats[4]), { toys: [] }), + Object.assign(clone(cats[3]), { toys: [] }), + Object.assign(clone(cats[2]), { toys: [] }), + Object.assign(clone(cats[1]), { toys: [toy3] }), + Object.assign(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: PaginateConfig = { + sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], + searchableColumns: ['name'], + relations: ['cat'], + } + const query: PaginateQuery = { + path: '', + sortBy: [ + ['cat.size.height', 'DESC'], + ['cat.size.length', 'DESC'], + ['name', 'ASC'], + ], + } + + const result = await paginate(query, catToyRepo, config) + const orderedToys = [catToys[3], catToys[0], catToys[2], catToys[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: PaginateConfig = { + sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], + searchableColumns: ['name'], + relations: ['cat'], + } + const query: PaginateQuery = { + path: '', + sortBy: [['cat.size.height', 'DESC']], + } + + const result = await paginate(query, catHomeRepo, config) + const orderedHomes = [catHomes[1], catHomes[0]] + + 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], + searchableColumns: ['size.height'], + } + const query: PaginateQuery = { + path: '', + search: '10', + } + + const result = await paginate(query, catRepo, config) + + expect(result.data).toStrictEqual([cats[4]]) + 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], + searchableColumns: ['size.height'], + relations: ['home', 'toys'], + } + const query: PaginateQuery = { + path: '', + search: '10', + } + + const result = await paginate(query, catRepo, config) + + expect(result.meta.search).toStrictEqual('10') + + const copyCat = clone(cats[4]) + copyCat.home = null + copyCat.toys = [] + + expect(result.data).toStrictEqual([copyCat]) + 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], + searchableColumns: ['cat.size.height'], + relations: ['cat'], + } + const query: PaginateQuery = { + path: '', + search: '30', + } + + const result = await 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'toys.size.height', 'toys.size.length', 'toys.size.width'], + searchableColumns: ['toys.size.height'], + relations: ['toys'], + } + const query: PaginateQuery = { + path: '', + search: '1', + } + + const result = await paginate(query, catRepo, config) + + const toy0 = clone(catToys[0]) + delete toy0.cat + + const toy3 = clone(catToys[3]) + delete toy3.cat + + expect(result.data).toStrictEqual([ + Object.assign(clone(cats[0]), { toys: [toy0] }), + Object.assign(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: PaginateConfig = { + sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], + searchableColumns: ['cat.size.height'], + relations: ['cat'], + } + const query: PaginateQuery = { + path: '', + search: '30', + } + + const result = await paginate(query, catHomeRepo, config) + + expect(result.data).toStrictEqual([catHomes[1]]) + 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'cat.size.height', 'cat.size.length', 'cat.size.width'], + searchableColumns: ['cat.size.width'], + relations: ['cat'], + } + const query: PaginateQuery = { + path: '', + search: '1', + sortBy: [['cat.size.height', 'DESC']], + } + + const result = await paginate(query, catToyRepo, config) + expect(result.meta.search).toStrictEqual('1') + expect(result.data).toStrictEqual([catToys[3], catToys[0], catToys[1], catToys[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: PaginateConfig = { + sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], + searchableColumns: ['size.height'], + filterableColumns: { + 'size.height': [FilterOperator.NOT], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + 'size.height': '$not:25', + }, + } + + const result = await paginate(query, catRepo, config) + + expect(result.data).toStrictEqual([cats[1], cats[3], cats[4]]) + 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: PaginateConfig = { + sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], + searchableColumns: ['size.height'], + filterableColumns: { + 'size.height': [FilterOperator.NOT], + }, + relations: ['home'], + } + const query: PaginateQuery = { + path: '', + filter: { + 'size.height': '$not:25', + }, + } + + const result = await paginate(query, catRepo, config) + + const home = clone(catHomes[1]) + delete home.cat + + const copyCats = [ + Object.assign(clone(cats[1]), { home: home }), + Object.assign(clone(cats[3]), { home: null }), + Object.assign(clone(cats[4]), { 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: PaginateConfig = { + relations: ['cat'], + sortableColumns: ['id', 'name'], + filterableColumns: { + 'cat.size.height': [FilterOperator.NOT], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + 'cat.size.height': '$not:25', + }, + } + + const result = await 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: PaginateConfig = { + relations: ['toys'], + sortableColumns: ['id', 'name'], + filterableColumns: { + 'toys.size.height': [FilterOperator.EQ], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + 'toys.size.height': '$eq:1', + }, + } + + const result = await paginate(query, catRepo, config) + + const cat2 = clone(cats[1]) + const catToys3 = clone(catToys[3]) + delete catToys3.cat + cat2.toys = [catToys3] + + expect(result.meta.filter).toStrictEqual({ + 'toys.size.height': '$eq:1', + }) + expect(result.data).toStrictEqual([cat2]) + expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.size.height=$eq:1') + }) + + it('should return result based on filter on embedded on one-to-one relation', async () => { + const config: PaginateConfig = { + relations: ['cat'], + sortableColumns: ['id', 'name'], + filterableColumns: { + 'cat.size.height': [FilterOperator.EQ], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + 'cat.size.height': '$eq:30', + }, + } + + const result = await paginate(query, catHomeRepo, config) + + expect(result.meta.filter).toStrictEqual({ + 'cat.size.height': '$eq:30', + }) + expect(result.data).toStrictEqual([catHomes[1]]) + 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: PaginateConfig = { + relations: ['cat'], + sortableColumns: ['id', 'name'], + filterableColumns: { + 'cat.size.height': [FilterOperator.IN], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + 'cat.size.height': '$in:10,30,35', + }, + } + + const result = await paginate(query, catHomeRepo, config) + + expect(result.meta.filter).toStrictEqual({ + 'cat.size.height': '$in:10,30,35', + }) + expect(result.data).toStrictEqual([catHomes[1]]) + 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: PaginateConfig = { + relations: ['cat'], + sortableColumns: ['id', 'name'], + filterableColumns: { + 'cat.size.height': [FilterOperator.BTW], + }, + } + const query: PaginateQuery = { + path: '', + filter: { + 'cat.size.height': '$btw:18,33', + }, + } + + const result = await paginate(query, catHomeRepo, config) + + expect(result.meta.filter).toStrictEqual({ + 'cat.size.height': '$btw:18,33', + }) + expect(result.data).toStrictEqual([catHomes[0], catHomes[1]]) + 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: PaginateConfig = { sortableColumns: ['id'], diff --git a/src/paginate.ts b/src/paginate.ts index 47dbaf6..09aa94f 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -245,7 +245,7 @@ export async function paginate( } for (const order of sortBy) { - if (order[0].split('.').length > 1) { + if (queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath(order[0].split('.')[0])) { queryBuilder.addOrderBy(`${queryBuilder.alias}_${order[0]}`, order[1], nullSort) } else { queryBuilder.addOrderBy(`${queryBuilder.alias}.${order[0]}`, order[1], nullSort) @@ -278,9 +278,14 @@ export async function paginate( for (const column of searchBy) { const propertyPath = (column as string).split('.') if (propertyPath.length > 1) { + const alias = queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath( + propertyPath[0] + ) + ? `${qb.alias}_${column}` + : `${qb.alias}.${column}` const condition: WherePredicateOperator = { operator: 'ilike', - parameters: [`${qb.alias}_${column}`, `:${column}`], + parameters: [alias, `:${column}`], } qb.orWhere(qb['createWhereConditionExpression'](condition), { [column]: `%${query.search}%`, @@ -308,19 +313,24 @@ export async function paginate( ) as WherePredicateOperator let parameters = { [column]: filter[column].value } // TODO: refactor below + const alias = queryBuilder.expressionMap.mainAlias.metadata.hasRelationWithPropertyPath( + propertyPath[0] + ) + ? `${qb.alias}_${column}` + : `${qb.alias}.${column}` switch (condition.operator) { case 'between': - condition.parameters = [`${qb.alias}_${column}`, `:${column}_from`, `:${column}_to`] + condition.parameters = [alias, `:${column}_from`, `:${column}_to`] parameters = { [column + '_from']: filter[column].value[0], [column + '_to']: filter[column].value[1], } break case 'in': - condition.parameters = [`${qb.alias}_${column}`, `:...${column}`] + condition.parameters = [alias, `:...${column}`] break default: - condition.parameters = [`${qb.alias}_${column}`, `:${column}`] + condition.parameters = [alias, `:${column}`] break } qb.andWhere(qb['createWhereConditionExpression'](condition), parameters)