Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Obsolete product handling #9

Merged
merged 10 commits into from
Oct 12, 2023
Merged
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ COMPOSE_PATH_SEPARATOR=;
TAG=latest
QUERY_PORT=127.0.0.1:5511
POSTGRES_HOST=localhost
POSTGRES_PORT=5512
POSTGRES_PORT=127.0.0.1:5512
POSTGRES_DB=query
POSTGRES_USER=productopener
POSTGRES_PASSWORD=productopener
Expand Down
121 changes: 107 additions & 14 deletions src/domain/services/query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,36 @@ describe('count', () => {
});
it('should cope with no filters', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue, aminoValue, neucleotideValue } =
await createTestTags(app);
await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.count(null);
expect(response).toBeGreaterThan(2);
});
});

it('should be able to count obsolete products', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.count({
obsolete: 1,
origins_tags: originValue,
});
expect(response).toBe(1);
});
});

it('should be able to count not obsolete products', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.count({
obsolete: 0,
origins_tags: originValue,
});
expect(response).toBe(3);
});
});
});

describe('aggregate', () => {
Expand Down Expand Up @@ -129,6 +152,20 @@ describe('aggregate', () => {
});
});

it('should filter products when grouping by a product field', async () => {
await createTestingModule([DomainModule], async (app) => {
const { aminoValue, creatorValue } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.aggregate([
{ $match: { amino_acids_tags: aminoValue } },
{ $group: { _id: '$creator' } },
]);
const myTag = response.find((r) => r._id === creatorValue);
expect(myTag).toBeTruthy();
expect(parseInt(myTag.count)).toBe(1);
});
});

it('should be able to do not filtering', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue, aminoValue } = await createTestTags(app);
Expand Down Expand Up @@ -179,13 +216,26 @@ describe('aggregate', () => {
expect(parseInt(myTag.count)).toBe(1);
});
});

it('should be able to group obsolete products', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.aggregate([
{ $match: { obsolete: true } },
{ $group: { _id: '$origins_tags' } },
]);
const myTag = response.find((r) => r._id === originValue);
expect(myTag).toBeTruthy();
expect(parseInt(myTag.count)).toBe(1);
});
});
});

