mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 03:42:30 +00:00
Fix composite subfields format in OpenAPI schema (#8592)
Fixes https://github.com/twentyhq/twenty/issues/7262 ## Before  ## After  
This commit is contained in:
@@ -23,7 +23,10 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldPhones: {
|
fieldPhones: {
|
||||||
properties: {
|
properties: {
|
||||||
additionalPhones: {
|
additionalPhones: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
primaryPhoneCountryCode: {
|
primaryPhoneCountryCode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -41,7 +44,11 @@ describe('computeSchemaComponents', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
additionalEmails: {
|
additionalEmails: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'email',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -85,6 +92,7 @@ describe('computeSchemaComponents', () => {
|
|||||||
properties: {
|
properties: {
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -200,7 +208,10 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldPhones: {
|
fieldPhones: {
|
||||||
properties: {
|
properties: {
|
||||||
additionalPhones: {
|
additionalPhones: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
primaryPhoneCountryCode: {
|
primaryPhoneCountryCode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -218,7 +229,11 @@ describe('computeSchemaComponents', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
additionalEmails: {
|
additionalEmails: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'email',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -262,6 +277,7 @@ describe('computeSchemaComponents', () => {
|
|||||||
properties: {
|
properties: {
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -376,7 +392,10 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldPhones: {
|
fieldPhones: {
|
||||||
properties: {
|
properties: {
|
||||||
additionalPhones: {
|
additionalPhones: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
primaryPhoneCountryCode: {
|
primaryPhoneCountryCode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -394,7 +413,11 @@ describe('computeSchemaComponents', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
additionalEmails: {
|
additionalEmails: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'email',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -438,6 +461,7 @@ describe('computeSchemaComponents', () => {
|
|||||||
properties: {
|
properties: {
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { OpenAPIV3_1 } from 'openapi-types';
|
import { OpenAPIV3_1 } from 'openapi-types';
|
||||||
|
|
||||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
computeDepthParameters,
|
computeDepthParameters,
|
||||||
computeEndingBeforeParameters,
|
computeEndingBeforeParameters,
|
||||||
@@ -11,7 +9,6 @@ import {
|
|||||||
computeOrderByParameters,
|
computeOrderByParameters,
|
||||||
computeStartingAfterParameters,
|
computeStartingAfterParameters,
|
||||||
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
|
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
|
||||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
|
||||||
import {
|
import {
|
||||||
FieldMetadataEntity,
|
FieldMetadataEntity,
|
||||||
FieldMetadataType,
|
FieldMetadataType,
|
||||||
@@ -41,18 +38,8 @@ const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFieldProperties = (
|
const getFieldProperties = (type: FieldMetadataType): Property => {
|
||||||
type: FieldMetadataType,
|
|
||||||
propertyName?: string,
|
|
||||||
options?: FieldMetadataOptions,
|
|
||||||
): Property => {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FieldMetadataType.SELECT:
|
|
||||||
case FieldMetadataType.MULTI_SELECT:
|
|
||||||
return {
|
|
||||||
type: 'string',
|
|
||||||
enum: options?.map((option: { value: string }) => option.value),
|
|
||||||
};
|
|
||||||
case FieldMetadataType.UUID:
|
case FieldMetadataType.UUID:
|
||||||
return { type: 'string', format: 'uuid' };
|
return { type: 'string', format: 'uuid' };
|
||||||
case FieldMetadataType.TEXT:
|
case FieldMetadataType.TEXT:
|
||||||
@@ -64,31 +51,12 @@ const getFieldProperties = (
|
|||||||
return { type: 'string', format: 'date' };
|
return { type: 'string', format: 'date' };
|
||||||
case FieldMetadataType.NUMBER:
|
case FieldMetadataType.NUMBER:
|
||||||
return { type: 'integer' };
|
return { type: 'integer' };
|
||||||
case FieldMetadataType.RATING:
|
|
||||||
return {
|
|
||||||
type: 'string',
|
|
||||||
enum: options?.map((option: { value: string }) => option.value),
|
|
||||||
};
|
|
||||||
case FieldMetadataType.NUMERIC:
|
case FieldMetadataType.NUMERIC:
|
||||||
case FieldMetadataType.POSITION:
|
case FieldMetadataType.POSITION:
|
||||||
return { type: 'number' };
|
return { type: 'number' };
|
||||||
case FieldMetadataType.BOOLEAN:
|
case FieldMetadataType.BOOLEAN:
|
||||||
return { type: 'boolean' };
|
return { type: 'boolean' };
|
||||||
case FieldMetadataType.RAW_JSON:
|
case FieldMetadataType.RAW_JSON:
|
||||||
if (propertyName === 'secondaryLinks') {
|
|
||||||
return {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
description: `A secondary link`,
|
|
||||||
properties: {
|
|
||||||
url: { type: 'string' },
|
|
||||||
label: { type: 'string' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { type: 'object' };
|
return { type: 'object' };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -147,32 +115,155 @@ const getSchemaComponentsProperties = ({
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case FieldMetadataType.LINKS:
|
case FieldMetadataType.LINKS:
|
||||||
case FieldMetadataType.CURRENCY:
|
|
||||||
case FieldMetadataType.FULL_NAME:
|
|
||||||
case FieldMetadataType.ADDRESS:
|
|
||||||
case FieldMetadataType.ACTOR:
|
|
||||||
case FieldMetadataType.EMAILS:
|
|
||||||
case FieldMetadataType.PHONES:
|
|
||||||
itemProperty = {
|
itemProperty = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: compositeTypeDefinitions
|
properties: {
|
||||||
.get(field.type)
|
primaryLinkLabel: {
|
||||||
?.properties?.reduce((properties, property) => {
|
type: 'string',
|
||||||
if (
|
},
|
||||||
property.hidden === true ||
|
primaryLinkUrl: {
|
||||||
(property.hidden === 'input' && !forResponse) ||
|
type: 'string',
|
||||||
(property.hidden === 'output' && forResponse)
|
},
|
||||||
) {
|
secondaryLinks: {
|
||||||
return properties;
|
type: 'array',
|
||||||
}
|
items: {
|
||||||
properties[property.name] = getFieldProperties(
|
type: 'object',
|
||||||
property.type,
|
description: 'A secondary link',
|
||||||
property.name,
|
properties: {
|
||||||
property.options,
|
url: {
|
||||||
);
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
return properties;
|
},
|
||||||
}, {} as Properties),
|
label: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.CURRENCY:
|
||||||
|
itemProperty = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
amountMicros: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
currencyCode: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.FULL_NAME:
|
||||||
|
itemProperty = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
firstName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.ADDRESS:
|
||||||
|
itemProperty = {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.ACTOR:
|
||||||
|
itemProperty = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
enum: [
|
||||||
|
'EMAIL',
|
||||||
|
'CALENDAR',
|
||||||
|
'WORKFLOW',
|
||||||
|
'API',
|
||||||
|
'IMPORT',
|
||||||
|
'MANUAL',
|
||||||
|
'SYSTEM',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(forResponse
|
||||||
|
? {
|
||||||
|
workspaceMemberId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.EMAILS:
|
||||||
|
itemProperty = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
primaryEmail: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
additionalEmails: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.PHONES:
|
||||||
|
itemProperty = {
|
||||||
|
properties: {
|
||||||
|
additionalPhones: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primaryPhoneCountryCode: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
primaryPhoneNumber: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -401,22 +492,59 @@ export const computeMetadataSchemaComponents = (
|
|||||||
return schemas;
|
return schemas;
|
||||||
}
|
}
|
||||||
case 'field': {
|
case 'field': {
|
||||||
schemas[`${capitalize(item.nameSingular)}`] = {
|
const baseFieldProperties = ({
|
||||||
|
withImmutableFields,
|
||||||
|
withRequiredFields,
|
||||||
|
}: {
|
||||||
|
withImmutableFields: boolean;
|
||||||
|
withRequiredFields: boolean;
|
||||||
|
}): OpenAPIV3_1.SchemaObject => ({
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: `A field`,
|
description: `A field`,
|
||||||
properties: {
|
properties: {
|
||||||
type: {
|
...(withImmutableFields
|
||||||
type: 'string',
|
? {
|
||||||
enum: Object.keys(FieldMetadataType),
|
type: {
|
||||||
},
|
type: 'string',
|
||||||
|
enum: Object.keys(FieldMetadataType),
|
||||||
|
},
|
||||||
|
objectMetadataId: { type: 'string', format: 'uuid' },
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
name: { type: 'string' },
|
name: { type: 'string' },
|
||||||
label: { type: 'string' },
|
label: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
icon: { type: 'string' },
|
icon: { type: 'string' },
|
||||||
|
defaultValue: {},
|
||||||
isNullable: { type: 'boolean' },
|
isNullable: { type: 'boolean' },
|
||||||
objectMetadataId: { type: 'string', format: 'uuid' },
|
settings: { type: 'object' },
|
||||||
|
options: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'For enum field types like SELECT or MULTI_SELECT',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
color: { type: 'string' },
|
||||||
|
label: { type: 'string' },
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
pattern: '^[A-Z0-9]+_[A-Z0-9]+$',
|
||||||
|
example: 'OPTION_1',
|
||||||
|
},
|
||||||
|
position: { type: 'number' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
...(withRequiredFields
|
||||||
|
? { required: ['type', 'name', 'label', 'objectMetadataId'] }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
schemas[`${capitalize(item.nameSingular)}`] = baseFieldProperties({
|
||||||
|
withImmutableFields: true,
|
||||||
|
withRequiredFields: true,
|
||||||
|
});
|
||||||
schemas[`${capitalize(item.namePlural)}`] = {
|
schemas[`${capitalize(item.namePlural)}`] = {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: `A list of ${item.namePlural}`,
|
description: `A list of ${item.namePlural}`,
|
||||||
@@ -424,38 +552,22 @@ export const computeMetadataSchemaComponents = (
|
|||||||
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
|
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
schemas[`${capitalize(item.nameSingular)} for Update`] = {
|
schemas[`${capitalize(item.nameSingular)} for Update`] =
|
||||||
type: 'object',
|
baseFieldProperties({
|
||||||
description: `An object`,
|
withImmutableFields: false,
|
||||||
properties: {
|
withRequiredFields: false,
|
||||||
description: { type: 'string' },
|
});
|
||||||
icon: { type: 'string' },
|
|
||||||
isActive: { type: 'boolean' },
|
|
||||||
isCustom: { type: 'boolean' },
|
|
||||||
isNullable: { type: 'boolean' },
|
|
||||||
isSystem: { type: 'boolean' },
|
|
||||||
label: { type: 'string' },
|
|
||||||
name: { type: 'string' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
schemas[`${capitalize(item.nameSingular)} for Response`] = {
|
schemas[`${capitalize(item.nameSingular)} for Response`] = {
|
||||||
...schemas[`${capitalize(item.nameSingular)}`],
|
...baseFieldProperties({
|
||||||
|
withImmutableFields: true,
|
||||||
|
withRequiredFields: false,
|
||||||
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
type: {
|
...schemas[`${capitalize(item.nameSingular)}`].properties,
|
||||||
type: 'string',
|
|
||||||
enum: Object.keys(FieldMetadataType),
|
|
||||||
},
|
|
||||||
name: { type: 'string' },
|
|
||||||
label: { type: 'string' },
|
|
||||||
description: { type: 'string' },
|
|
||||||
icon: { type: 'string' },
|
|
||||||
isNullable: { type: 'boolean' },
|
|
||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
isCustom: { type: 'boolean' },
|
isCustom: { type: 'boolean' },
|
||||||
isActive: { type: 'boolean' },
|
isActive: { type: 'boolean' },
|
||||||
isSystem: { type: 'boolean' },
|
isSystem: { type: 'boolean' },
|
||||||
defaultValue: { type: 'object' },
|
|
||||||
options: { type: 'object' },
|
|
||||||
createdAt: { type: 'string', format: 'date-time' },
|
createdAt: { type: 'string', format: 'date-time' },
|
||||||
updatedAt: { type: 'string', format: 'date-time' },
|
updatedAt: { type: 'string', format: 'date-time' },
|
||||||
fromRelationMetadata: {
|
fromRelationMetadata: {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ Queues facilitate async operations to be performed. They can be used for perform
|
|||||||
Each use case will have its own queue class extended from `MessageQueueServiceBase`.
|
Each use case will have its own queue class extended from `MessageQueueServiceBase`.
|
||||||
|
|
||||||
Currently, queue supports two drivers which can be configured by env variable `MESSAGE_QUEUE_TYPE`.
|
Currently, queue supports two drivers which can be configured by env variable `MESSAGE_QUEUE_TYPE`.
|
||||||
1. `pg-boss`: this is the default driver, which uses [pg-boss](https://github.com/timgit/pg-boss) under the hood.
|
1. `bull-mq`: this is the default driver, which uses [bull-mq](https://bullmq.io/) under the hood.
|
||||||
2. `bull-mq`: this uses [bull-mq](https://bullmq.io/) under the hood.
|
2. `pg-boss`: this uses [pg-boss](https://github.com/timgit/pg-boss) under the hood.
|
||||||
|
|
||||||
## Steps to create and use a new queue
|
## Steps to create and use a new queue
|
||||||
|
|
||||||
@@ -43,4 +43,4 @@ class CustomWorker {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<ArticleEditContent></ArticleEditContent>
|
<ArticleEditContent></ArticleEditContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user