diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index a3368ae6a..8767507e5 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -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; diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/filter-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/filter-input.factory.spec.ts index a27948d44..12dd66750 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/filter-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/filter-input.factory.spec.ts @@ -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] } }, ], }, diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts index deedebe78..ddd8b3eff 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts @@ -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]', }, }; diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter-content.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter-content.utils.spec.ts index 60cf46def..ed0efea1c 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter-content.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter-content.utils.spec.ts @@ -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'", ]); }); }); diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter.utils.spec.ts index 54de0817b..3dfdd2b75 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/__tests__/parse-filter.utils.spec.ts @@ -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' } }] }, ], }); }); diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts index 9e6f2e348..76dbe1b73 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts @@ -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(` diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index c01c66c3d..db004bbcd 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -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', }, }, }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index f95011dfe..eb5bf7cbb 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -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; }