5582 get httpsapitwentycomrestmetadata objects filters dont work (#5906)
- Remove filters from metadata rest api - add limite before and after parameters for metadata - remove update from metadata relations - fix typing issue - fix naming - fix before parameter --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -0,0 +1,33 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
|
||||
describe('EndingBeforeInputFactory', () => {
|
||||
let service: EndingBeforeInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [EndingBeforeInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EndingBeforeInputFactory>(EndingBeforeInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if ending_before missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return ending_before', () => {
|
||||
const request: any = { query: { ending_before: 'uuid' } };
|
||||
|
||||
expect(service.create(request)).toEqual('uuid');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,120 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
|
||||
describe('FilterInputFactory', () => {
|
||||
const objectMetadata = { objectMetadataItem: objectMetadataItemMock };
|
||||
|
||||
let service: FilterInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FilterInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FilterInputFactory>(FilterInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if filter missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual({});
|
||||
});
|
||||
|
||||
it('should throw when wrong field provided', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'wrongField[eq]:1',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"field 'wrongField' does not exist in 'objectName' object",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when wrong comparator provided', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'fieldNumber[wrongComparator]:1',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'filter' invalid for 'fieldNumber[wrongComparator]:1', comparator wrongComparator not in eq,neq,in,is,gt,gte,lt,lte,startsWith,like,ilike",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when wrong filter provided', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'fieldNumber[wrongComparator:1',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'filter' invalid for 'fieldNumber[wrongComparator:1'. eg: price[gte]:10",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when parenthesis are not closed', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'and(fieldNumber[eq]:1,not(fieldNumber[neq]:1)',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'filter' invalid. 1 close bracket is missing in the query",
|
||||
);
|
||||
});
|
||||
|
||||
it('should create filter parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'fieldNumber[eq]:1,fieldText[eq]:"Test"',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual({
|
||||
and: [{ fieldNumber: { eq: 1 } }, { fieldText: { eq: 'Test' } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create complex filter parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter:
|
||||
'and(fieldNumber[eq]:1,fieldText[gte]:"Test",not(fieldText[ilike]:"%val%"),or(not(and(fieldText[startsWith]:"test",fieldNumber[in]:[2,4,5])),fieldCurrency.amountMicros[gt]:1))',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: 1 } },
|
||||
{ fieldText: { gte: 'Test' } },
|
||||
{ not: { fieldText: { ilike: '%val%' } } },
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
and: [
|
||||
{ fieldText: { startsWith: 'test' } },
|
||||
{ fieldNumber: { in: [2, 4, 5] } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ fieldCurrency: { amountMicros: { gt: '1' } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||
|
||||
describe('LimitInputFactory', () => {
|
||||
let service: LimitInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [LimitInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LimitInputFactory>(LimitInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if limit missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request)).toEqual(60);
|
||||
});
|
||||
|
||||
it('should return limit', () => {
|
||||
const request: any = { query: { limit: '10' } };
|
||||
|
||||
expect(service.create(request)).toEqual(10);
|
||||
});
|
||||
|
||||
it('should throw if not integer', () => {
|
||||
const request: any = { query: { limit: 'aaa' } };
|
||||
|
||||
expect(() => service.create(request)).toThrow(
|
||||
"limit 'aaa' is invalid. Should be an integer",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if limit negative', () => {
|
||||
const request: any = { query: { limit: -1 } };
|
||||
|
||||
expect(() => service.create(request)).toThrow(
|
||||
"limit '-1' is invalid. Should be an integer",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,119 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
|
||||
describe('OrderByInputFactory', () => {
|
||||
const objectMetadata = { objectMetadataItem: objectMetadataItemMock };
|
||||
|
||||
let service: OrderByInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [OrderByInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<OrderByInputFactory>(OrderByInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if order by missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([{}]);
|
||||
});
|
||||
|
||||
it('should create order by parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldNumber[AscNullsFirst],fieldText[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldNumber: OrderByDirection.AscNullsFirst },
|
||||
{ fieldText: OrderByDirection.DescNullsLast },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should choose default direction if missing', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldNumber',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldNumber: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handler complex fields', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldCurrency.amountMicros',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handler complex fields with direction', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldCurrency.amountMicros[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handler multiple complex fields with direction', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by:
|
||||
'fieldCurrency.amountMicros[DescNullsLast],fieldLink.label[AscNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
|
||||
{ fieldLink: { label: OrderByDirection.AscNullsLast } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw if direction invalid', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldText[invalid]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'order_by' direction 'invalid' invalid. Allowed values are 'AscNullsFirst', 'AscNullsLast', 'DescNullsFirst', 'DescNullsLast'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if field invalid', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'wrongField[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"field 'wrongField' does not exist in 'objectName' object",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||
|
||||
describe('StartingAfterInputFactory', () => {
|
||||
let service: StartingAfterInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [StartingAfterInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<StartingAfterInputFactory>(StartingAfterInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if starting_after missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return starting_after', () => {
|
||||
const request: any = { query: { starting_after: 'uuid' } };
|
||||
|
||||
expect(service.create(request)).toEqual('uuid');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class EndingBeforeInputFactory {
|
||||
create(request: Request): string | undefined {
|
||||
const cursorQuery = request.query.ending_before;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cursorQuery;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
|
||||
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
|
||||
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
|
||||
export const inputFactories = [
|
||||
StartingAfterInputFactory,
|
||||
EndingBeforeInputFactory,
|
||||
LimitInputFactory,
|
||||
OrderByInputFactory,
|
||||
FilterInputFactory,
|
||||
];
|
||||
@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils';
|
||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
|
||||
@Injectable()
|
||||
export class FilterInputFactory {
|
||||
create(request: Request, objectMetadata): Record<string, FieldValue> {
|
||||
let filterQuery = request.query.filter;
|
||||
|
||||
if (typeof filterQuery !== 'string') {
|
||||
return {};
|
||||
}
|
||||
|
||||
checkFilterQuery(filterQuery);
|
||||
|
||||
filterQuery = addDefaultConjunctionIfMissing(filterQuery);
|
||||
|
||||
return parseFilter(filterQuery, objectMetadata.objectMetadataItem);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class LimitInputFactory {
|
||||
create(request: Request, defaultLimit = 60): number {
|
||||
if (!request.query.limit) {
|
||||
return defaultLimit;
|
||||
}
|
||||
const limit = +request.query.limit;
|
||||
|
||||
if (isNaN(limit) || limit < 0) {
|
||||
throw new BadRequestException(
|
||||
`limit '${request.query.limit}' is invalid. Should be an integer`,
|
||||
);
|
||||
}
|
||||
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import {
|
||||
OrderByDirection,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils';
|
||||
|
||||
export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
|
||||
|
||||
@Injectable()
|
||||
export class OrderByInputFactory {
|
||||
create(request: Request, objectMetadata): RecordOrderBy {
|
||||
const orderByQuery = request.query.order_by;
|
||||
|
||||
if (typeof orderByQuery !== 'string') {
|
||||
return [{}];
|
||||
}
|
||||
|
||||
//orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3
|
||||
const orderByItems = orderByQuery.split(',');
|
||||
let result: Array<Record<string, OrderByDirection>> = [];
|
||||
let itemDirection = '';
|
||||
let itemFields = '';
|
||||
|
||||
for (const orderByItem of orderByItems) {
|
||||
// orderByItem -> field_1[AscNullsFirst]
|
||||
if (orderByItem.includes('[') && orderByItem.includes(']')) {
|
||||
const [fieldsString, direction] = orderByItem
|
||||
.replace(']', '')
|
||||
.split('[');
|
||||
|
||||
// fields -> [field_1] ; direction -> AscNullsFirst
|
||||
if (!(direction in OrderByDirection)) {
|
||||
throw new BadRequestException(
|
||||
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
||||
OrderByDirection,
|
||||
).join(
|
||||
"', '",
|
||||
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
||||
);
|
||||
}
|
||||
|
||||
itemDirection = direction;
|
||||
itemFields = fieldsString;
|
||||
} else {
|
||||
// orderByItem -> field_3
|
||||
itemDirection = DEFAULT_ORDER_DIRECTION;
|
||||
itemFields = orderByItem;
|
||||
}
|
||||
|
||||
let fieldResult = {};
|
||||
|
||||
itemFields
|
||||
.split('.')
|
||||
.reverse()
|
||||
.forEach((field) => {
|
||||
if (Object.keys(fieldResult).length) {
|
||||
fieldResult = { [field]: fieldResult };
|
||||
} else {
|
||||
fieldResult[field] = itemDirection;
|
||||
}
|
||||
}, itemDirection);
|
||||
|
||||
const resultFields = Object.keys(fieldResult).map((key) => ({
|
||||
[key]: fieldResult[key],
|
||||
}));
|
||||
|
||||
result = [...result, ...resultFields];
|
||||
}
|
||||
|
||||
checkArrayFields(objectMetadata.objectMetadataItem, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class StartingAfterInputFactory {
|
||||
create(request: Request): string | undefined {
|
||||
const cursorQuery = request.query.starting_after;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cursorQuery;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user