12660 bugapi create one person post api request example is returning 400 in playground (#12787)

Use faker to provide simple working examples for REST API create one,
create many, update one and find duplicates

Eg:
<img width="1505" alt="image"
src="https://github.com/user-attachments/assets/99be990f-efd6-4ad7-8c29-f9dcecac112f"
/>
This commit is contained in:
martmull
2025-06-23 18:24:42 +02:00
committed by GitHub
parent 06fddc2ae0
commit 6e4dc16f2b
6 changed files with 302 additions and 22 deletions

View File

@ -4,6 +4,9 @@ exports[`computeSchemaComponents Float without decimals 1`] = `
{
"ObjectName": {
"description": undefined,
"example": {
"number2": 692.1852368365229,
},
"properties": {
"number2": {
"type": "number",
@ -25,6 +28,9 @@ exports[`computeSchemaComponents Float without decimals 1`] = `
},
"ObjectNameForUpdate": {
"description": undefined,
"example": {
"number2": 316.2001153750569,
},
"properties": {
"number2": {
"type": "number",
@ -39,6 +45,9 @@ exports[`computeSchemaComponents Integer dataType with decimals 1`] = `
{
"ObjectName": {
"description": undefined,
"example": {
"number1": 957.9316406203515,
},
"properties": {
"number1": {
"type": "number",
@ -60,6 +69,9 @@ exports[`computeSchemaComponents Integer dataType with decimals 1`] = `
},
"ObjectNameForUpdate": {
"description": undefined,
"example": {
"number1": 533.6321196880441,
},
"properties": {
"number1": {
"type": "number",
@ -74,6 +86,9 @@ exports[`computeSchemaComponents Integer with a 0 decimals 1`] = `
{
"ObjectName": {
"description": undefined,
"example": {
"number3": 686.8144267539021,
},
"properties": {
"number3": {
"type": "integer",
@ -95,6 +110,9 @@ exports[`computeSchemaComponents Integer with a 0 decimals 1`] = `
},
"ObjectNameForUpdate": {
"description": undefined,
"example": {
"number3": 834.7910462254755,
},
"properties": {
"number3": {
"type": "integer",

View File

@ -1,5 +1,6 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { faker } from '@faker-js/faker';
import { NumberDataType } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
@ -9,6 +10,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
describe('computeSchemaComponents', () => {
faker.seed(1);
it('should compute schema components', () => {
expect(
computeSchemaComponents([
@ -18,6 +20,36 @@ describe('computeSchemaComponents', () => {
{
"ObjectName": {
"description": undefined,
"example": {
"fieldCurrency": {
"amountMicros": 284000000,
"currencyCode": "EUR",
},
"fieldEmails": {
"additionalEmails": null,
"primaryEmail": "mina.gutmann9@hotmail.com",
},
"fieldFullName": {
"firstName": "Shad",
"lastName": "Osinski",
},
"fieldLinks": {
"additionalLinks": [],
"primaryLinkLabel": "",
"primaryLinkUrl": "https://narrow-help.net/",
},
"fieldMultiSelect": [
"OPTION_1",
],
"fieldNumber": 346.2151663160047,
"fieldPhones": {
"additionalPhones": [],
"primaryPhoneCallingCode": "+33",
"primaryPhoneCountryCode": "FR",
"primaryPhoneNumber": "06 10 20 30 40",
},
"fieldSelect": "OPTION_1",
},
"properties": {
"fieldActor": {
"properties": {
@ -444,6 +476,36 @@ describe('computeSchemaComponents', () => {
},
"ObjectNameForUpdate": {
"description": undefined,
"example": {
"fieldCurrency": {
"amountMicros": 253000000,
"currencyCode": "EUR",
},
"fieldEmails": {
"additionalEmails": null,
"primaryEmail": "keegan_donnelly96@hotmail.com",
},
"fieldFullName": {
"firstName": "Shad",
"lastName": "Jones",
},
"fieldLinks": {
"additionalLinks": [],
"primaryLinkLabel": "",
"primaryLinkUrl": "https://unlawful-blowgun.biz",
},
"fieldMultiSelect": [
"OPTION_1",
],
"fieldNumber": 692.6302930536448,
"fieldPhones": {
"additionalPhones": [],
"primaryPhoneCallingCode": "+33",
"primaryPhoneCountryCode": "FR",
"primaryPhoneNumber": "06 10 20 30 40",
},
"fieldSelect": "OPTION_1",
},
"properties": {
"fieldActor": {
"properties": {

View File

@ -7,6 +7,7 @@ import {
NumberDataType,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import {
computeDepthParameters,
@ -20,6 +21,8 @@ import {
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { camelToTitleCase } from 'src/utils/camel-to-title-case';
import { generateRandomFieldValue } from 'src/engine/core-modules/open-api/utils/generate-random-field-value.utils';
type Property = OpenAPIV3_1.SchemaObject;
@ -27,6 +30,8 @@ type Properties = {
[name: string]: Property;
};
type OpenApiExample = Record<string, FieldMetadataDefaultValue>;
const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => {
if (forResponse) {
return true;
@ -86,6 +91,47 @@ const getFieldProperties = (field: FieldMetadataEntity): Property => {
}
};
const getSchemaComponentsExample = (
item: ObjectMetadataEntity,
): OpenApiExample => {
return item.fields.reduce((node, field) => {
// If field is required
if (!field.isNullable && field.defaultValue === null) {
return { ...node, [field.name]: generateRandomFieldValue({ field }) };
}
switch (field.type) {
case FieldMetadataType.TEXT: {
if (field.name !== 'name') {
return node;
}
return {
...node,
[field.name]: `${camelToTitleCase(item.nameSingular)} name`,
};
}
case FieldMetadataType.EMAILS:
case FieldMetadataType.LINKS:
case FieldMetadataType.CURRENCY:
case FieldMetadataType.FULL_NAME:
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.PHONES: {
return {
...node,
[field.name]: generateRandomFieldValue({ field }),
};
}
default: {
return node;
}
}
}, {});
};
const getSchemaComponentsProperties = ({
item,
forResponse,
@ -105,12 +151,13 @@ const getSchemaComponentsProperties = ({
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) &&
field.settings?.relationType === RelationType.MANY_TO_ONE
) {
node[`${field.name}Id`] = {
type: 'string',
format: 'uuid',
return {
...node,
[`${field.name}Id`]: {
type: 'string',
format: 'uuid',
},
};
return node;
}
if (
@ -333,7 +380,7 @@ const getSchemaComponentsProperties = ({
}
if (Object.keys(itemProperty).length) {
node[field.name] = itemProperty;
return { ...node, [field.name]: itemProperty };
}
return node;
@ -379,7 +426,7 @@ const getSchemaComponentsRelationProperties = (
}
if (Object.keys(itemProperty).length) {
node[field.name] = itemProperty;
return { ...node, [field.name]: itemProperty };
}
return node;
@ -400,20 +447,23 @@ const getRequiredFields = (item: ObjectMetadataEntity): string[] => {
const computeSchemaComponent = ({
item,
withRequiredFields,
forResponse,
withRelations,
forUpdate,
}: {
item: ObjectMetadataEntity;
withRequiredFields: boolean;
forResponse: boolean;
withRelations: boolean;
forUpdate: boolean;
}): OpenAPIV3_1.SchemaObject => {
const result = {
const withRelations = forResponse && !forUpdate;
const withRequiredFields = !forResponse && !forUpdate;
const result: OpenAPIV3_1.SchemaObject = {
type: 'object',
description: item.description,
properties: getSchemaComponentsProperties({ item, forResponse }),
} as OpenAPIV3_1.SchemaObject;
...(!forResponse ? { example: getSchemaComponentsExample(item) } : {}),
};
if (withRelations) {
result.properties = {
@ -442,23 +492,20 @@ export const computeSchemaComponents = (
(schemas, item) => {
schemas[capitalize(item.nameSingular)] = computeSchemaComponent({
item,
withRequiredFields: true,
forResponse: false,
withRelations: false,
forUpdate: false,
});
schemas[capitalize(item.nameSingular) + 'ForUpdate'] =
computeSchemaComponent({
item,
withRequiredFields: false,
forResponse: false,
withRelations: false,
forUpdate: true,
});
schemas[capitalize(item.nameSingular) + 'ForResponse'] =
computeSchemaComponent({
item,
withRequiredFields: false,
forResponse: true,
withRelations: true,
forUpdate: false,
});
return schemas;

View File

@ -0,0 +1,144 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { faker } from '@faker-js/faker';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const generateRandomFieldValue = ({
field,
}: {
field: FieldMetadataEntity;
}): FieldMetadataDefaultValue => {
switch (field.type) {
case FieldMetadataType.UUID: {
return v4();
}
case FieldMetadataType.TEXT: {
return faker.string.fromCharacters(field.name);
}
case FieldMetadataType.PHONES: {
return {
primaryPhoneNumber: '06 10 20 30 40',
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'FR',
additionalPhones: [],
};
}
case FieldMetadataType.EMAILS: {
return {
primaryEmail: faker.internet.email().toLowerCase(),
additionalEmails: null,
};
}
case FieldMetadataType.DATE:
case FieldMetadataType.DATE_TIME: {
return faker.date.soon();
}
case FieldMetadataType.BOOLEAN: {
return false;
}
case FieldMetadataType.NUMBER: {
return faker.number.float({ min: 1, max: 1_000 });
}
case FieldMetadataType.NUMERIC: {
return faker.number.int({ min: 1, max: 1_000 });
}
case FieldMetadataType.LINKS: {
return {
primaryLinkLabel: '',
primaryLinkUrl: faker.internet.url(),
additionalLinks: [],
};
}
case FieldMetadataType.CURRENCY: {
return {
amountMicros: faker.number.int({ min: 100, max: 1_000 }) * 1_000_000,
currencyCode: 'EUR',
};
}
case FieldMetadataType.FULL_NAME: {
return {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
};
}
case FieldMetadataType.RATING: {
return 'RATING_5';
}
case FieldMetadataType.SELECT: {
return isDefined(field.options[0].value) ? field.options[0].value : [];
}
case FieldMetadataType.MULTI_SELECT: {
return isDefined(field.options[0].value) ? [field.options[0].value] : [];
}
case FieldMetadataType.RELATION: {
return null;
}
case FieldMetadataType.POSITION: {
return 1;
}
case FieldMetadataType.ADDRESS: {
return {
addressStreet1: faker.location.streetAddress(),
addressStreet2: faker.location.secondaryAddress(),
addressCity: faker.location.city(),
addressState: faker.location.state(),
addressCountry: faker.location.country(),
addressPostcode: faker.location.zipCode(),
addressLat: faker.location.latitude(),
addressLng: faker.location.longitude(),
};
}
case FieldMetadataType.RAW_JSON: {
return {};
}
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.RICH_TEXT_V2: {
return '';
}
case FieldMetadataType.ACTOR: {
return {
source: 'MANUAL',
context: {},
name: faker.person.fullName(),
workspaceMemberId: null,
};
}
case FieldMetadataType.ARRAY: {
return [];
}
case FieldMetadataType.TS_VECTOR: {
throw new Error(
`We should not generate fake version for ${field.type} field`,
);
}
default: {
assertUnreachable(field.type, `Unsupported field type '${field.type}'`);
}
}
};

View File

@ -1,3 +1,5 @@
import { v4 } from 'uuid';
export const getRequestBody = (name: string) => {
return {
description: 'body',
@ -59,8 +61,13 @@ export const getFindDuplicatesRequestBody = (name: string) => {
},
ids: {
type: 'array',
items: {
type: 'string',
format: 'uuid',
},
},
},
example: { ids: [v4()] },
},
},
},

View File

@ -1,5 +1,7 @@
import { capitalize } from 'twenty-shared/utils';
export const camelToTitleCase = (camelCaseText: string) =>
capitalize(camelCaseText)
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
capitalize(
camelCaseText
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase()),
);