5805 typing issue in rest api (#5818)

Fixed types for:
- uuid
- email
- datetime
- date
- number
- currency.amountMicros
- select
- multiSelect

## Before

![image](https://github.com/twentyhq/twenty/assets/29927851/4bfa3a6d-a26f-47e4-a46f-7a5582825482)


## After

![image](https://github.com/twentyhq/twenty/assets/29927851/0bbab32f-4172-4525-91d1-76c37f299ac0)
This commit is contained in:
martmull
2024-06-11 14:54:02 +02:00
committed by GitHub
parent 710291238e
commit 5c15fcd249
8 changed files with 342 additions and 62 deletions

View File

@ -8,8 +8,8 @@ export const fieldNumberMock = {
defaultValue: null,
};
export const fieldStringMock = {
name: 'fieldString',
export const fieldTextMock = {
name: 'fieldText',
type: FieldMetadataType.TEXT,
isNullable: true,
defaultValue: null,
@ -52,15 +52,177 @@ export const fieldSelectMock = {
],
};
const fieldMultiSelectMock = {
name: 'fieldMultiSelect',
type: FieldMetadataType.MULTI_SELECT,
isNullable: true,
defaultValue: "{'OPTION_1'}",
options: [
{
id: '9a519a86-422b-4598-88ae-78751353f683',
color: 'red',
label: 'Opt 1',
value: 'OPTION_1',
position: 0,
},
{
id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4',
color: 'purple',
label: 'Opt 2',
value: 'OPTION_2',
position: 1,
},
],
};
const fieldRelationMock = {
name: 'fieldRelation',
type: FieldMetadataType.RELATION,
fromRelationMetadata: {
toObjectMetadata: {
nameSingular: 'toObjectMetadataName',
},
},
isNullable: true,
defaultValue: null,
};
const fieldLinksMock = {
name: 'fieldLinks',
type: FieldMetadataType.LINKS,
isNullable: false,
defaultValue: [
{ primaryLinkLabel: '', primaryLinkUrl: '', secondaryLinks: {} },
],
};
const fieldUuidMock = {
name: 'fieldUuid',
type: FieldMetadataType.UUID,
isNullable: true,
defaultValue: null,
};
const fieldPhoneMock = {
name: 'fieldPhone',
type: FieldMetadataType.PHONE,
isNullable: true,
defaultValue: null,
};
const fieldEmailMock = {
name: 'fieldEmail',
type: FieldMetadataType.EMAIL,
isNullable: true,
defaultValue: null,
};
const fieldDateTimeMock = {
name: 'fieldDateTime',
type: FieldMetadataType.DATE_TIME,
isNullable: true,
defaultValue: null,
};
const fieldDateMock = {
name: 'fieldDate',
type: FieldMetadataType.DATE,
isNullable: true,
defaultValue: null,
};
const fieldBooleanMock = {
name: 'fieldBoolean',
type: FieldMetadataType.BOOLEAN,
isNullable: true,
defaultValue: null,
};
const fieldNumericMock = {
name: 'fieldNumeric',
type: FieldMetadataType.NUMERIC,
isNullable: true,
defaultValue: null,
};
const fieldProbabilityMock = {
name: 'fieldProbability',
type: FieldMetadataType.PROBABILITY,
isNullable: true,
defaultValue: null,
};
const fieldFullNameMock = {
name: 'fieldFullName',
type: FieldMetadataType.FULL_NAME,
isNullable: true,
defaultValue: { firstName: '', lastName: '' },
};
const fieldRatingMock = {
name: 'fieldRating',
type: FieldMetadataType.RATING,
isNullable: true,
defaultValue: null,
};
const fieldPositionMock = {
name: 'fieldPosition',
type: FieldMetadataType.POSITION,
isNullable: true,
defaultValue: null,
};
const fieldAddressMock = {
name: 'fieldAddress',
type: FieldMetadataType.ADDRESS,
isNullable: true,
defaultValue: {
addressStreet1: '',
addressStreet2: null,
addressCity: null,
addressState: null,
addressCountry: null,
addressPostcode: null,
addressLat: null,
addressLng: null,
},
};
const fieldRawJsonMock = {
name: 'fieldRawJson',
type: FieldMetadataType.RAW_JSON,
isNullable: true,
defaultValue: null,
};
export const fields = [
fieldUuidMock,
fieldTextMock,
fieldPhoneMock,
fieldEmailMock,
fieldDateTimeMock,
fieldDateMock,
fieldBooleanMock,
fieldNumberMock,
fieldNumericMock,
fieldProbabilityMock,
fieldLinkMock,
fieldLinksMock,
fieldCurrencyMock,
fieldFullNameMock,
fieldRatingMock,
fieldSelectMock,
fieldMultiSelectMock,
fieldRelationMock,
fieldPositionMock,
fieldAddressMock,
fieldRawJsonMock,
];
export const objectMetadataItemMock = {
targetTableName: 'testingObject',
nameSingular: 'objectName',
namePlural: 'objectsName',
fields: [
fieldNumberMock,
fieldStringMock,
fieldLinkMock,
fieldCurrencyMock,
fieldSelectMock,
],
fields,
} as ObjectMetadataEntity;

View File

@ -78,12 +78,12 @@ describe('FilterInputFactory', () => {
it('should create filter parser properly', () => {
const request: any = {
query: {
filter: 'fieldNumber[eq]:1,fieldString[eq]:"Test"',
filter: 'fieldNumber[eq]:1,fieldText[eq]:"Test"',
},
};
expect(service.create(request, objectMetadata)).toEqual({
and: [{ fieldNumber: { eq: 1 } }, { fieldString: { eq: 'Test' } }],
and: [{ fieldNumber: { eq: 1 } }, { fieldText: { eq: 'Test' } }],
});
});
@ -91,21 +91,21 @@ describe('FilterInputFactory', () => {
const request: any = {
query: {
filter:
'and(fieldNumber[eq]:1,fieldString[gte]:"Test",not(fieldString[ilike]:"%val%"),or(not(and(fieldString[startsWith]:"test",fieldNumber[in]:[2,4,5])),fieldCurrency.amountMicros[gt]:1))',
'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 } },
{ fieldString: { gte: 'Test' } },
{ not: { fieldString: { ilike: '%val%' } } },
{ fieldText: { gte: 'Test' } },
{ not: { fieldText: { ilike: '%val%' } } },
{
or: [
{
not: {
and: [
{ fieldString: { startsWith: 'test' } },
{ fieldText: { startsWith: 'test' } },
{ fieldNumber: { in: [2, 4, 5] } },
],
},

View File

@ -32,13 +32,13 @@ describe('OrderByInputFactory', () => {
it('should create order by parser properly', () => {
const request: any = {
query: {
order_by: 'fieldNumber[AscNullsFirst],fieldString[DescNullsLast]',
order_by: 'fieldNumber[AscNullsFirst],fieldText[DescNullsLast]',
},
};
expect(service.create(request, objectMetadata)).toEqual({
fieldNumber: OrderByDirection.AscNullsFirst,
fieldString: OrderByDirection.DescNullsLast,
fieldText: OrderByDirection.DescNullsLast,
});
});
@ -95,7 +95,7 @@ describe('OrderByInputFactory', () => {
it('should throw if direction invalid', () => {
const request: any = {
query: {
order_by: 'fieldString[invalid]',
order_by: 'fieldText[invalid]',
},
};

View File

@ -41,14 +41,14 @@ describe('parseFilterContent', () => {
});
it('should parse query filter with comma in value ', () => {
expect(parseFilterContent('and(fieldString[eq]:"val,ue")')).toEqual([
'fieldString[eq]:"val,ue"',
expect(parseFilterContent('and(fieldText[eq]:"val,ue")')).toEqual([
'fieldText[eq]:"val,ue"',
]);
});
it('should parse query filter with comma in value ', () => {
expect(parseFilterContent("and(fieldString[eq]:'val,ue')")).toEqual([
"fieldString[eq]:'val,ue'",
expect(parseFilterContent("and(fieldText[eq]:'val,ue')")).toEqual([
"fieldText[eq]:'val,ue'",
]);
});
});

View File

@ -51,25 +51,25 @@ describe('parseFilter', () => {
it('should parse string filter test 4', () => {
expect(
parseFilter(
'and(fieldString[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldString[startsWith]:"val"),and(fieldNumber[eq]:6,fieldString[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldString[is]:NULL))',
'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))',
objectMetadataItemMock,
),
).toEqual({
and: [
{ fieldString: { gt: 'val,ue' } },
{ fieldText: { gt: 'val,ue' } },
{
or: [
{ fieldNumber: { is: 'NOT_NULL' } },
{ not: { fieldString: { startsWith: 'val' } } },
{ not: { fieldText: { startsWith: 'val' } } },
{
and: [
{ fieldNumber: { eq: 6 } },
{ fieldString: { ilike: '%val%' } },
{ fieldText: { ilike: '%val%' } },
],
},
],
},
{ or: [{ fieldNumber: { eq: 4 } }, { fieldString: { is: 'NULL' } }] },
{ or: [{ fieldNumber: { eq: 4 } }, { fieldText: { is: 'NULL' } }] },
],
});
});

View File

@ -2,7 +2,7 @@ import {
fieldCurrencyMock,
fieldLinkMock,
fieldNumberMock,
fieldStringMock,
fieldTextMock,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/rest-api-core-query-builder/utils/map-field-metadata-to-graphql-query.utils';
@ -13,8 +13,8 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
mapFieldMetadataToGraphqlQuery(objectMetadataItemMock, fieldNumberMock),
).toEqual('fieldNumber');
expect(
mapFieldMetadataToGraphqlQuery(objectMetadataItemMock, fieldStringMock),
).toEqual('fieldString');
mapFieldMetadataToGraphqlQuery(objectMetadataItemMock, fieldTextMock),
).toEqual('fieldText');
expect(
mapFieldMetadataToGraphqlQuery(objectMetadataItemMock, fieldLinkMock),
).toEqual(`

View File

@ -1,8 +1,17 @@
import { computeSchemaComponents } from 'src/engine/core-modules/open-api/utils/components.utils';
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import {
fields,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
describe('computeSchemaComponents', () => {
it('should test all field types', () => {
expect(fields.map((field) => field.type)).toEqual(
Object.keys(FieldMetadataType),
);
});
it('should compute schema components', () => {
expect(
computeSchemaComponents([
@ -15,12 +24,39 @@ describe('computeSchemaComponents', () => {
required: ['fieldNumber'],
example: { fieldNumber: '' },
properties: {
fieldCurrency: {
properties: {
amountMicros: { type: 'string' },
currencyCode: { type: 'string' },
},
type: 'object',
fieldUuid: {
type: 'string',
format: 'uuid',
},
fieldText: {
type: 'string',
},
fieldPhone: {
type: 'string',
},
fieldEmail: {
type: 'string',
format: 'email',
},
fieldDateTime: {
type: 'string',
format: 'date',
},
fieldDate: {
type: 'string',
format: 'date',
},
fieldBoolean: {
type: 'boolean',
},
fieldNumber: {
type: 'integer',
},
fieldNumeric: {
type: 'number',
},
fieldProbability: {
type: 'number',
},
fieldLink: {
properties: {
@ -29,14 +65,83 @@ describe('computeSchemaComponents', () => {
},
type: 'object',
},
fieldNumber: {
fieldLinks: {
properties: {
primaryLinkLabel: { type: 'string' },
primaryLinkUrl: { type: 'string' },
secondaryLinks: { type: 'object' },
},
type: 'object',
},
fieldCurrency: {
properties: {
amountMicros: { type: 'number' },
currencyCode: { type: 'string' },
},
type: 'object',
},
fieldFullName: {
properties: {
firstName: {
type: 'string',
},
lastName: {
type: 'string',
},
},
type: 'object',
},
fieldRating: {
type: 'number',
},
fieldSelect: {
type: 'string',
enum: ['OPTION_1', 'OPTION_2'],
},
fieldString: {
fieldMultiSelect: {
type: 'string',
enum: ['OPTION_1', 'OPTION_2'],
},
fieldRelation: {
type: 'array',
items: {
$ref: '#/components/schemas/ToObjectMetadataName',
},
},
fieldPosition: {
type: 'number',
},
fieldAddress: {
properties: {
addressCity: {
type: 'string',
},
addressCountry: {
type: 'string',
},
addressLat: {
type: 'number',
},
addressLng: {
type: 'number',
},
addressPostcode: {
type: 'string',
},
addressState: {
type: 'string',
},
addressStreet1: {
type: 'string',
},
addressStreet2: {
type: 'string',
},
},
type: 'object',
},
fieldRawJson: {
type: 'object',
},
},
},

View File

@ -19,6 +19,34 @@ type Properties = {
[name: string]: Property;
};
const getFieldProperties = (type: FieldMetadataType): Property => {
switch (type) {
case FieldMetadataType.UUID:
return { type: 'string', format: 'uuid' };
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
return { type: 'string' };
case FieldMetadataType.EMAIL:
return { type: 'string', format: 'email' };
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.DATE:
return { type: 'string', format: 'date' };
case FieldMetadataType.NUMBER:
return { type: 'integer' };
case FieldMetadataType.NUMERIC:
case FieldMetadataType.PROBABILITY:
case FieldMetadataType.RATING:
case FieldMetadataType.POSITION:
return { type: 'number' };
case FieldMetadataType.BOOLEAN:
return { type: 'boolean' };
case FieldMetadataType.RAW_JSON:
return { type: 'object' };
default:
return { type: 'string' };
}
};
const getSchemaComponentsProperties = (
item: ObjectMetadataEntity,
): Properties => {
@ -26,23 +54,12 @@ const getSchemaComponentsProperties = (
let itemProperty = {} as Property;
switch (field.type) {
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.DATE:
itemProperty.type = 'string';
break;
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
case FieldMetadataType.PROBABILITY:
case FieldMetadataType.RATING:
case FieldMetadataType.POSITION:
itemProperty.type = 'number';
break;
case FieldMetadataType.BOOLEAN:
itemProperty.type = 'boolean';
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
itemProperty = {
type: 'string',
enum: field.options.map((option: { value: string }) => option.value),
};
break;
case FieldMetadataType.RELATION:
if (field.fromRelationMetadata?.toObjectMetadata.nameSingular) {
@ -66,18 +83,14 @@ const getSchemaComponentsProperties = (
properties: compositeTypeDefintions
.get(field.type)
?.properties?.reduce((properties, property) => {
// TODO: This should not be statically typed, instead we should do someting recursive
properties[property.name] = { type: 'string' };
properties[property.name] = getFieldProperties(property.type);
return properties;
}, {} as Properties),
};
break;
case FieldMetadataType.RAW_JSON:
itemProperty.type = 'object';
break;
default:
itemProperty.type = 'string';
itemProperty = getFieldProperties(field.type);
break;
}