Setup relations for remote objects (#5149)

New strategy:
- add settings field on FieldMetadata. Contains a boolean isIdField and
for numbers, a precision
- if idField, the graphql scalar returned will be a GraphQL id. This
will allow the app to work even for ids that are not uuid
- remove globals dateScalar and numberScalar modes. These were not used
- set limit as Integer
- check manually in query runner mutations that we send a valid id

Todo left:
- remove WorkspaceBuildSchemaOptions since this is not used anymore.
Will do in another PR

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Thomas Trompette 2024-04-26 14:37:34 +02:00 committed by GitHub
parent dc576d0818
commit 224c8d361b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 616 additions and 223 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -861,6 +861,7 @@ export type Field = {
name: Scalars['String']; name: Scalars['String'];
options?: Maybe<Scalars['JSON']>; options?: Maybe<Scalars['JSON']>;
relationDefinition?: Maybe<RelationDefinition>; relationDefinition?: Maybe<RelationDefinition>;
settings?: Maybe<Scalars['JSON']>;
toRelationMetadata?: Maybe<Relation>; toRelationMetadata?: Maybe<Relation>;
type: FieldMetadataType; type: FieldMetadataType;
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];

View File

@ -51,7 +51,7 @@ const mocks: MockedResponse[] = [
$filter: ActivityTargetFilterInput $filter: ActivityTargetFilterInput
$orderBy: ActivityTargetOrderByInput $orderBy: ActivityTargetOrderByInput
$lastCursor: String $lastCursor: String
$limit: Float $limit: Int
) { ) {
activityTargets( activityTargets(
filter: $filter filter: $filter
@ -105,7 +105,7 @@ const mocks: MockedResponse[] = [
$filter: ActivityFilterInput $filter: ActivityFilterInput
$orderBy: ActivityOrderByInput $orderBy: ActivityOrderByInput
$lastCursor: String $lastCursor: String
$limit: Float $limit: Int
) { ) {
activities( activities(
filter: $filter filter: $filter

View File

@ -36,7 +36,7 @@ const mocks: MockedResponse[] = [
$filter: ActivityTargetFilterInput $filter: ActivityTargetFilterInput
$orderBy: ActivityTargetOrderByInput $orderBy: ActivityTargetOrderByInput
$lastCursor: String $lastCursor: String
$limit: Float $limit: Int
) { ) {
activityTargets( activityTargets(
filter: $filter filter: $filter

View File

@ -16,7 +16,7 @@ const mocks: MockedResponse[] = [
{ {
request: { request: {
query: gql` query: gql`
query FindOneWorkspaceMember($objectRecordId: UUID!) { query FindOneWorkspaceMember($objectRecordId: ID!) {
workspaceMember(filter: { id: { eq: $objectRecordId } }) { workspaceMember(filter: { id: { eq: $objectRecordId } }) {
__typename __typename
colorScheme colorScheme

View File

@ -17,7 +17,7 @@ const mocks: MockedResponse[] = [
request: { request: {
query: gql` query: gql`
mutation UpdateOneActivity( mutation UpdateOneActivity(
$idToUpdate: UUID! $idToUpdate: ID!
$input: ActivityUpdateInput! $input: ActivityUpdateInput!
) { ) {
updateActivity(id: $idToUpdate, data: $input) { updateActivity(id: $idToUpdate, data: $input) {

View File

@ -177,7 +177,7 @@ export const mocks = [
{ {
request: { request: {
query: gql` query: gql`
mutation DeleteOneFavorite($idToDelete: UUID!) { mutation DeleteOneFavorite($idToDelete: ID!) {
deleteFavorite(id: $idToDelete) { deleteFavorite(id: $idToDelete) {
id id
} }
@ -197,7 +197,7 @@ export const mocks = [
request: { request: {
query: gql` query: gql`
mutation UpdateOneFavorite( mutation UpdateOneFavorite(
$idToUpdate: UUID! $idToUpdate: ID!
$input: FavoriteUpdateInput! $input: FavoriteUpdateInput!
) { ) {
updateFavorite(id: $idToUpdate, data: $input) { updateFavorite(id: $idToUpdate, data: $input) {

View File

@ -48,6 +48,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
nameSingular nameSingular
namePlural namePlural
isSystem isSystem
isRemote
} }
toFieldMetadataId toFieldMetadataId
} }
@ -60,6 +61,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
nameSingular nameSingular
namePlural namePlural
isSystem isSystem
isRemote
} }
fromFieldMetadataId fromFieldMetadataId
} }

View File

@ -26,7 +26,7 @@ export const findManyViewsQuery = gql`
$filter: ViewFilterInput $filter: ViewFilterInput
$orderBy: ViewOrderByInput $orderBy: ViewOrderByInput
$lastCursor: String $lastCursor: String
$limit: Float $limit: Int
) { ) {
views( views(
filter: $filter filter: $filter

View File

@ -21,6 +21,7 @@ export type FieldMetadataItem = Omit<
| 'toRelationMetadata' | 'toRelationMetadata'
| 'defaultValue' | 'defaultValue'
| 'options' | 'options'
| 'settings'
| 'relationDefinition' | 'relationDefinition'
> & { > & {
__typename?: string; __typename?: string;
@ -28,7 +29,7 @@ export type FieldMetadataItem = Omit<
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & { | (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
toObjectMetadata: Pick< toObjectMetadata: Pick<
Relation['toObjectMetadata'], Relation['toObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem' 'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>; >;
}) })
| null; | null;
@ -36,7 +37,7 @@ export type FieldMetadataItem = Omit<
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & { | (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
fromObjectMetadata: Pick< fromObjectMetadata: Pick<
Relation['fromObjectMetadata'], Relation['fromObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem' 'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>; >;
}) })
| null; | null;

View File

@ -2,10 +2,15 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const isObjectMetadataAvailableForRelation = ( export const isObjectMetadataAvailableForRelation = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isSystem' | 'nameSingular'>, objectMetadataItem: Pick<
ObjectMetadataItem,
'isSystem' | 'nameSingular' | 'isRemote'
>,
) => { ) => {
return ( return (
!objectMetadataItem.isSystem || (!objectMetadataItem.isSystem ||
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkspaceMember objectMetadataItem.nameSingular ===
CoreObjectNameSingular.WorkspaceMember) &&
!objectMetadataItem.isRemote
); );
}; };

View File

@ -1,8 +1,6 @@
import { useContext } from 'react';
import { EntityChip, EntityChipVariant } from 'twenty-ui'; import { EntityChip, EntityChipVariant } from 'twenty-ui';
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
@ -25,16 +23,8 @@ export const RecordChip = ({
objectNameSingular, objectNameSingular,
}); });
// Will only exists if the chip is inside a record table.
// This is temporary until we have the show page for remote objects.
const { isReadOnly } = useContext(RecordTableRowContext);
const objectRecordIdentifier = mapToObjectRecordIdentifier(record); const objectRecordIdentifier = mapToObjectRecordIdentifier(record);
const linkToEntity = isReadOnly
? undefined
: objectRecordIdentifier.linkToShowPage;
return ( return (
<EntityChip <EntityChip
entityId={record.id} entityId={record.id}
@ -43,7 +33,7 @@ export const RecordChip = ({
avatarUrl={ avatarUrl={
getImageAbsoluteURIOrBase64(objectRecordIdentifier.avatarUrl) || '' getImageAbsoluteURIOrBase64(objectRecordIdentifier.avatarUrl) || ''
} }
linkToEntity={linkToEntity} linkToEntity={objectRecordIdentifier.linkToShowPage}
maxWidth={maxWidth} maxWidth={maxWidth}
className={className} className={className}
variant={variant} variant={variant}

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const query = gql` export const query = gql`
mutation DeleteOnePerson($idToDelete: UUID!) { mutation DeleteOnePerson($idToDelete: ID!) {
deletePerson(id: $idToDelete) { deletePerson(id: $idToDelete) {
id id
} }

View File

@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
export { responseData } from './useUpdateOneRecord'; export { responseData } from './useUpdateOneRecord';
export const query = gql` export const query = gql`
mutation ExecuteQuickActionOnOnePerson($idToExecuteQuickActionOn: UUID!) { mutation ExecuteQuickActionOnOnePerson($idToExecuteQuickActionOn: ID!) {
executeQuickActionOnPerson(id: $idToExecuteQuickActionOn) { executeQuickActionOnPerson(id: $idToExecuteQuickActionOn) {
__typename __typename
xLink { xLink {

View File

@ -5,7 +5,7 @@ export const query = gql`
$filter: PersonFilterInput $filter: PersonFilterInput
$orderBy: PersonOrderByInput $orderBy: PersonOrderByInput
$lastCursor: String $lastCursor: String
$limit: Float $limit: Int
) { ) {
people( people(
filter: $filter filter: $filter

View File

@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
import { responseData as person } from './useUpdateOneRecord'; import { responseData as person } from './useUpdateOneRecord';
export const query = gql` export const query = gql`
query FindOnePerson($objectRecordId: UUID!) { query FindOnePerson($objectRecordId: ID!) {
person(filter: { id: { eq: $objectRecordId } }) { person(filter: { id: { eq: $objectRecordId } }) {
__typename __typename
xLink { xLink {

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const query = gql` export const query = gql`
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) { mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) { updatePerson(id: $idToUpdate, data: $input) {
__typename __typename
xLink { xLink {

View File

@ -26,7 +26,7 @@ export const useDeleteOneRecordMutation = ({
); );
const deleteOneRecordMutation = gql` const deleteOneRecordMutation = gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: UUID!) { mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
${mutationResponseField}(id: $idToDelete) { ${mutationResponseField}(id: $idToDelete) {
id id
} }

View File

@ -39,7 +39,7 @@ export const useExecuteQuickActionOnOneRecordMutation = ({
}); });
const executeQuickActionOnOneRecordMutation = gql` const executeQuickActionOnOneRecordMutation = gql`
mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: UUID!) { mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: ID!) {
${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) ${mapObjectMetadataToGraphQLQuery( ${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) ${mapObjectMetadataToGraphQLQuery(
{ {
objectMetadataItems, objectMetadataItems,

View File

@ -23,7 +23,7 @@ export const useFindDuplicateRecordsQuery = ({
const findDuplicateRecordsQuery = gql` const findDuplicateRecordsQuery = gql`
query FindDuplicate${capitalize( query FindDuplicate${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}($id: UUID) { )}($id: ID!) {
${getFindDuplicateRecordsQueryResponseField( ${getFindDuplicateRecordsQueryResponseField(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}(id: $id) { )}(id: $id) {

View File

@ -22,7 +22,7 @@ export const useFindOneRecordQuery = ({
const findOneRecordQuery = gql` const findOneRecordQuery = gql`
query FindOne${capitalize( query FindOne${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}($objectRecordId: UUID!) { )}($objectRecordId: ID!) {
${objectMetadataItem.nameSingular}(filter: { ${objectMetadataItem.nameSingular}(filter: {
id: { id: {
eq: $objectRecordId eq: $objectRecordId
@ -32,7 +32,7 @@ export const useFindOneRecordQuery = ({
objectMetadataItem, objectMetadataItem,
depth, depth,
})} })}
} },
`; `;
return { return {

View File

@ -35,7 +35,7 @@ export const useUpdateOneRecordMutation = ({
); );
const updateOneRecordMutation = gql` const updateOneRecordMutation = gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) { mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery( ${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
{ {
objectMetadataItems, objectMetadataItems,

View File

@ -47,7 +47,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({
const limitPerMetadataItemArray = capitalizedObjectNameSingulars const limitPerMetadataItemArray = capitalizedObjectNameSingulars
.map( .map(
(capitalizedObjectNameSingular) => (capitalizedObjectNameSingular) =>
`$limit${capitalizedObjectNameSingular}: Float`, `$limit${capitalizedObjectNameSingular}: Int`,
) )
.join(', '); .join(', ');

View File

@ -21,7 +21,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
const query = gql` const query = gql`
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) { mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) { updatePerson(id: $idToUpdate, data: $input) {
__typename __typename
xLink { xLink {

View File

@ -21,7 +21,7 @@ const mocks: MockedResponse[] = [
request: { request: {
query: gql` query: gql`
mutation UpdateOneCompany( mutation UpdateOneCompany(
$idToUpdate: UUID! $idToUpdate: ID!
$input: CompanyUpdateInput! $input: CompanyUpdateInput!
) { ) {
updateCompany(id: $idToUpdate, data: $input) { updateCompany(id: $idToUpdate, data: $input) {

View File

@ -8,7 +8,7 @@ import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFi
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue'; import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue'; import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue'; import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';

View File

@ -13,7 +13,7 @@ const query = gql`
$filterNameSingular: NameSingularFilterInput $filterNameSingular: NameSingularFilterInput
$orderByNameSingular: NameSingularOrderByInput $orderByNameSingular: NameSingularOrderByInput
$lastCursorNameSingular: String $lastCursorNameSingular: String
$limitNameSingular: Float $limitNameSingular: Int
) { ) {
namePlural( namePlural(
filter: $filterNameSingular filter: $filterNameSingular

View File

@ -24,7 +24,7 @@ query FindMany${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize( )}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Float) { )}OrderByInput, $lastCursor: String, $limit: Int) {
${ ${
objectMetadataItem.namePlural objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){

View File

@ -5,7 +5,7 @@ export const query = gql`
$filter: PersonFilterInput $filter: PersonFilterInput
$orderBy: PersonOrderByInput $orderBy: PersonOrderByInput
$lastCursor: String $lastCursor: String
$limit: Float = 60 $limit: Int = 60
) { ) {
people( people(
filter: $filter filter: $filter

View File

@ -65,7 +65,12 @@ export const ShowPageRightContainer = ({
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(activeTabIdState); const activeTabId = useRecoilValue(activeTabIdState);
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED'); const shouldDisplayCalendarTab =
useIsFeatureEnabled('IS_CALENDAR_ENABLED') &&
(targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Company ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Person);
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED'); const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
const shouldDisplayEmailsTab = const shouldDisplayEmailsTab =

View File

@ -96,11 +96,6 @@ export const RecordShowPage = () => {
? `${pageName} - ${capitalize(objectNameSingular)}` ? `${pageName} - ${capitalize(objectNameSingular)}`
: capitalize(objectNameSingular); : capitalize(objectNameSingular);
// Temporarily since we don't have relations for remote objects yet
if (objectMetadataItem.isRemote) {
return null;
}
return ( return (
<PageContainer> <PageContainer>
<PageTitle title={pageTitle} /> <PageTitle title={pageTitle} />

View File

@ -88,6 +88,8 @@ export const SettingsObjectDetail = () => {
}, },
}); });
const shouldDisplayAddFieldButton = !activeObjectMetadataItem.isRemote;
return ( return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings"> <SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer> <SettingsPageContainer>
@ -207,6 +209,7 @@ export const SettingsObjectDetail = () => {
</TableSection> </TableSection>
)} )}
</Table> </Table>
{shouldDisplayAddFieldButton && (
<StyledDiv> <StyledDiv>
<Button <Button
Icon={IconPlus} Icon={IconPlus}
@ -222,6 +225,7 @@ export const SettingsObjectDetail = () => {
} }
/> />
</StyledDiv> </StyledDiv>
)}
</Section> </Section>
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>

View File

@ -9,6 +9,7 @@ export const mockedClientConfig: ClientConfig = {
google: true, google: true,
password: true, password: true,
magicLink: false, magicLink: false,
microsoft: false,
__typename: 'AuthProviders', __typename: 'AuthProviders',
}, },
telemetry: { telemetry: {

View File

@ -63,6 +63,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'person', nameSingular: 'person',
namePlural: 'people', namePlural: 'people',
isSystem: false, isSystem: false,
isRemote: false,
}, },
toFieldMetadataId: 'c756f6ff-8c00-4fe5-a923-c6cfc7b1ac4a', toFieldMetadataId: 'c756f6ff-8c00-4fe5-a923-c6cfc7b1ac4a',
}, },
@ -91,6 +92,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'opportunity', nameSingular: 'opportunity',
namePlural: 'opportunities', namePlural: 'opportunities',
isSystem: false, isSystem: false,
isRemote: false,
}, },
toFieldMetadataId: '00468e2a-a601-4635-ae9c-a9bb826cc860', toFieldMetadataId: '00468e2a-a601-4635-ae9c-a9bb826cc860',
}, },
@ -119,6 +121,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'activityTarget', nameSingular: 'activityTarget',
namePlural: 'activityTargets', namePlural: 'activityTargets',
isSystem: true, isSystem: true,
isRemote: false,
}, },
toFieldMetadataId: 'bba19feb-c248-487b-92d7-98df54c51e44', toFieldMetadataId: 'bba19feb-c248-487b-92d7-98df54c51e44',
}, },
@ -221,6 +224,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'attachment', nameSingular: 'attachment',
namePlural: 'attachments', namePlural: 'attachments',
isSystem: true, isSystem: true,
isRemote: false,
}, },
toFieldMetadataId: '0880dac5-37d2-43a6-b143-722126d4923f', toFieldMetadataId: '0880dac5-37d2-43a6-b143-722126d4923f',
}, },
@ -331,6 +335,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'workspaceMember', nameSingular: 'workspaceMember',
namePlural: 'workspaceMembers', namePlural: 'workspaceMembers',
isSystem: true, isSystem: true,
isRemote: false,
}, },
fromFieldMetadataId: '0f3e456f-3bb4-4261-a436-95246dc0e159', fromFieldMetadataId: '0f3e456f-3bb4-4261-a436-95246dc0e159',
}, },
@ -378,6 +383,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'favorite', nameSingular: 'favorite',
namePlural: 'favorites', namePlural: 'favorites',
isSystem: true, isSystem: true,
isRemote: false,
}, },
toFieldMetadataId: '8fd8965b-bd4e-4a9b-90e9-c75652dadda1', toFieldMetadataId: '8fd8965b-bd4e-4a9b-90e9-c75652dadda1',
}, },

View File

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSettingsColumnToFieldMetadata1713793656356
implements MigrationInterface
{
name = 'AddSettingsColumnToFieldMetadata1713793656356';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" ADD "settings" jsonb`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."remoteServer" DROP COLUMN "foreignDataWrapperType"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."remoteServer" ADD "foreignDataWrapperType" text`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."remoteServer" DROP COLUMN "foreignDataWrapperType"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."remoteServer" ADD "foreignDataWrapperType" character varying`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "settings"`,
);
}
}

View File

@ -78,14 +78,16 @@ export class FindDuplicatesQueryFactory {
} }
buildQueryForExistingRecord( buildQueryForExistingRecord(
id: string, id: string | number,
options: WorkspaceQueryBuilderOptions, options: WorkspaceQueryBuilderOptions,
) { ) {
const idQueryField = typeof id === 'string' ? `"${id}"` : id;
return ` return `
query { query {
${computeObjectTargetTable( ${computeObjectTargetTable(
options.objectMetadataItem, options.objectMetadataItem,
)}Collection(filter: { id: { eq: "${id}" }}){ )}Collection(filter: { id: { eq: ${idQueryField} }}){
edges { edges {
node { node {
__typename __typename

View File

@ -77,7 +77,7 @@ export class WorkspaceQueryBuilderFactory {
} }
findDuplicatesExistingRecord( findDuplicatesExistingRecord(
id: string, id: string | number,
options: WorkspaceQueryBuilderOptions, options: WorkspaceQueryBuilderOptions,
): string { ): string {
return this.findDuplicatesQueryFactory.buildQueryForExistingRecord( return this.findDuplicatesQueryFactory.buildQueryForExistingRecord(

View File

@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -14,6 +15,7 @@ describe('QueryRunnerArgsFactory', () => {
const options = { const options = {
fieldMetadataCollection: [ fieldMetadataCollection: [
{ name: 'position', type: FieldMetadataType.POSITION }, { name: 'position', type: FieldMetadataType.POSITION },
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[], ] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' }, objectMetadataItem: { isCustom: true, nameSingular: 'test' },
} as WorkspaceQueryRunnerOptions; } as WorkspaceQueryRunnerOptions;
@ -45,18 +47,92 @@ describe('QueryRunnerArgsFactory', () => {
const args = { const args = {
data: [], data: [],
}; };
const result = await factory.create(args, options); const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
);
expect(result).toEqual(args); expect(result).toEqual(args);
}); });
it('should override args when of type array', async () => { it('createMany type should override data position and number', async () => {
const args = { data: [{ id: 1 }, { position: 'last' }] }; const args = {
id: 'uuid',
data: [{ position: 'last', testNumber: '1' }],
};
const result = await factory.create(args, options); const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
);
expect(result).toEqual({ expect(result).toEqual({
data: [{ id: 1 }, { position: 2 }], id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
});
});
it('findMany type should override data position and number', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
};
const result = await factory.create(
args,
options,
ResolverArgsType.FindMany,
);
expect(result).toEqual({
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
});
});
it('findOne type should override number in filter', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
};
const result = await factory.create(
args,
options,
ResolverArgsType.FindOne,
);
expect(result).toEqual({
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
});
});
it('findDuplicates type should override number in data and id', async () => {
const optionsDuplicate = {
fieldMetadataCollection: [
{ name: 'id', type: FieldMetadataType.NUMBER },
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
} as WorkspaceQueryRunnerOptions;
const args = {
id: '123',
data: { testNumber: '1', otherField: 'test' },
};
const result = await factory.create(
args,
optionsDuplicate,
ResolverArgsType.FindDuplicates,
);
expect(result).toEqual({
id: 123,
data: { testNumber: 1, otherField: 'test' },
}); });
}); });
}); });

View File

@ -2,6 +2,15 @@ import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
CreateManyResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
ResolverArgs,
ResolverArgsType,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -12,8 +21,9 @@ export class QueryRunnerArgsFactory {
constructor(private readonly recordPositionFactory: RecordPositionFactory) {} constructor(private readonly recordPositionFactory: RecordPositionFactory) {}
async create( async create(
args: Record<string, any>, args: ResolverArgs,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
resolverArgsType: ResolverArgsType,
) { ) {
const fieldMetadataCollection = options.fieldMetadataCollection; const fieldMetadataCollection = options.fieldMetadataCollection;
@ -24,21 +34,62 @@ export class QueryRunnerArgsFactory {
]), ]),
); );
switch (resolverArgsType) {
case ResolverArgsType.CreateMany:
return { return {
...args,
data: await Promise.all( data: await Promise.all(
args.data.map((arg) => (args as CreateManyResolverArgs).data.map((arg) =>
this.overrideArgByFieldMetadata(arg, options, fieldMetadataMap), this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap),
), ),
), ),
} satisfies CreateManyResolverArgs;
case ResolverArgsType.FindOne:
return {
...args,
filter: await this.overrideFilterByFieldMetadata(
(args as FindOneResolverArgs).filter,
fieldMetadataMap,
),
}; };
case ResolverArgsType.FindMany:
return {
...args,
filter: await this.overrideFilterByFieldMetadata(
(args as FindManyResolverArgs).filter,
fieldMetadataMap,
),
};
case ResolverArgsType.FindDuplicates:
return {
...args,
id: await this.overrideValueByFieldMetadata(
'id',
(args as FindDuplicatesResolverArgs).id,
fieldMetadataMap,
),
data: await this.overrideDataByFieldMetadata(
(args as FindDuplicatesResolverArgs).data,
options,
fieldMetadataMap,
),
};
default:
return args;
}
} }
private async overrideArgByFieldMetadata( private async overrideDataByFieldMetadata(
arg: Record<string, any>, data: Record<string, any> | undefined,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
fieldMetadataMap: Map<string, FieldMetadataInterface>, fieldMetadataMap: Map<string, FieldMetadataInterface>,
) { ) {
const createArgPromiseByArgKey = Object.entries(arg).map( if (!data) {
return;
}
const createArgPromiseByArgKey = Object.entries(data).map(
async ([key, value]) => { async ([key, value]) => {
const fieldMetadata = fieldMetadataMap.get(key); const fieldMetadata = fieldMetadataMap.get(key);
@ -59,6 +110,8 @@ export class QueryRunnerArgsFactory {
options.workspaceId, options.workspaceId,
), ),
]; ];
case FieldMetadataType.NUMBER:
return [key, await Promise.resolve(Number(value))];
default: default:
return [key, await Promise.resolve(value)]; return [key, await Promise.resolve(value)];
} }
@ -69,4 +122,57 @@ export class QueryRunnerArgsFactory {
return Object.fromEntries(newArgEntries); return Object.fromEntries(newArgEntries);
} }
private overrideFilterByFieldMetadata(
filter: RecordFilter | undefined,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
if (!filter) {
return;
}
const createArgPromiseByArgKey = Object.entries(filter).map(
([key, value]) => {
const fieldMetadata = fieldMetadataMap.get(key);
if (!fieldMetadata) {
return [key, value];
}
const createFilterByKey = Object.entries(value).map(
([filterKey, filterValue]) => {
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER:
return [filterKey, Number(filterValue)];
default:
return [filterKey, filterValue];
}
},
);
return [key, Object.fromEntries(createFilterByKey)];
},
);
return Object.fromEntries(createArgPromiseByArgKey);
}
private async overrideValueByFieldMetadata(
key: string,
value: any,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
const fieldMetadata = fieldMetadataMap.get(key);
if (!fieldMetadata) {
return value;
}
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER:
return Number(value);
default:
return value;
}
}
} }

View File

@ -0,0 +1,12 @@
import { BadRequestException } from '@nestjs/common';
export const assertIsValidUuid = (value: string) => {
const isValid =
/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value,
);
if (!isValid) {
throw new BadRequestException(`Value "${value}" is not a valid UUID`);
}
};

View File

@ -22,6 +22,7 @@ import {
FindDuplicatesResolverArgs, FindDuplicatesResolverArgs,
FindManyResolverArgs, FindManyResolverArgs,
FindOneResolverArgs, FindOneResolverArgs,
ResolverArgsType,
UpdateManyResolverArgs, UpdateManyResolverArgs,
UpdateOneResolverArgs, UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@ -48,6 +49,7 @@ import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-r
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory'; import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assertIsValidUuid.util';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
import { import {
@ -83,9 +85,15 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, userId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const start = performance.now(); const start = performance.now();
const query = await this.workspaceQueryBuilderFactory.findMany( const computedArgs = (await this.queryRunnerArgsFactory.create(
args, args,
options, options,
ResolverArgsType.FindMany,
)) as FindManyResolverArgs<Filter, OrderBy>;
const query = await this.workspaceQueryBuilderFactory.findMany(
computedArgs,
options,
); );
await this.workspacePreQueryHookService.executePreHooks( await this.workspacePreQueryHookService.executePreHooks(
@ -123,9 +131,16 @@ export class WorkspaceQueryRunnerService {
throw new BadRequestException('Missing filter argument'); throw new BadRequestException('Missing filter argument');
} }
const { workspaceId, userId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.findOne(
const computedArgs = (await this.queryRunnerArgsFactory.create(
args, args,
options, options,
ResolverArgsType.FindOne,
)) as FindOneResolverArgs<Filter>;
const query = await this.workspaceQueryBuilderFactory.findOne(
computedArgs,
options,
); );
await this.workspacePreQueryHookService.executePreHooks( await this.workspacePreQueryHookService.executePreHooks(
@ -164,12 +179,18 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, userId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
options,
ResolverArgsType.FindDuplicates,
)) as FindDuplicatesResolverArgs<TRecord>;
let existingRecord: Record<string, unknown> | undefined; let existingRecord: Record<string, unknown> | undefined;
if (args.id) { if (computedArgs.id) {
const existingRecordQuery = const existingRecordQuery =
this.workspaceQueryBuilderFactory.findDuplicatesExistingRecord( this.workspaceQueryBuilderFactory.findDuplicatesExistingRecord(
args.id, computedArgs.id,
options, options,
); );
@ -192,7 +213,7 @@ export class WorkspaceQueryRunnerService {
} }
const query = await this.workspaceQueryBuilderFactory.findDuplicates( const query = await this.workspaceQueryBuilderFactory.findDuplicates(
args, computedArgs,
options, options,
existingRecord, existingRecord,
); );
@ -202,7 +223,7 @@ export class WorkspaceQueryRunnerService {
workspaceId, workspaceId,
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
'findDuplicates', 'findDuplicates',
args, computedArgs,
); );
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
@ -222,10 +243,17 @@ export class WorkspaceQueryRunnerService {
assertMutationNotOnRemoteObject(objectMetadataItem); assertMutationNotOnRemoteObject(objectMetadataItem);
const computedArgs = await this.queryRunnerArgsFactory.create( args.data.forEach((record) => {
if (record.id) {
assertIsValidUuid(record.id);
}
});
const computedArgs = (await this.queryRunnerArgsFactory.create(
args, args,
options, options,
); ResolverArgsType.CreateMany,
)) as CreateManyResolverArgs<Record>;
await this.workspacePreQueryHookService.executePreHooks( await this.workspacePreQueryHookService.executePreHooks(
userId, userId,
@ -288,6 +316,7 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, userId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem); assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.id);
const existingRecord = await this.findOne( const existingRecord = await this.findOne(
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs, { filter: { id: { eq: args.id } } } as FindOneResolverArgs,
@ -337,6 +366,7 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem); assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.data.id);
const maximumRecordAffected = this.environmentService.get( const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_RECORD_AFFECTED', 'MUTATION_MAXIMUM_RECORD_AFFECTED',

View File

@ -10,6 +10,19 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>; export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
export enum ResolverArgsType {
FindMany = 'FindMany',
FindOne = 'FindOne',
FindDuplicates = 'FindDuplicates',
CreateOne = 'CreateOne',
CreateMany = 'CreateMany',
UpdateOne = 'UpdateOne',
UpdateMany = 'UpdateMany',
DeleteOne = 'DeleteOne',
DeleteMany = 'DeleteMany',
ExecuteQuickActionOnOne = 'ExecuteQuickActionOnOne',
}
export interface FindManyResolverArgs< export interface FindManyResolverArgs<
Filter extends RecordFilter = RecordFilter, Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy, OrderBy extends RecordOrderBy = RecordOrderBy,

View File

@ -32,27 +32,7 @@ export class ArgsFactory {
// Argument is a scalar type // Argument is a scalar type
if (arg.type) { if (arg.type) {
const fieldType = this.typeMapperService.mapToScalarType( const gqlType = this.typeMapperService.mapToGqlType(arg.type, {
arg.type,
options.dateScalarMode,
options.numberScalarMode,
);
if (!fieldType) {
this.logger.error(
`Could not find a GraphQL type for ${arg.type.toString()}`,
{
arg,
options,
},
);
throw new Error(
`Could not find a GraphQL type for ${arg.type.toString()}`,
);
}
const gqlType = this.typeMapperService.mapToGqlType(fieldType, {
defaultValue: arg.defaultValue, defaultValue: arg.defaultValue,
nullable: arg.isNullable, nullable: arg.isNullable,
isArray: arg.isArray, isArray: arg.isArray,

View File

@ -102,6 +102,8 @@ export class InputTypeDefinitionFactory {
? fieldMetadata.type.toString() ? fieldMetadata.type.toString()
: fieldMetadata.id; : fieldMetadata.id;
const isIdField = fieldMetadata.name === 'id';
const type = this.inputTypeFactory.create( const type = this.inputTypeFactory.create(
target, target,
fieldMetadata.type, fieldMetadata.type,
@ -111,6 +113,8 @@ export class InputTypeDefinitionFactory {
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue, defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField,
}, },
); );

View File

@ -41,8 +41,8 @@ export class InputTypeFactory {
case InputTypeDefinitionKind.Update: case InputTypeDefinitionKind.Update:
inputType = this.typeMapperService.mapToScalarType( inputType = this.typeMapperService.mapToScalarType(
type, type,
buildOptions.dateScalarMode, typeOptions.settings,
buildOptions.numberScalarMode, typeOptions.isIdField,
); );
break; break;
/** /**
@ -54,8 +54,8 @@ export class InputTypeFactory {
} else { } else {
inputType = this.typeMapperService.mapToFilterType( inputType = this.typeMapperService.mapToFilterType(
type, type,
buildOptions.dateScalarMode, typeOptions.settings,
buildOptions.numberScalarMode, typeOptions.isIdField,
); );
} }

View File

@ -69,6 +69,9 @@ export class ObjectTypeDefinitionFactory {
{ {
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
// Scalar type is already defined in the entity itself.
isIdField: false,
}, },
); );

View File

@ -32,8 +32,8 @@ export class OutputTypeFactory {
let gqlType: GraphQLOutputType | undefined = let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType( this.typeMapperService.mapToScalarType(
type, type,
buildOtions.dateScalarMode, typeOptions.settings,
buildOtions.numberScalarMode, typeOptions.isIdField,
); );
gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind); gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind);

View File

@ -0,0 +1,17 @@
import { GraphQLID, GraphQLInputObjectType, GraphQLList } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const IDFilterType = new GraphQLInputObjectType({
name: 'IDFilter',
fields: {
eq: { type: GraphQLID },
gt: { type: GraphQLID },
gte: { type: GraphQLID },
in: { type: new GraphQLList(GraphQLID) },
lt: { type: GraphQLID },
lte: { type: GraphQLID },
neq: { type: GraphQLID },
is: { type: FilterIs },
},
});

View File

@ -1,9 +1,10 @@
import { GraphQLScalarType } from 'graphql';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface ArgMetadata<T = any> { export interface ArgMetadata<T = any> {
kind?: InputTypeDefinitionKind; kind?: InputTypeDefinitionKind;
type?: FieldMetadataType; type?: GraphQLScalarType;
isNullable?: boolean; isNullable?: boolean;
isArray?: boolean; isArray?: boolean;
defaultValue?: T; defaultValue?: T;

View File

@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { GraphQLISODateTime, GraphQLTimestamp } from '@nestjs/graphql'; import { GraphQLISODateTime } from '@nestjs/graphql';
import { import {
GraphQLBoolean, GraphQLBoolean,
GraphQLEnumType, GraphQLEnumType,
GraphQLFloat, GraphQLFloat,
GraphQLID,
GraphQLInputObjectType, GraphQLInputObjectType,
GraphQLInputType, GraphQLInputType,
GraphQLInt, GraphQLInt,
@ -15,22 +16,17 @@ import {
GraphQLType, GraphQLType,
} from 'graphql'; } from 'graphql';
import { import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
DateScalarMode,
NumberScalarMode,
} from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { import {
UUIDFilterType,
StringFilterType, StringFilterType,
DatetimeFilterType,
DateFilterType, DateFilterType,
FloatFilterType, FloatFilterType,
IntFilterType,
BooleanFilterType, BooleanFilterType,
BigFloatFilterType, BigFloatFilterType,
RawJsonFilterType, RawJsonFilterType,
IntFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input'; } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum'; import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
import { import {
@ -39,38 +35,46 @@ import {
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar'; import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar';
import { JsonScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar'; import { JsonScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar';
import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type';
export interface TypeOptions<T = any> { export interface TypeOptions<T = any> {
nullable?: boolean; nullable?: boolean;
isArray?: boolean; isArray?: boolean;
arrayDepth?: number; arrayDepth?: number;
defaultValue?: T; defaultValue?: T;
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>;
isIdField?: boolean;
} }
@Injectable() @Injectable()
export class TypeMapperService { export class TypeMapperService {
mapToScalarType( mapToScalarType(
fieldMetadataType: FieldMetadataType, fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate', settings?: FieldMetadataSettings<FieldMetadataType | 'default'>,
numberScalarMode: NumberScalarMode = 'float', isIdField?: boolean,
): GraphQLScalarType | undefined { ): GraphQLScalarType | undefined {
const dateScalar = if (isIdField || settings?.isForeignKey) {
dateScalarMode === 'timestamp' ? GraphQLTimestamp : GraphQLISODateTime; return GraphQLID;
}
const numberScalar = const numberScalar =
numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt; fieldMetadataType === FieldMetadataType.NUMBER &&
(settings as FieldMetadataSettings<FieldMetadataType.NUMBER>)
?.precision === 0
? GraphQLInt
: GraphQLFloat;
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([ const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
[FieldMetadataType.UUID, UUIDScalarType], [FieldMetadataType.UUID, UUIDScalarType],
[FieldMetadataType.TEXT, GraphQLString], [FieldMetadataType.TEXT, GraphQLString],
[FieldMetadataType.PHONE, GraphQLString], [FieldMetadataType.PHONE, GraphQLString],
[FieldMetadataType.EMAIL, GraphQLString], [FieldMetadataType.EMAIL, GraphQLString],
[FieldMetadataType.DATE_TIME, dateScalar], [FieldMetadataType.DATE_TIME, GraphQLISODateTime],
[FieldMetadataType.DATE, dateScalar], [FieldMetadataType.DATE, GraphQLISODateTime],
[FieldMetadataType.BOOLEAN, GraphQLBoolean], [FieldMetadataType.BOOLEAN, GraphQLBoolean],
[FieldMetadataType.NUMBER, numberScalar], [FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatScalarType], [FieldMetadataType.NUMERIC, BigFloatScalarType],
[FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, UUIDScalarType],
[FieldMetadataType.POSITION, PositionScalarType], [FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.RAW_JSON, JsonScalarType], [FieldMetadataType.RAW_JSON, JsonScalarType],
]); ]);
@ -80,29 +84,34 @@ export class TypeMapperService {
mapToFilterType( mapToFilterType(
fieldMetadataType: FieldMetadataType, fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate', settings?: FieldMetadataSettings<FieldMetadataType | 'default'>,
numberScalarMode: NumberScalarMode = 'float', isIdField?: boolean,
): GraphQLInputObjectType | GraphQLScalarType | undefined { ): GraphQLInputObjectType | GraphQLScalarType | undefined {
const dateFilter = if (isIdField || settings?.isForeignKey) {
dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType; return IDFilterType;
}
const numberScalar = const numberScalar =
numberScalarMode === 'float' ? FloatFilterType : IntFilterType; fieldMetadataType === FieldMetadataType.NUMBER &&
(settings as FieldMetadataSettings<FieldMetadataType.NUMBER>)
?.precision === 0
? IntFilterType
: FloatFilterType;
const typeFilterMapping = new Map< const typeFilterMapping = new Map<
FieldMetadataType, FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType GraphQLInputObjectType | GraphQLScalarType
>([ >([
[FieldMetadataType.UUID, UUIDFilterType], [FieldMetadataType.UUID, IDFilterType],
[FieldMetadataType.TEXT, StringFilterType], [FieldMetadataType.TEXT, StringFilterType],
[FieldMetadataType.PHONE, StringFilterType], [FieldMetadataType.PHONE, StringFilterType],
[FieldMetadataType.EMAIL, StringFilterType], [FieldMetadataType.EMAIL, StringFilterType],
[FieldMetadataType.DATE_TIME, dateFilter], [FieldMetadataType.DATE_TIME, DateFilterType],
[FieldMetadataType.DATE, DateFilterType], [FieldMetadataType.DATE, DateFilterType],
[FieldMetadataType.BOOLEAN, BooleanFilterType], [FieldMetadataType.BOOLEAN, BooleanFilterType],
[FieldMetadataType.NUMBER, numberScalar], [FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatFilterType], [FieldMetadataType.NUMERIC, BigFloatFilterType],
[FieldMetadataType.PROBABILITY, FloatFilterType], [FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType], [FieldMetadataType.POSITION, FloatFilterType],
[FieldMetadataType.RAW_JSON, RawJsonFilterType], [FieldMetadataType.RAW_JSON, RawJsonFilterType],
]); ]);

View File

@ -1,18 +1,20 @@
import { GraphQLID, GraphQLInt, GraphQLString } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
describe('getResolverArgs', () => { describe('getResolverArgs', () => {
const expectedOutputs = { const expectedOutputs = {
findMany: { findMany: {
first: { type: FieldMetadataType.NUMBER, isNullable: true }, first: { type: GraphQLInt, isNullable: true },
last: { type: FieldMetadataType.NUMBER, isNullable: true }, last: { type: GraphQLInt, isNullable: true },
before: { type: FieldMetadataType.TEXT, isNullable: true }, before: { type: GraphQLString, isNullable: true },
after: { type: FieldMetadataType.TEXT, isNullable: true }, after: { type: GraphQLString, isNullable: true },
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true }, filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true },
orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true }, orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true },
limit: { type: GraphQLInt, isNullable: true },
}, },
findOne: { findOne: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false }, filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
@ -28,14 +30,14 @@ describe('getResolverArgs', () => {
data: { kind: InputTypeDefinitionKind.Create, isNullable: false }, data: { kind: InputTypeDefinitionKind.Create, isNullable: false },
}, },
updateOne: { updateOne: {
id: { type: FieldMetadataType.UUID, isNullable: false }, id: { type: GraphQLID, isNullable: false },
data: { kind: InputTypeDefinitionKind.Update, isNullable: false }, data: { kind: InputTypeDefinitionKind.Update, isNullable: false },
}, },
deleteOne: { deleteOne: {
id: { type: FieldMetadataType.UUID, isNullable: false }, id: { type: GraphQLID, isNullable: false },
}, },
executeQuickActionOnOne: { executeQuickActionOnOne: {
id: { type: FieldMetadataType.UUID, isNullable: false }, id: { type: GraphQLID, isNullable: false },
}, },
}; };

View File

@ -1,7 +1,8 @@
import { GraphQLString, GraphQLInt, GraphQLID } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface'; import { ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
export const getResolverArgs = ( export const getResolverArgs = (
@ -11,19 +12,23 @@ export const getResolverArgs = (
case 'findMany': case 'findMany':
return { return {
first: { first: {
type: FieldMetadataType.NUMBER, type: GraphQLInt,
isNullable: true, isNullable: true,
}, },
last: { last: {
type: FieldMetadataType.NUMBER, type: GraphQLInt,
isNullable: true, isNullable: true,
}, },
before: { before: {
type: FieldMetadataType.TEXT, type: GraphQLString,
isNullable: true, isNullable: true,
}, },
after: { after: {
type: FieldMetadataType.TEXT, type: GraphQLString,
isNullable: true,
},
limit: {
type: GraphQLInt,
isNullable: true, isNullable: true,
}, },
filter: { filter: {
@ -61,7 +66,7 @@ export const getResolverArgs = (
case 'updateOne': case 'updateOne':
return { return {
id: { id: {
type: FieldMetadataType.UUID, type: GraphQLID,
isNullable: false, isNullable: false,
}, },
data: { data: {
@ -72,7 +77,7 @@ export const getResolverArgs = (
case 'findDuplicates': case 'findDuplicates':
return { return {
id: { id: {
type: FieldMetadataType.UUID, type: GraphQLID,
isNullable: true, isNullable: true,
}, },
data: { data: {
@ -83,14 +88,14 @@ export const getResolverArgs = (
case 'deleteOne': case 'deleteOne':
return { return {
id: { id: {
type: FieldMetadataType.UUID, type: GraphQLID,
isNullable: false, isNullable: false,
}, },
}; };
case 'executeQuickActionOnOne': case 'executeQuickActionOnOne':
return { return {
id: { id: {
type: FieldMetadataType.UUID, type: GraphQLID,
isNullable: false, isNullable: false,
}, },
}; };

View File

@ -16,7 +16,7 @@ export class FindManyQueryFactory {
$filter: ${objectNameSingular}FilterInput, $filter: ${objectNameSingular}FilterInput,
$orderBy: ${objectNameSingular}OrderByInput, $orderBy: ${objectNameSingular}OrderByInput,
$lastCursor: String, $lastCursor: String,
$limit: Float = 60 $limit: Int = 60
) { ) {
${objectNamePlural}( ${objectNamePlural}(
filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor

View File

@ -22,7 +22,6 @@ export enum FeatureFlagKeys {
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED', IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IsMultiSelectEnabled = 'IS_MULTI_SELECT_ENABLED', IsMultiSelectEnabled = 'IS_MULTI_SELECT_ENABLED',
IsRelationForRemoteObjectsEnabled = 'IS_RELATION_FOR_REMOTE_OBJECTS_ENABLED',
} }
@Entity({ name: 'featureFlag', schema: 'core' }) @Entity({ name: 'featureFlag', schema: 'core' })

View File

@ -26,6 +26,7 @@ import {
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto'; import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -120,6 +121,10 @@ export class FieldMetadataDTO<
@Field(() => GraphQLJSON, { nullable: true }) @Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions<T>; options?: FieldMetadataOptions<T>;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
settings?: FieldMetadataSettings<T>;
@HideField() @HideField()
workspaceId: string; workspaceId: string;

View File

@ -14,6 +14,7 @@ import {
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -87,6 +88,9 @@ export class FieldMetadataEntity<
@Column('jsonb', { nullable: true }) @Column('jsonb', { nullable: true })
options: FieldMetadataOptions<T>; options: FieldMetadataOptions<T>;
@Column('jsonb', { nullable: true })
settings?: FieldMetadataSettings<T>;
@Column({ default: false }) @Column({ default: false })
isCustom: boolean; isCustom: boolean;

View File

@ -17,7 +17,6 @@ import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-m
import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { FieldMetadataService } from './field-metadata.service'; import { FieldMetadataService } from './field-metadata.service';
import { FieldMetadataEntity } from './field-metadata.entity'; import { FieldMetadataEntity } from './field-metadata.entity';
@ -29,10 +28,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
imports: [ imports: [
NestjsQueryGraphQLModule.forFeature({ NestjsQueryGraphQLModule.forFeature({
imports: [ imports: [
NestjsQueryTypeOrmModule.forFeature( NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
[FieldMetadataEntity, RelationMetadataEntity],
'metadata',
),
WorkspaceMigrationModule, WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule, WorkspaceMigrationRunnerModule,
ObjectMetadataModule, ObjectMetadataModule,

View File

@ -58,8 +58,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly metadataDataSource: DataSource, private readonly metadataDataSource: DataSource,
@InjectRepository(FieldMetadataEntity, 'metadata') @InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,

View File

@ -0,0 +1,24 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
type FieldMetadataDefaultSettings = {
isForeignKey?: boolean;
};
type FieldMetadataNumberSettings = {
precision: number;
};
type FieldMetadataSettingsMapping = {
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
};
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =
T extends keyof FieldMetadataSettingsMapping
? FieldMetadataSettingsMapping[T] & FieldMetadataDefaultSettings
: T extends 'default'
? FieldMetadataDefaultSettings
: never;
export type FieldMetadataSettings<
T extends FieldMetadataType | 'default' = 'default',
> = SettingsByFieldMetadata<T>;

View File

@ -1,5 +1,6 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -13,6 +14,7 @@ export interface FieldMetadataInterface<
label: string; label: string;
defaultValue?: FieldMetadataDefaultValue<T>; defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>; options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;
objectMetadataId: string; objectMetadataId: string;
workspaceId?: string; workspaceId?: string;
description?: string; description?: string;

View File

@ -8,9 +8,13 @@ import {
IsString, IsString,
IsUUID, IsUUID,
} from 'class-validator'; } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook'; import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@InputType() @InputType()
@BeforeCreateOne(BeforeCreateOneObject) @BeforeCreateOne(BeforeCreateOneObject)
@ -70,5 +74,11 @@ export class CreateObjectInput {
@IsOptional() @IsOptional()
@Field({ nullable: true }) @Field({ nullable: true })
remoteTablePrimaryKeyColumnType?: string; primaryKeyColumnType?: string;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
primaryKeyFieldMetadataSettings?: FieldMetadataSettings<
FieldMetadataType | 'default'
>;
} }

View File

@ -16,6 +16,8 @@ import {
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core'; import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { import {
@ -54,12 +56,10 @@ import {
import { createWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-custom-object.util'; import { createWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-custom-object.util';
import { createWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object.util'; import { createWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
FeatureFlagEntity,
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { validateObjectMetadataInput } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { validateObjectMetadataInput } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util';
import { ObjectMetadataEntity } from './object-metadata.entity'; import { ObjectMetadataEntity } from './object-metadata.entity';
@ -469,35 +469,38 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceDataSource: DataSource | undefined, workspaceDataSource: DataSource | undefined,
isRemoteObject = false, isRemoteObject = false,
) { ) {
const isRelationEnabledForRemoteObjects =
await this.isRelationEnabledForRemoteObjects(
objectMetadataInput.workspaceId,
);
if (isRemoteObject && !isRelationEnabledForRemoteObjects) {
return;
}
const { timelineActivityObjectMetadata } = const { timelineActivityObjectMetadata } =
await this.createTimelineActivityRelation( await this.createTimelineActivityRelation(
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
createdObjectMetadata, createdObjectMetadata,
mapUdtNameToFieldType(
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
),
objectMetadataInput.primaryKeyFieldMetadataSettings,
); );
const { activityTargetObjectMetadata } = const { activityTargetObjectMetadata } =
await this.createActivityTargetRelation( await this.createActivityTargetRelation(
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
createdObjectMetadata, createdObjectMetadata,
mapUdtNameToFieldType(
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
),
objectMetadataInput.primaryKeyFieldMetadataSettings,
); );
const { favoriteObjectMetadata } = await this.createFavoriteRelation( const { favoriteObjectMetadata } = await this.createFavoriteRelation(
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
createdObjectMetadata, createdObjectMetadata,
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
objectMetadataInput.primaryKeyFieldMetadataSettings,
); );
const { attachmentObjectMetadata } = await this.createAttachmentRelation( const { attachmentObjectMetadata } = await this.createAttachmentRelation(
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
createdObjectMetadata, createdObjectMetadata,
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
objectMetadataInput.primaryKeyFieldMetadataSettings,
); );
return this.workspaceMigrationService.createCustomMigration( return this.workspaceMigrationService.createCustomMigration(
@ -511,7 +514,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
timelineActivityObjectMetadata, timelineActivityObjectMetadata,
favoriteObjectMetadata, favoriteObjectMetadata,
lastDataSourceMetadata.schema, lastDataSourceMetadata.schema,
objectMetadataInput.remoteTablePrimaryKeyColumnType ?? 'uuid', objectMetadataInput.primaryKeyColumnType ?? 'uuid',
workspaceDataSource, workspaceDataSource,
) )
: createWorkspaceMigrationsForCustomObject( : createWorkspaceMigrationsForCustomObject(
@ -527,6 +530,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createActivityTargetRelation( private async createActivityTargetRelation(
workspaceId: string, workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity, createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) { ) {
const activityTargetObjectMetadata = const activityTargetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -577,7 +584,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: false, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`, description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`,
@ -585,6 +592,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true, isNullable: true,
isSystem: true, isSystem: true,
defaultValue: undefined, defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
}, },
]); ]);
@ -622,6 +630,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createAttachmentRelation( private async createAttachmentRelation(
workspaceId: string, workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity, createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) { ) {
const attachmentObjectMetadata = const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -672,7 +684,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: false, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`, description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
@ -680,6 +692,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true, isNullable: true,
isSystem: true, isSystem: true,
defaultValue: undefined, defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
}, },
]); ]);
@ -715,6 +728,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createTimelineActivityRelation( private async createTimelineActivityRelation(
workspaceId: string, workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity, createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) { ) {
const timelineActivityObjectMetadata = const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -765,7 +782,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: false, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`, description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`,
@ -773,6 +790,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true, isNullable: true,
isSystem: true, isSystem: true,
defaultValue: undefined, defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
}, },
]); ]);
@ -810,6 +828,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createFavoriteRelation( private async createFavoriteRelation(
workspaceId: string, workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity, createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) { ) {
const favoriteObjectMetadata = const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -861,7 +883,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: false, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`, description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
@ -869,6 +891,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true, isNullable: true,
isSystem: true, isSystem: true,
defaultValue: undefined, defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
}, },
]); ]);
@ -900,14 +923,4 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
return { favoriteObjectMetadata }; return { favoriteObjectMetadata };
} }
private async isRelationEnabledForRemoteObjects(workspaceId: string) {
const featureFlag = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKeys.IsRelationForRemoteObjectsEnabled,
value: true,
});
return featureFlag && featureFlag.value;
}
} }

View File

@ -1,9 +1,11 @@
import { BadRequestException } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
export const assertMutationNotOnRemoteObject = ( export const assertMutationNotOnRemoteObject = (
objectMetadataItem: ObjectMetadataInterface, objectMetadataItem: ObjectMetadataInterface,
) => { ) => {
if (objectMetadataItem.isRemote) { if (objectMetadataItem.isRemote) {
throw new Error('Remote objects are read-only'); throw new BadRequestException('Remote objects are read-only');
} }
}; };

View File

@ -54,7 +54,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
eventObjectMetadata: ObjectMetadataEntity, eventObjectMetadata: ObjectMetadataEntity,
favoriteObjectMetadata: ObjectMetadataEntity, favoriteObjectMetadata: ObjectMetadataEntity,
schema: string, schema: string,
remoteTablePrimaryKeyColumnType: string, primaryKeyColumnType: string,
workspaceDataSource: DataSource | undefined, workspaceDataSource: DataSource | undefined,
): Promise<WorkspaceMigrationTableAction[]> => { ): Promise<WorkspaceMigrationTableAction[]> => {
const createdObjectName = createdObjectMetadata.nameSingular; const createdObjectName = createdObjectMetadata.nameSingular;
@ -69,7 +69,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, { columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true, isForeignKey: true,
}), }),
columnType: remoteTablePrimaryKeyColumnType, columnType: primaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -99,7 +99,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, { columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true, isForeignKey: true,
}), }),
columnType: remoteTablePrimaryKeyColumnType, columnType: primaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -129,7 +129,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, { columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true, isForeignKey: true,
}), }),
columnType: remoteTablePrimaryKeyColumnType, columnType: primaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],
@ -159,7 +159,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, { columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true, isForeignKey: true,
}), }),
columnType: remoteTablePrimaryKeyColumnType, columnType: primaryKeyColumnType,
isNullable: true, isNullable: true,
} satisfies WorkspaceMigrationColumnCreate, } satisfies WorkspaceMigrationColumnCreate,
], ],

View File

@ -1,5 +1,7 @@
import { Repository } from 'typeorm/repository/Repository'; import { Repository } from 'typeorm/repository/Repository';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { decryptText } from 'src/engine/core-modules/auth/auth.util'; import { decryptText } from 'src/engine/core-modules/auth/auth.util';
import { import {
FeatureFlagEntity, FeatureFlagEntity,
@ -42,11 +44,32 @@ export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => {
case 'timestamp': case 'timestamp':
case 'timestamptz': case 'timestamptz':
return FieldMetadataType.DATE_TIME; return FieldMetadataType.DATE_TIME;
case 'integer':
case 'int2':
case 'int4':
case 'int8':
return FieldMetadataType.NUMBER;
default: default:
return FieldMetadataType.TEXT; return FieldMetadataType.TEXT;
} }
}; };
export const mapUdtNameToSettings = (
udtName: string,
): FieldMetadataSettings<FieldMetadataType> | undefined => {
switch (udtName) {
case 'integer':
case 'int2':
case 'int4':
case 'int8':
return {
precision: 0,
} satisfies FieldMetadataSettings<FieldMetadataType.NUMBER>;
default:
return undefined;
}
};
export const isPostgreSQLIntegrationEnabled = async ( export const isPostgreSQLIntegrationEnabled = async (
featureFlagRepository: Repository<FeatureFlagEntity>, featureFlagRepository: Repository<FeatureFlagEntity>,
workspaceId: string, workspaceId: string,

View File

@ -12,6 +12,7 @@ import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/rem
import { import {
isPostgreSQLIntegrationEnabled, isPostgreSQLIntegrationEnabled,
mapUdtNameToFieldType, mapUdtNameToFieldType,
mapUdtNameToSettings,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util'; } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util';
import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input'; import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@ -429,7 +430,11 @@ export class RemoteTableService {
workspaceId: workspaceId, workspaceId: workspaceId,
icon: 'IconPlug', icon: 'IconPlug',
isRemote: true, isRemote: true,
remoteTablePrimaryKeyColumnType: remoteTableIdColumn.udtName, primaryKeyColumnType: remoteTableIdColumn.udtName,
// TODO: function should work for other types than Postgres
primaryKeyFieldMetadataSettings: mapUdtNameToSettings(
remoteTableIdColumn.udtName,
),
} satisfies CreateObjectInput); } satisfies CreateObjectInput);
for (const column of remoteTableColumns) { for (const column of remoteTableColumns) {
@ -444,6 +449,8 @@ export class RemoteTableService {
isRemoteCreation: true, isRemoteCreation: true,
isNullable: true, isNullable: true,
icon: 'IconPlug', icon: 'IconPlug',
// TODO: function should work for other types than Postgres
settings: mapUdtNameToSettings(column.udtName),
} satisfies CreateFieldInput); } satisfies CreateFieldInput);
if (column.columnName === 'id') { if (column.columnName === 'id') {

View File

@ -59,7 +59,6 @@ export class AddStandardIdCommand extends CommandRunner {
IS_AIRTABLE_INTEGRATION_ENABLED: true, IS_AIRTABLE_INTEGRATION_ENABLED: true,
IS_POSTGRESQL_INTEGRATION_ENABLED: true, IS_POSTGRESQL_INTEGRATION_ENABLED: true,
IS_MULTI_SELECT_ENABLED: false, IS_MULTI_SELECT_ENABLED: false,
IS_RELATION_FOR_REMOTE_OBJECTS_ENABLED: false,
}, },
); );
const standardFieldMetadataCollection = this.standardFieldFactory.create( const standardFieldMetadataCollection = this.standardFieldFactory.create(
@ -75,7 +74,6 @@ export class AddStandardIdCommand extends CommandRunner {
IS_AIRTABLE_INTEGRATION_ENABLED: true, IS_AIRTABLE_INTEGRATION_ENABLED: true,
IS_POSTGRESQL_INTEGRATION_ENABLED: true, IS_POSTGRESQL_INTEGRATION_ENABLED: true,
IS_MULTI_SELECT_ENABLED: false, IS_MULTI_SELECT_ENABLED: false,
IS_RELATION_FOR_REMOTE_OBJECTS_ENABLED: false,
}, },
); );

View File

@ -48,6 +48,7 @@ export function FieldMetadata<T extends FieldMetadataType>(
description: `${restParams.description} id foreign key`, description: `${restParams.description} id foreign key`,
defaultValue: null, defaultValue: null,
options: undefined, options: undefined,
settings: undefined,
}, },
joinColumn, joinColumn,
isNullable, isNullable,

View File

@ -1,6 +1,7 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { GateDecoratorParams } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/gate-decorator.interface'; import { GateDecoratorParams } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/gate-decorator.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -16,12 +17,13 @@ export interface FieldMetadataDecoratorParams<
defaultValue?: FieldMetadataDefaultValue<T>; defaultValue?: FieldMetadataDefaultValue<T>;
joinColumn?: string; joinColumn?: string;
options?: FieldMetadataOptions<T>; options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;
} }
export interface ReflectFieldMetadata { export interface ReflectFieldMetadata {
[key: string]: Omit< [key: string]: Omit<
FieldMetadataDecoratorParams<'default'>, FieldMetadataDecoratorParams<'default'>,
'defaultValue' | 'type' | 'options' 'defaultValue' | 'type' | 'options' | 'settings'
> & { > & {
name: string; name: string;
type: FieldMetadataType; type: FieldMetadataType;
@ -31,5 +33,6 @@ export interface ReflectFieldMetadata {
defaultValue: FieldMetadataDefaultValue<'default'> | null; defaultValue: FieldMetadataDefaultValue<'default'> | null;
gate?: GateDecoratorParams; gate?: GateDecoratorParams;
options?: FieldMetadataOptions<'default'> | null; options?: FieldMetadataOptions<'default'> | null;
settings?: FieldMetadataSettings<'default'> | null;
}; };
} }

View File

@ -241,7 +241,9 @@ export class WorkspaceMetadataUpdaterService {
manager: EntityManager, manager: EntityManager,
entityClass: EntityTarget<Entity>, entityClass: EntityTarget<Entity>,
updateCollection: Array< updateCollection: Array<
DeepPartial<Omit<Entity, 'fields' | 'options'>> & { id: string } DeepPartial<Omit<Entity, 'fields' | 'options' | 'settings'>> & {
id: string;
}
>, >,
keysToOmit: (keyof Entity)[] = [], keysToOmit: (keyof Entity)[] = [],
): Promise<{ current: Entity; altered: Entity }[]> { ): Promise<{ current: Entity; altered: Entity }[]> {