describe('select', () => {
it('should return matching products', async () =>{
it('should return matching products', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue, aminoValue, neucleotideValue, product1, product2, product3 } =
await createTestTags(app);
const { aminoValue, product1 } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.select({
amino_acids_tags: aminoValue,
Expand All @@ -195,24 +245,43 @@ describe('select', () => {
expect(p1).toBeTruthy();
});
});

it('should return obsolete matching products', async () => {
await createTestingModule([DomainModule], async (app) => {
const { aminoValue, product4 } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.select({
amino_acids_tags: aminoValue,
obsolete: 'true',
});
expect(response).toHaveLength(1);
const p4 = response.find((r) => r.code === product4.code);
expect(p4).toBeTruthy();
});
});
});

async function createTestTags(app) {
const em = app.get(EntityManager);
// Create some dummy products with a specific tag
const product1 = em.create(Product, { code: randomCode() });
const product2 = em.create(Product, { code: randomCode() });
const product3 = em.create(Product, { code: randomCode() });

// Using origins and amino acids as they are smaller than most
const originValue = randomCode();
const aminoValue = randomCode();
const neucleotideValue = randomCode();
const creatorValue = randomCode();

// Create some dummy products with a specific tag
const product1 = em.create(Product, { code: randomCode() });
const product2 = em.create(Product, { code: randomCode(), creator: creatorValue });
const product3 = em.create(Product, { code: randomCode(), creator: creatorValue });
const product4 = em.create(Product, { code: randomCode(), obsolete: true });

// Matrix for testing
// Product | Origin | AminoAcid | Neucleotide
// Product1 | x | x | x
// Product2 | x | x |
// Product3 | x | | x
// Product | Origin | AminoAcid | Neucleotide | Obsolete | Creator
// Product1 | x | x | x | |
// Product2 | x | x | | | x
// Product3 | x | | x | | x
// Product4 | x | x | x | x |

em.create(ProductOriginsTag, {
product: product1,
Expand All @@ -226,6 +295,11 @@ async function createTestTags(app) {
product: product3,
value: originValue,
});
em.create(ProductOriginsTag, {
product: product4,
value: originValue,
obsolete: true,
});

em.create(ProductAminoAcidsTag, {
product: product1,
Expand All @@ -235,6 +309,11 @@ async function createTestTags(app) {
product: product2,
value: aminoValue,
});
em.create(ProductAminoAcidsTag, {
product: product4,
value: aminoValue,
obsolete: true,
});

em.create(ProductNucleotidesTag, {
product: product1,
Expand All @@ -244,7 +323,21 @@ async function createTestTags(app) {
product: product3,
value: neucleotideValue,
});
em.create(ProductNucleotidesTag, {
product: product4,
value: neucleotideValue,
obsolete: true,
});

await em.flush();
return { originValue, aminoValue, neucleotideValue, product1, product2, product3 };
return {
originValue,
aminoValue,
neucleotideValue,
creatorValue,
product1,
product2,
product3,
product4,
};
}
31 changes: 21 additions & 10 deletions src/domain/services/query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export class QueryService {
} else {
qb.select(`${column}`).distinct();
}
qb.where('not pt.obsolete');
qb.where(this.obsoleteWhere(match));

const whereLog = this.addMatches(match, qb);
const whereLog = this.addMatches(match, qb, entity);

if (count) {
qb = this.em.createQueryBuilder(qb, 'temp');
Expand Down Expand Up @@ -63,7 +63,8 @@ export class QueryService {
return results;
}

private addMatches(match: any, qb: QueryBuilder<object>, parentKey = 'pt.product_id') {
private addMatches(match: any, qb: QueryBuilder<object>, parentEntity) {
const parentId = parentEntity === Product ? 'id': 'product_id';
const whereLog = [];
for (const [matchTag, matchValue] of Object.entries(match)) {
let whereValue = matchValue;
Expand All @@ -76,7 +77,7 @@ export class QueryService {
const qbWhere = this.em
.createQueryBuilder(matchEntity, 'pt2')
.select('*')
.where(`pt2.product_id = ${parentKey} and pt2.${matchColumn} = ?`, [
.where(`pt2.product_id = pt.${parentId} and pt2.${matchColumn} = ?`, [
whereValue,
]);
qb.andWhere(`${not ? 'NOT ' : ''}EXISTS (${qbWhere.getKnexQuery()})`);
Expand All @@ -85,16 +86,23 @@ export class QueryService {
return whereLog;
}

obsoleteWhere(body: any) {
const obsolete = !!body?.obsolete;
delete body?.obsolete;
return `${obsolete ? '' : 'not '}pt.obsolete`;
}

async count(body: any) {
const start = Date.now();
this.logger.debug(body);

const obsoleteWhere = this.obsoleteWhere(body);
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');
qb.where(obsoleteWhere);

let whereLog = [];
if (tag) {
Expand All @@ -106,7 +114,7 @@ export class QueryService {
}
qb.andWhere(`${not ? 'NOT ' : ''}pt.${column} = ?`, [matchValue]);
delete body[tag];
whereLog.push(...this.addMatches(body, qb));
whereLog.push(...this.addMatches(body, qb, entity));
}

this.logger.debug(qb.getFormattedQuery());
Expand All @@ -122,16 +130,19 @@ export class QueryService {
const start = Date.now();
this.logger.debug(body);

const tags = Object.keys(body);
const obsoleteWhere = this.obsoleteWhere(body);
let entity: EntityName<object> = Product;
const qb = this.em.createQueryBuilder(entity, 'p');
const qb = this.em.createQueryBuilder(entity, 'pt');
qb.select(`*`);
qb.where('not p.obsolete');
qb.where(obsoleteWhere);

const whereLog = this.addMatches(body, qb, 'p.id');
const whereLog = this.addMatches(body, qb, entity);

this.logger.debug(qb.getFormattedQuery());
const results = await qb.execute();
this.logger.log(
`Processed ${whereLog.join(' and ')} in ${Date.now() - start} ms. Selected ${results.length} records`,
);
return results;
}

Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule);
const migrator = app.get(MikroORM).getMigrator();
await migrator.up();
await app.listen(5510);
await app.listen(5510, '0.0.0.0');
}
bootstrap();
2 changes: 1 addition & 1 deletion src/mikro-orm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default defineConfig({
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT),
port: parseInt(process.env.POSTGRES_PORT.split(':').pop()),
schema: SCHEMA,
driverOptions: {
searchPath: [SCHEMA, 'public'],
Expand Down
Loading