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

Support $in but not other value objects. Fix NOT on count #21

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/domain/services/query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ describe('count', () => {
});
});

it('should count the number of products without a specified tag', async () => {
await createTestingModule([DomainModule], async (app) => {
const { aminoValue, originValue } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.count({
amino_acids_tags: { $ne: aminoValue },
origins_tags: originValue, // Need at least one other criteria to avoid products from other tests
});
expect(response).toBe(1);
});
});

it('should throw and unprocessable exception for an unknwon tag', async () => {
await createTestingModule([DomainModule], async (app) => {
try {
Expand All @@ -96,6 +108,17 @@ describe('count', () => {
});
});

it('should throw and unprocessable exception for an unrecognised value object', async () => {
await createTestingModule([DomainModule], async (app) => {
try {
await app.get(QueryService).count({ origins_tags: { $unknown: 'x' } });
fail('should not get here');
} catch (e) {
expect(e).toBeInstanceOf(UnprocessableEntityException);
}
});
});

it('should cope with more than two filters', async () => {
await createTestingModule([DomainModule], async (app) => {
const { originValue, aminoValue, neucleotideValue } =
Expand Down Expand Up @@ -171,6 +194,17 @@ describe('count', () => {
expect(response).toBe(1);
});
});

it('should cope with an $in value', async () => {
await createTestingModule([DomainModule], async (app) => {
const { aminoValue, aminoValue2 } = await createTestTags(app);
const queryService = app.get(QueryService);
const response = await queryService.count({
amino_acids_tags: { $in: [aminoValue, aminoValue2] },
});
expect(response).toBe(3);
});
});
});

describe('aggregate', () => {
Expand Down
63 changes: 31 additions & 32 deletions src/domain/services/query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,29 @@ export class QueryService {
if (not) {
whereValue = not;
}
// If the value is still an object then we can't handle it
if (whereValue === Object(whereValue)) {
// Unless it is an $in
const keys = Object.keys(whereValue);
if (keys.length != 1 || keys[0] !== '$in')
this.throwUnprocessableException(
`Unable to process ${JSON.stringify(whereValue)}`,
);
}

const { entity: matchEntity, column: matchColumn } =
await this.getEntityAndColumn(matchTag);
// The following creates an EXISTS / NOT EXISTS sub-query for the specified tag
const knex = this.em.getKnex();
const qbWhere = this.em
.createQueryBuilder(matchEntity, 'pt2')
.select('*')
.where(
`pt2.${this.productId(matchEntity)} = pt.${this.productId(
parentEntity,
)} and pt2.${matchColumn} = ?`,
[whereValue],
);
.where({
[`pt2.${this.productId(matchEntity)}`]: knex.ref(
`pt.${this.productId(parentEntity)}`,
),
[`pt2.${matchColumn}`]: whereValue,
});
qb.andWhere(`${not ? 'NOT ' : ''}EXISTS (${qbWhere.getKnexQuery()})`);
whereLog.push(`${matchTag} ${not ? '!=' : '=='} ${whereValue}`);
}
Expand All @@ -167,26 +178,14 @@ export class QueryService {

const filters = this.parseFilter(body ?? {});

// The main table for the query is determined from the first filter
const mainFilter = filters.shift();
const { entity, column } = await this.getEntityAndColumn(mainFilter?.[0]);
// Always use product as the main table otherwise "nots" are not handled correctly
const entity: EntityName<object> = Product;
const qb = this.em.createQueryBuilder(entity, 'pt');
qb.select(`count(*) count`);
qb.where(this.obsoleteWhere(obsolete));

const whereLog = [];
if (mainFilter) {
let matchValue = mainFilter[1];
const not = matchValue?.['$ne'];
if (not) {
matchValue = not;
}
whereLog.push(`${mainFilter[0]} ${not ? '!=' : '=='} ${matchValue}`);
qb.andWhere(`${not ? 'NOT ' : ''}pt.${column} = ?`, [matchValue]);

// Add any further where clauses
whereLog.push(...(await this.addMatches(filters, qb, entity)));
}
// Add where clauses
const whereLog = await this.addMatches(filters, qb, entity);

this.logger.debug(qb.getFormattedQuery());
const results = await qb.execute();
Expand Down Expand Up @@ -235,18 +234,18 @@ export class QueryService {
// Check to see if the tag has been loaded. This allows us to introduce
// new tags but they will initially not be supported until a full import
// is performed
if (!(await this.tagService.getLoadedTags()).includes(tag)) {
const message = `Tag '${tag}' is not loaded`;
this.logger.warn(message);
throw new UnprocessableEntityException(message);
}
if (!(await this.tagService.getLoadedTags()).includes(tag))
this.throwUnprocessableException(`Tag '${tag}' is not loaded`);
}
}
if (entity == null) {
const message = `Tag '${tag}' is not supported`;
this.logger.warn(message);
throw new UnprocessableEntityException(message);
}
if (entity == null)
this.throwUnprocessableException(`Tag '${tag}' is not supported`);

return { entity, column };
}

private throwUnprocessableException(message: string) {
this.logger.warn(message);
throw new UnprocessableEntityException(message);
}
}
Loading