diff --git a/.env b/.env index 594d940..c492160 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ COMPOSE_PATH_SEPARATOR=; TAG=latest QUERY_PORT=127.0.0.1:5511 POSTGRES_HOST=localhost -POSTGRES_PORT=127.0.0.1:5512 +POSTGRES_PORT=5512 POSTGRES_DB=query POSTGRES_USER=productopener POSTGRES_PASSWORD=productopener diff --git a/src/app.controller.ts b/src/app.controller.ts index 69c942d..248e178 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Post, Query, All } from '@nestjs/common'; import { ImportService } from './domain/services/import.service'; import { QueryService } from './domain/services/query.service'; @@ -27,8 +27,13 @@ export class AppController { return await this.queryService.aggregate(body); } - @Post('count') + @All('count') async count(@Body() body: any) { return await this.queryService.count(body); } + + @Post('select') + async select(@Body() body: any) { + return await this.queryService.select(body); + } } diff --git a/src/domain/services/query.service.spec.ts b/src/domain/services/query.service.spec.ts index 123186b..cbdda9e 100644 --- a/src/domain/services/query.service.spec.ts +++ b/src/domain/services/query.service.spec.ts @@ -89,6 +89,15 @@ describe('count', () => { expect(response).toBe(1); }); }); + it('should cope with no filters', async () => { + await createTestingModule([DomainModule], async (app) => { + const { originValue, aminoValue, neucleotideValue } = + await createTestTags(app); + const queryService = app.get(QueryService); + const response = await queryService.count(null); + expect(response).toBeGreaterThan(2); + }); + }); }); describe('aggregate', () => { @@ -172,6 +181,22 @@ describe('aggregate', () => { }); }); +describe('select', () => { + it('should return matching products', async () =>{ + await createTestingModule([DomainModule], async (app) => { + const { originValue, aminoValue, neucleotideValue, product1, product2, product3 } = + await createTestTags(app); + const queryService = app.get(QueryService); + const response = await queryService.select({ + amino_acids_tags: aminoValue, + }); + expect(response).toHaveLength(2); + const p1 = response.find((r) => r.code === product1.code); + expect(p1).toBeTruthy(); + }); + }); +}); + async function createTestTags(app) { const em = app.get(EntityManager); // Create some dummy products with a specific tag @@ -221,5 +246,5 @@ async function createTestTags(app) { }); await em.flush(); - return { originValue, aminoValue, neucleotideValue }; + return { originValue, aminoValue, neucleotideValue, product1, product2, product3 }; } diff --git a/src/domain/services/query.service.ts b/src/domain/services/query.service.ts index 9ad99f3..f7e457e 100644 --- a/src/domain/services/query.service.ts +++ b/src/domain/services/query.service.ts @@ -63,7 +63,7 @@ export class QueryService { return results; } - private addMatches(match: any, qb: QueryBuilder) { + private addMatches(match: any, qb: QueryBuilder, parentKey = 'pt.product_id') { const whereLog = []; for (const [matchTag, matchValue] of Object.entries(match)) { let whereValue = matchValue; @@ -76,7 +76,7 @@ export class QueryService { const qbWhere = this.em .createQueryBuilder(matchEntity, 'pt2') .select('*') - .where(`pt2.product_id = pt.product_id and pt2.${matchColumn} = ?`, [ + .where(`pt2.product_id = ${parentKey} and pt2.${matchColumn} = ?`, [ whereValue, ]); qb.andWhere(`${not ? 'NOT ' : ''}EXISTS (${qbWhere.getKnexQuery()})`); @@ -89,37 +89,56 @@ export class QueryService { const start = Date.now(); this.logger.debug(body); - const tags = Object.keys(body); - const tag = tags[0]; + const tags = Object.keys(body ?? {}); + const tag = tags?.[0]; const { entity, column } = this.getEntityAndColumn(tag); const qb = this.em.createQueryBuilder(entity, 'pt'); qb.select(`count(*) count`); qb.where('not pt.obsolete'); - let matchValue = body[tag]; - const not = matchValue?.['$ne']; - if (not) { - matchValue = not; + let whereLog = []; + if (tag) { + let matchValue = body[tag]; + const not = matchValue?.['$ne']; + whereLog.push(`${tag} ${not ? '!=' : '=='} ${matchValue}`); + if (not) { + matchValue = not; + } + qb.andWhere(`${not ? 'NOT ' : ''}pt.${column} = ?`, [matchValue]); + delete body[tag]; + whereLog.push(...this.addMatches(body, qb)); } - qb.andWhere(`${not ? 'NOT ' : ''}pt.${column} = ?`, [matchValue]); - delete body[tag]; - const whereLog = this.addMatches(body, qb); this.logger.debug(qb.getFormattedQuery()); const results = await qb.execute(); const response = results[0].count; this.logger.log( - `Processed ${tag} ${not ? '!=' : '=='} ${matchValue}${ - whereLog.length ? ` and ${whereLog.join(' and ')}` : '' - } in ${Date.now() - start} ms. Count: ${response}`, + `Processed ${whereLog.join(' and ')} in ${Date.now() - start} ms. Count: ${response}`, ); return parseInt(response); } + async select(body: any) { + const start = Date.now(); + this.logger.debug(body); + + const tags = Object.keys(body); + let entity: EntityName = Product; + const qb = this.em.createQueryBuilder(entity, 'p'); + qb.select(`*`); + qb.where('not p.obsolete'); + + const whereLog = this.addMatches(body, qb, 'p.id'); + + this.logger.debug(qb.getFormattedQuery()); + const results = await qb.execute(); + return results; + } + private getEntityAndColumn(tag: any) { let entity: EntityName; let column = 'value'; - if (MAPPED_FIELDS.includes(tag)) { + if (!tag || MAPPED_FIELDS.includes(tag)) { entity = Product; column = tag; } else {