From 5f8040af5dc6bc7b75480c7319ce562d09a98ac6 Mon Sep 17 00:00:00 2001
From: TakuyaKurimoto <75765648+TakuyaKurimoto@users.noreply.github.com>
Date: Mon, 5 May 2025 22:48:23 +0900
Subject: [PATCH] Modify Decimal Fields to be treated as `number` in OpenAPI
schema. (#11871)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes https://github.com/twentyhq/twenty/issues/10807
## Description
This PR will Modify Decimal Fields to be treated as `number` in OpenAPI
schema.
## Testing
---------
Co-authored-by: Takuya Kurimoto
Co-authored-by: prastoin
---
.../components.utils.spec.ts.snap | 106 ++
.../utils/__tests__/components.utils.spec.ts | 1228 +++++++++--------
.../open-api/utils/components.utils.ts | 59 +-
3 files changed, 812 insertions(+), 581 deletions(-)
create mode 100644 packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/__snapshots__/components.utils.spec.ts.snap
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/__snapshots__/components.utils.spec.ts.snap b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/__snapshots__/components.utils.spec.ts.snap
new file mode 100644
index 000000000..9de04fe89
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/__snapshots__/components.utils.spec.ts.snap
@@ -0,0 +1,106 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`computeSchemaComponents Float without decimals 1`] = `
+{
+ "ObjectName": {
+ "description": undefined,
+ "properties": {
+ "number2": {
+ "type": "number",
+ },
+ },
+ "required": [
+ "number2",
+ ],
+ "type": "object",
+ },
+ "ObjectName for Response": {
+ "description": undefined,
+ "properties": {
+ "number2": {
+ "type": "number",
+ },
+ },
+ "type": "object",
+ },
+ "ObjectName for Update": {
+ "description": undefined,
+ "properties": {
+ "number2": {
+ "type": "number",
+ },
+ },
+ "type": "object",
+ },
+}
+`;
+
+exports[`computeSchemaComponents Integer dataType with decimals 1`] = `
+{
+ "ObjectName": {
+ "description": undefined,
+ "properties": {
+ "number1": {
+ "type": "number",
+ },
+ },
+ "required": [
+ "number1",
+ ],
+ "type": "object",
+ },
+ "ObjectName for Response": {
+ "description": undefined,
+ "properties": {
+ "number1": {
+ "type": "number",
+ },
+ },
+ "type": "object",
+ },
+ "ObjectName for Update": {
+ "description": undefined,
+ "properties": {
+ "number1": {
+ "type": "number",
+ },
+ },
+ "type": "object",
+ },
+}
+`;
+
+exports[`computeSchemaComponents Integer with a 0 decimals 1`] = `
+{
+ "ObjectName": {
+ "description": undefined,
+ "properties": {
+ "number3": {
+ "type": "integer",
+ },
+ },
+ "required": [
+ "number3",
+ ],
+ "type": "object",
+ },
+ "ObjectName for Response": {
+ "description": undefined,
+ "properties": {
+ "number3": {
+ "type": "integer",
+ },
+ },
+ "type": "object",
+ },
+ "ObjectName for Update": {
+ "description": undefined,
+ "properties": {
+ "number3": {
+ "type": "integer",
+ },
+ },
+ "type": "object",
+ },
+}
+`;
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 e98e33c68..d8f98b06b 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,5 +1,11 @@
+import { FieldMetadataType } from 'twenty-shared/types';
+import { EachTestingContext } from 'twenty-shared/testing';
+
+import { NumberDataType } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
+
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { computeSchemaComponents } from 'src/engine/core-modules/open-api/utils/components.utils';
+import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
describe('computeSchemaComponents', () => {
@@ -8,585 +14,681 @@ describe('computeSchemaComponents', () => {
computeSchemaComponents([
objectMetadataItemMock,
] as ObjectMetadataEntity[]),
- ).toEqual({
- ObjectName: {
- description: undefined,
- type: 'object',
- properties: {
- fieldUuid: {
- type: 'string',
- format: 'uuid',
- },
- fieldText: {
- type: 'string',
- },
- fieldPhones: {
- properties: {
- additionalPhones: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- primaryPhoneCountryCode: {
- type: 'string',
- },
- primaryPhoneCallingCode: {
- type: 'string',
- },
- primaryPhoneNumber: {
- type: 'string',
- },
- },
- type: 'object',
- },
- fieldEmails: {
- type: 'object',
- properties: {
- primaryEmail: {
- type: 'string',
- },
- additionalEmails: {
- type: 'array',
- items: {
- type: 'string',
- format: 'email',
- },
- },
- },
- },
- fieldDateTime: {
- type: 'string',
- format: 'date-time',
- },
- fieldDate: {
- type: 'string',
- format: 'date',
- },
- fieldArray: {
- items: {
- type: 'string',
- },
- type: 'array',
- },
- fieldBoolean: {
- type: 'boolean',
- },
- fieldNumber: {
- type: 'integer',
- },
- fieldNumeric: {
- type: 'number',
- },
- fieldLinks: {
- type: 'object',
- properties: {
- primaryLinkLabel: {
- type: 'string',
- },
- primaryLinkUrl: {
- type: 'string',
- },
- secondaryLinks: {
- type: 'array',
- items: {
- type: 'object',
- description: 'A secondary link',
- properties: {
- url: {
- type: 'string',
- format: 'uri',
- },
- label: {
- type: 'string',
- },
- },
- },
- },
- },
- },
- fieldCurrency: {
- type: 'object',
- properties: {
- amountMicros: {
- type: 'number',
- },
- currencyCode: {
- type: 'string',
- },
- },
- },
- fieldFullName: {
- type: 'object',
- properties: {
- firstName: {
- type: 'string',
- },
- lastName: {
- type: 'string',
- },
- },
- },
- fieldRating: {
- type: 'string',
- enum: ['RATING_1', 'RATING_2'],
- },
- fieldSelect: {
- type: 'string',
- enum: ['OPTION_1', 'OPTION_2'],
- },
- fieldMultiSelect: {
- type: 'array',
- items: { type: 'string', enum: ['OPTION_1', 'OPTION_2'] },
- },
- fieldPosition: {
- type: 'number',
- },
- fieldAddress: {
- type: 'object',
- properties: {
- addressStreet1: {
- type: 'string',
- },
- addressStreet2: {
- type: 'string',
- },
- addressCity: {
- type: 'string',
- },
- addressPostcode: {
- type: 'string',
- },
- addressState: {
- type: 'string',
- },
- addressCountry: {
- type: 'string',
- },
- addressLat: {
- type: 'number',
- },
- addressLng: {
- type: 'number',
- },
- },
- },
- fieldRawJson: {
- type: 'object',
- },
- fieldRichText: {
- type: 'string',
- },
- fieldActor: {
- type: 'object',
- properties: {
- source: {
- type: 'string',
- enum: [
- 'EMAIL',
- 'CALENDAR',
- 'WORKFLOW',
- 'API',
- 'IMPORT',
- 'MANUAL',
- 'SYSTEM',
- 'WEBHOOK',
- ],
- },
- },
+ ).toMatchInlineSnapshot(`
+{
+ "ObjectName": {
+ "description": undefined,
+ "properties": {
+ "fieldActor": {
+ "properties": {
+ "source": {
+ "enum": [
+ "EMAIL",
+ "CALENDAR",
+ "WORKFLOW",
+ "API",
+ "IMPORT",
+ "MANUAL",
+ "SYSTEM",
+ "WEBHOOK",
+ ],
+ "type": "string",
},
},
- required: ['fieldNumber'],
+ "type": "object",
},
- 'ObjectName for Update': {
- description: undefined,
- type: 'object',
- properties: {
- fieldUuid: {
- type: 'string',
- format: 'uuid',
+ "fieldAddress": {
+ "properties": {
+ "addressCity": {
+ "type": "string",
},
- fieldText: {
- type: 'string',
+ "addressCountry": {
+ "type": "string",
},
- fieldPhones: {
- properties: {
- additionalPhones: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- primaryPhoneCountryCode: {
- type: 'string',
- },
- primaryPhoneCallingCode: {
- type: 'string',
- },
- primaryPhoneNumber: {
- type: 'string',
- },
- },
- type: 'object',
+ "addressLat": {
+ "type": "number",
},
- fieldEmails: {
- type: 'object',
- properties: {
- primaryEmail: {
- type: 'string',
- },
- additionalEmails: {
- type: 'array',
- items: {
- type: 'string',
- format: 'email',
- },
- },
- },
+ "addressLng": {
+ "type": "number",
},
- fieldDateTime: {
- type: 'string',
- format: 'date-time',
+ "addressPostcode": {
+ "type": "string",
},
- fieldDate: {
- type: 'string',
- format: 'date',
+ "addressState": {
+ "type": "string",
},
- fieldArray: {
- items: {
- type: 'string',
- },
- type: 'array',
+ "addressStreet1": {
+ "type": "string",
},
- fieldBoolean: {
- type: 'boolean',
- },
- fieldNumber: {
- type: 'integer',
- },
- fieldNumeric: {
- type: 'number',
- },
- fieldLinks: {
- type: 'object',
- properties: {
- primaryLinkLabel: {
- type: 'string',
- },
- primaryLinkUrl: {
- type: 'string',
- },
- secondaryLinks: {
- type: 'array',
- items: {
- type: 'object',
- description: 'A secondary link',
- properties: {
- url: {
- type: 'string',
- format: 'uri',
- },
- label: {
- type: 'string',
- },
- },
- },
- },
- },
- },
- fieldCurrency: {
- type: 'object',
- properties: {
- amountMicros: {
- type: 'number',
- },
- currencyCode: {
- type: 'string',
- },
- },
- },
- fieldFullName: {
- type: 'object',
- properties: {
- firstName: {
- type: 'string',
- },
- lastName: {
- type: 'string',
- },
- },
- },
- fieldRating: {
- type: 'string',
- enum: ['RATING_1', 'RATING_2'],
- },
- fieldSelect: {
- type: 'string',
- enum: ['OPTION_1', 'OPTION_2'],
- },
- fieldMultiSelect: {
- type: 'array',
- items: { type: 'string', enum: ['OPTION_1', 'OPTION_2'] },
- },
- fieldPosition: {
- type: 'number',
- },
- fieldAddress: {
- type: 'object',
- properties: {
- addressStreet1: {
- type: 'string',
- },
- addressStreet2: {
- type: 'string',
- },
- addressCity: {
- type: 'string',
- },
- addressPostcode: {
- type: 'string',
- },
- addressState: {
- type: 'string',
- },
- addressCountry: {
- type: 'string',
- },
- addressLat: {
- type: 'number',
- },
- addressLng: {
- type: 'number',
- },
- },
- },
- fieldRawJson: {
- type: 'object',
- },
- fieldRichText: {
- type: 'string',
- },
- fieldActor: {
- type: 'object',
- properties: {
- source: {
- type: 'string',
- enum: [
- 'EMAIL',
- 'CALENDAR',
- 'WORKFLOW',
- 'API',
- 'IMPORT',
- 'MANUAL',
- 'SYSTEM',
- 'WEBHOOK',
- ],
- },
- },
+ "addressStreet2": {
+ "type": "string",
},
},
+ "type": "object",
},
- 'ObjectName for Response': {
- description: undefined,
- type: 'object',
- properties: {
- fieldUuid: {
- type: 'string',
- format: 'uuid',
+ "fieldArray": {
+ "items": {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "fieldBoolean": {
+ "type": "boolean",
+ },
+ "fieldCurrency": {
+ "properties": {
+ "amountMicros": {
+ "type": "number",
},
- fieldText: {
- type: 'string',
- },
- fieldPhones: {
- properties: {
- additionalPhones: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- primaryPhoneCountryCode: {
- type: 'string',
- },
- primaryPhoneCallingCode: {
- type: 'string',
- },
- primaryPhoneNumber: {
- type: 'string',
- },
- },
- type: 'object',
- },
- fieldEmails: {
- type: 'object',
- properties: {
- primaryEmail: {
- type: 'string',
- },
- additionalEmails: {
- type: 'array',
- items: {
- type: 'string',
- format: 'email',
- },
- },
- },
- },
- fieldDateTime: {
- type: 'string',
- format: 'date-time',
- },
- fieldDate: {
- type: 'string',
- format: 'date',
- },
- fieldArray: {
- items: {
- type: 'string',
- },
- type: 'array',
- },
- fieldBoolean: {
- type: 'boolean',
- },
- fieldNumber: {
- type: 'integer',
- },
- fieldNumeric: {
- type: 'number',
- },
- fieldLinks: {
- type: 'object',
- properties: {
- primaryLinkLabel: {
- type: 'string',
- },
- primaryLinkUrl: {
- type: 'string',
- },
- secondaryLinks: {
- type: 'array',
- items: {
- type: 'object',
- description: 'A secondary link',
- properties: {
- url: {
- type: 'string',
- format: 'uri',
- },
- label: {
- type: 'string',
- },
- },
- },
- },
- },
- },
- fieldCurrency: {
- type: 'object',
- properties: {
- amountMicros: {
- type: 'number',
- },
- currencyCode: {
- type: 'string',
- },
- },
- },
- fieldFullName: {
- type: 'object',
- properties: {
- firstName: {
- type: 'string',
- },
- lastName: {
- type: 'string',
- },
- },
- },
- fieldRating: {
- type: 'string',
- enum: ['RATING_1', 'RATING_2'],
- },
- fieldSelect: {
- type: 'string',
- enum: ['OPTION_1', 'OPTION_2'],
- },
- fieldMultiSelect: {
- type: 'array',
- items: { type: 'string', enum: ['OPTION_1', 'OPTION_2'] },
- },
- fieldPosition: {
- type: 'number',
- },
- fieldAddress: {
- type: 'object',
- properties: {
- addressStreet1: {
- type: 'string',
- },
- addressStreet2: {
- type: 'string',
- },
- addressCity: {
- type: 'string',
- },
- addressPostcode: {
- type: 'string',
- },
- addressState: {
- type: 'string',
- },
- addressCountry: {
- type: 'string',
- },
- addressLat: {
- type: 'number',
- },
- addressLng: {
- type: 'number',
- },
- },
- },
- fieldRawJson: {
- type: 'object',
- },
- fieldRichText: {
- type: 'string',
- },
- fieldActor: {
- type: 'object',
- properties: {
- source: {
- type: 'string',
- enum: [
- 'EMAIL',
- 'CALENDAR',
- 'WORKFLOW',
- 'API',
- 'IMPORT',
- 'MANUAL',
- 'SYSTEM',
- 'WEBHOOK',
- ],
- },
- workspaceMemberId: {
- type: 'string',
- format: 'uuid',
- },
- name: {
- type: 'string',
- },
- },
- },
- fieldRelation: {
- type: 'array',
- items: {
- $ref: '#/components/schemas/ToObjectMetadataName for Response',
- },
+ "currencyCode": {
+ "type": "string",
},
},
+ "type": "object",
},
- });
+ "fieldDate": {
+ "format": "date",
+ "type": "string",
+ },
+ "fieldDateTime": {
+ "format": "date-time",
+ "type": "string",
+ },
+ "fieldEmails": {
+ "properties": {
+ "additionalEmails": {
+ "items": {
+ "format": "email",
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "primaryEmail": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldFullName": {
+ "properties": {
+ "firstName": {
+ "type": "string",
+ },
+ "lastName": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldLinks": {
+ "properties": {
+ "primaryLinkLabel": {
+ "type": "string",
+ },
+ "primaryLinkUrl": {
+ "type": "string",
+ },
+ "secondaryLinks": {
+ "items": {
+ "description": "A secondary link",
+ "properties": {
+ "label": {
+ "type": "string",
+ },
+ "url": {
+ "format": "uri",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "type": "object",
+ },
+ "fieldMultiSelect": {
+ "items": {
+ "enum": [
+ "OPTION_1",
+ "OPTION_2",
+ ],
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "fieldNumber": {
+ "type": "integer",
+ },
+ "fieldNumeric": {
+ "type": "number",
+ },
+ "fieldPhones": {
+ "properties": {
+ "additionalPhones": {
+ "items": {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "primaryPhoneCallingCode": {
+ "type": "string",
+ },
+ "primaryPhoneCountryCode": {
+ "type": "string",
+ },
+ "primaryPhoneNumber": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldPosition": {
+ "type": "number",
+ },
+ "fieldRating": {
+ "enum": [
+ "RATING_1",
+ "RATING_2",
+ ],
+ "type": "string",
+ },
+ "fieldRawJson": {
+ "type": "object",
+ },
+ "fieldRichText": {
+ "type": "string",
+ },
+ "fieldSelect": {
+ "enum": [
+ "OPTION_1",
+ "OPTION_2",
+ ],
+ "type": "string",
+ },
+ "fieldText": {
+ "type": "string",
+ },
+ "fieldUuid": {
+ "format": "uuid",
+ "type": "string",
+ },
+ },
+ "required": [
+ "fieldNumber",
+ ],
+ "type": "object",
+ },
+ "ObjectName for Response": {
+ "description": undefined,
+ "properties": {
+ "fieldActor": {
+ "properties": {
+ "name": {
+ "type": "string",
+ },
+ "source": {
+ "enum": [
+ "EMAIL",
+ "CALENDAR",
+ "WORKFLOW",
+ "API",
+ "IMPORT",
+ "MANUAL",
+ "SYSTEM",
+ "WEBHOOK",
+ ],
+ "type": "string",
+ },
+ "workspaceMemberId": {
+ "format": "uuid",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "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",
+ },
+ "fieldArray": {
+ "items": {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "fieldBoolean": {
+ "type": "boolean",
+ },
+ "fieldCurrency": {
+ "properties": {
+ "amountMicros": {
+ "type": "number",
+ },
+ "currencyCode": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldDate": {
+ "format": "date",
+ "type": "string",
+ },
+ "fieldDateTime": {
+ "format": "date-time",
+ "type": "string",
+ },
+ "fieldEmails": {
+ "properties": {
+ "additionalEmails": {
+ "items": {
+ "format": "email",
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "primaryEmail": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldFullName": {
+ "properties": {
+ "firstName": {
+ "type": "string",
+ },
+ "lastName": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldLinks": {
+ "properties": {
+ "primaryLinkLabel": {
+ "type": "string",
+ },
+ "primaryLinkUrl": {
+ "type": "string",
+ },
+ "secondaryLinks": {
+ "items": {
+ "description": "A secondary link",
+ "properties": {
+ "label": {
+ "type": "string",
+ },
+ "url": {
+ "format": "uri",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "type": "object",
+ },
+ "fieldMultiSelect": {
+ "items": {
+ "enum": [
+ "OPTION_1",
+ "OPTION_2",
+ ],
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "fieldNumber": {
+ "type": "integer",
+ },
+ "fieldNumeric": {
+ "type": "number",
+ },
+ "fieldPhones": {
+ "properties": {
+ "additionalPhones": {
+ "items": {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "primaryPhoneCallingCode": {
+ "type": "string",
+ },
+ "primaryPhoneCountryCode": {
+ "type": "string",
+ },
+ "primaryPhoneNumber": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldPosition": {
+ "type": "number",
+ },
+ "fieldRating": {
+ "enum": [
+ "RATING_1",
+ "RATING_2",
+ ],
+ "type": "string",
+ },
+ "fieldRawJson": {
+ "type": "object",
+ },
+ "fieldRelation": {
+ "items": {
+ "$ref": "#/components/schemas/ToObjectMetadataName for Response",
+ },
+ "type": "array",
+ },
+ "fieldRichText": {
+ "type": "string",
+ },
+ "fieldSelect": {
+ "enum": [
+ "OPTION_1",
+ "OPTION_2",
+ ],
+ "type": "string",
+ },
+ "fieldText": {
+ "type": "string",
+ },
+ "fieldUuid": {
+ "format": "uuid",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "ObjectName for Update": {
+ "description": undefined,
+ "properties": {
+ "fieldActor": {
+ "properties": {
+ "source": {
+ "enum": [
+ "EMAIL",
+ "CALENDAR",
+ "WORKFLOW",
+ "API",
+ "IMPORT",
+ "MANUAL",
+ "SYSTEM",
+ "WEBHOOK",
+ ],
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "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",
+ },
+ "fieldArray": {
+ "items": {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "fieldBoolean": {
+ "type": "boolean",
+ },
+ "fieldCurrency": {
+ "properties": {
+ "amountMicros": {
+ "type": "number",
+ },
+ "currencyCode": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldDate": {
+ "format": "date",
+ "type": "string",
+ },
+ "fieldDateTime": {
+ "format": "date-time",
+ "type": "string",
+ },
+ "fieldEmails": {
+ "properties": {
+ "additionalEmails": {
+ "items": {
+ "format": "email",
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "primaryEmail": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldFullName": {
+ "properties": {
+ "firstName": {
+ "type": "string",
+ },
+ "lastName": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldLinks": {
+ "properties": {
+ "primaryLinkLabel": {
+ "type": "string",
+ },
+ "primaryLinkUrl": {
+ "type": "string",
+ },
+ "secondaryLinks": {
+ "items": {
+ "description": "A secondary link",
+ "properties": {
+ "label": {
+ "type": "string",
+ },
+ "url": {
+ "format": "uri",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "type": "object",
+ },
+ "fieldMultiSelect": {
+ "items": {
+ "enum": [
+ "OPTION_1",
+ "OPTION_2",
+ ],
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "fieldNumber": {
+ "type": "integer",
+ },
+ "fieldNumeric": {
+ "type": "number",
+ },
+ "fieldPhones": {
+ "properties": {
+ "additionalPhones": {
+ "items": {
+ "type": "string",
+ },
+ "type": "array",
+ },
+ "primaryPhoneCallingCode": {
+ "type": "string",
+ },
+ "primaryPhoneCountryCode": {
+ "type": "string",
+ },
+ "primaryPhoneNumber": {
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+ "fieldPosition": {
+ "type": "number",
+ },
+ "fieldRating": {
+ "enum": [
+ "RATING_1",
+ "RATING_2",
+ ],
+ "type": "string",
+ },
+ "fieldRawJson": {
+ "type": "object",
+ },
+ "fieldRichText": {
+ "type": "string",
+ },
+ "fieldSelect": {
+ "enum": [
+ "OPTION_1",
+ "OPTION_2",
+ ],
+ "type": "string",
+ },
+ "fieldText": {
+ "type": "string",
+ },
+ "fieldUuid": {
+ "format": "uuid",
+ "type": "string",
+ },
+ },
+ "type": "object",
+ },
+}
+`);
+ });
+
+ const testsCases: EachTestingContext<
+ Pick<
+ FieldMetadataEntity,
+ 'id' | 'name' | 'type' | 'isNullable' | 'defaultValue' | 'settings'
+ >
+ >[] = [
+ {
+ title: 'Integer dataType with decimals',
+ context: {
+ id: 'number1',
+ name: 'number1',
+ type: FieldMetadataType.NUMBER,
+ isNullable: false,
+ defaultValue: null,
+ settings: { type: 'number', decimals: 1, dataType: NumberDataType.INT },
+ },
+ },
+ {
+ title: 'Float without decimals',
+ context: {
+ id: 'number2',
+ name: 'number2',
+ type: FieldMetadataType.NUMBER,
+ isNullable: false,
+ defaultValue: null,
+ settings: { type: 'number', dataType: NumberDataType.FLOAT },
+ },
+ },
+ {
+ title: 'Integer with a 0 decimals',
+ context: {
+ id: 'number3',
+ name: 'number3',
+ type: FieldMetadataType.NUMBER,
+ isNullable: false,
+ defaultValue: null,
+ settings: { type: 'number', decimals: 0, dataType: NumberDataType.INT },
+ },
+ },
+ ];
+
+ it.each(testsCases)('$title', ({ context: field }) => {
+ expect(
+ computeSchemaComponents([
+ {
+ targetTableName: 'testingObject',
+ id: 'mockObjectId',
+ nameSingular: 'objectName',
+ namePlural: 'objectsName',
+ //@ts-expect-error Passing partial FieldMetadataEntity array
+ fields: [field],
+ },
+ ]),
+ ).toMatchSnapshot();
});
});
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 ae373312b..d0529808f 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
@@ -1,6 +1,11 @@
import { OpenAPIV3_1 } from 'openapi-types';
-import { capitalize } from 'twenty-shared/utils';
import { FieldMetadataType } from 'twenty-shared/types';
+import { capitalize, isDefined } from 'twenty-shared/utils';
+
+import {
+ FieldMetadataSettings,
+ NumberDataType,
+} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import {
computeDepthParameters,
@@ -36,29 +41,47 @@ const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => {
}
};
-const getFieldProperties = (type: FieldMetadataType): Property => {
- switch (type) {
- case FieldMetadataType.UUID:
+const getFieldProperties = (field: FieldMetadataEntity): Property => {
+ switch (field.type) {
+ case FieldMetadataType.UUID: {
return { type: 'string', format: 'uuid' };
+ }
case FieldMetadataType.TEXT:
- case FieldMetadataType.RICH_TEXT:
+ case FieldMetadataType.RICH_TEXT: {
return { type: 'string' };
- case FieldMetadataType.DATE_TIME:
+ }
+ case FieldMetadataType.DATE_TIME: {
return { type: 'string', format: 'date-time' };
- case FieldMetadataType.DATE:
+ }
+ case FieldMetadataType.DATE: {
return { type: 'string', format: 'date' };
- case FieldMetadataType.NUMBER:
- return { type: 'integer' };
- case FieldMetadataType.NUMERIC:
- case FieldMetadataType.POSITION:
- return { type: 'number' };
- case FieldMetadataType.BOOLEAN:
- return { type: 'boolean' };
- case FieldMetadataType.RAW_JSON:
- return { type: 'object' };
+ }
+ case FieldMetadataType.NUMBER: {
+ const settings =
+ field.settings as FieldMetadataSettings;
- default:
+ if (
+ settings?.dataType === NumberDataType.FLOAT ||
+ (isDefined(settings?.decimals) && settings.decimals > 0)
+ ) {
+ return { type: 'number' };
+ }
+
+ return { type: 'integer' };
+ }
+ case FieldMetadataType.NUMERIC:
+ case FieldMetadataType.POSITION: {
+ return { type: 'number' };
+ }
+ case FieldMetadataType.BOOLEAN: {
+ return { type: 'boolean' };
+ }
+ case FieldMetadataType.RAW_JSON: {
+ return { type: 'object' };
+ }
+ default: {
return { type: 'string' };
+ }
}
};
@@ -282,7 +305,7 @@ const getSchemaComponentsProperties = ({
};
break;
default:
- itemProperty = getFieldProperties(field.type);
+ itemProperty = getFieldProperties(field);
break;
}