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:
martmull
2024-06-18 18:55:13 +02:00
committed by GitHub
parent dbaa787d19
commit 6fd8dab552
87 changed files with 701 additions and 567 deletions

View File

@ -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');
});
});
});

View File

@ -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' } } },
],
},
],
});
});
});
});

View File

@ -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",
);
});
});
});

View File

@ -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",
);
});
});
});

View File

@ -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');
});
});
});

View File

@ -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;
}
}

View File

@ -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,
];

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}