fix: many fields in an object (#10061)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2025-02-11 17:15:30 +01:00
committed by GitHub
parent 47f262c970
commit ed4a5b0c15
43 changed files with 22318 additions and 22058 deletions

View File

@ -3,9 +3,10 @@ import { JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tsConfig = require('./tsconfig.spec.json');
process.env.TZ = 'GMT';
process.env.LC_ALL = 'en_US.UTF-8';
const jestConfig: JestConfigWithTsJest = {
// to enable logs, comment out the following line
silent: true,
silent: false,
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
prettierPath: null,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -488,6 +488,7 @@ export type Field = {
label: Scalars['String'];
name: Scalars['String'];
object?: Maybe<Object>;
objectMetadataId: Scalars['UUID'];
options?: Maybe<Scalars['JSON']>;
relation?: Maybe<Relation>;
relationDefinition?: Maybe<RelationDefinition>;
@ -519,6 +520,7 @@ export type FieldFilter = {
isActive?: InputMaybe<BooleanFieldComparison>;
isCustom?: InputMaybe<BooleanFieldComparison>;
isSystem?: InputMaybe<BooleanFieldComparison>;
objectMetadataId?: InputMaybe<StringFieldComparison>;
or?: InputMaybe<Array<FieldFilter>>;
};
@ -1569,6 +1571,23 @@ export type SignUpOutput = {
workspace: WorkspaceUrlsAndId;
};
export type StringFieldComparison = {
eq?: InputMaybe<Scalars['String']>;
gt?: InputMaybe<Scalars['String']>;
gte?: InputMaybe<Scalars['String']>;
iLike?: InputMaybe<Scalars['String']>;
in?: InputMaybe<Array<Scalars['String']>>;
is?: InputMaybe<Scalars['Boolean']>;
isNot?: InputMaybe<Scalars['Boolean']>;
like?: InputMaybe<Scalars['String']>;
lt?: InputMaybe<Scalars['String']>;
lte?: InputMaybe<Scalars['String']>;
neq?: InputMaybe<Scalars['String']>;
notILike?: InputMaybe<Scalars['String']>;
notIn?: InputMaybe<Array<Scalars['String']>>;
notLike?: InputMaybe<Scalars['String']>;
};
export enum SubscriptionInterval {
Day = 'Day',
Month = 'Month',
@ -1690,6 +1709,7 @@ export type UpdateFieldInput = {
isUnique?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
objectMetadataId?: InputMaybe<Scalars['UUID']>;
options?: InputMaybe<Scalars['JSON']>;
settings?: InputMaybe<Scalars['JSON']>;
};

View File

@ -47,7 +47,8 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
rocketId
petId
surveyResultId
taskId
type
updatedAt

View File

@ -65,7 +65,8 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
rocketId
petId
surveyResultId
taskId
type
updatedAt
@ -94,8 +95,9 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
petId
position
rocketId
surveyResultId
taskId
updatedAt
viewId
@ -119,7 +121,8 @@ const mocks: MockedResponse[] = [
id
opportunityId
personId
rocketId
petId
surveyResultId
taskId
updatedAt
}
@ -141,8 +144,9 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
petId
properties
rocketId
surveyResultId
taskId
updatedAt
workflowId

View File

@ -269,9 +269,70 @@ mutation UpdateOneFavorite(
}
}
personId
position
rocket {
pet {
__typename
age
averageCostOfKibblePerMonth {
amountMicros
currencyCode
}
bio
birthday
comments
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
extraData
id
interestingFacts
isGoodWithKids
location {
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}
makesOwnerThinkOf {
firstName
lastName
}
name
pictures {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
position
soundSwag
species
traits
updatedAt
vetEmail {
primaryEmail
additionalEmails
}
vetPhone {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
}
petId
position
surveyResult {
__typename
averageEstimatedNumberOfAtomsInTheUniverse
comments
createdAt
createdBy {
source
@ -282,10 +343,14 @@ mutation UpdateOneFavorite(
deletedAt
id
name
participants
percentageOfCompletion
position
score
shortNotes
updatedAt
}
rocketId
surveyResultId
task {
__typename
assigneeId
@ -314,6 +379,8 @@ mutation UpdateOneFavorite(
icon
id
isCompact
kanbanAggregateOperation
kanbanAggregateOperationFieldMetadataId
kanbanFieldMetadataId
key
name
@ -326,6 +393,12 @@ mutation UpdateOneFavorite(
workflow {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
id
lastPublishedVersionId
@ -337,6 +410,7 @@ mutation UpdateOneFavorite(
workflowId
workflowRun {
__typename
context
createdAt
createdBy {
source
@ -564,9 +638,70 @@ export const mocks = [
}
}
personId
position
rocket {
pet {
__typename
age
averageCostOfKibblePerMonth {
amountMicros
currencyCode
}
bio
birthday
comments
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
extraData
id
interestingFacts
isGoodWithKids
location {
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}
makesOwnerThinkOf {
firstName
lastName
}
name
pictures {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
position
soundSwag
species
traits
updatedAt
vetEmail {
primaryEmail
additionalEmails
}
vetPhone {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
}
petId
position
surveyResult {
__typename
averageEstimatedNumberOfAtomsInTheUniverse
comments
createdAt
createdBy {
source
@ -577,10 +712,14 @@ export const mocks = [
deletedAt
id
name
participants
percentageOfCompletion
position
score
shortNotes
updatedAt
}
rocketId
surveyResultId
task {
__typename
assigneeId
@ -609,6 +748,8 @@ export const mocks = [
icon
id
isCompact
kanbanAggregateOperation
kanbanAggregateOperationFieldMetadataId
kanbanFieldMetadataId
key
name
@ -621,6 +762,12 @@ export const mocks = [
workflow {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
id
lastPublishedVersionId
@ -632,6 +779,7 @@ export const mocks = [
workflowId
workflowRun {
__typename
context
createdAt
createdBy {
source

View File

@ -1,11 +1,8 @@
import { gql } from '@apollo/client';
export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
query ObjectMetadataItems(
$objectFilter: ObjectFilter
$fieldFilter: FieldFilter
) {
objects(paging: { first: 1000 }, filter: $objectFilter) {
query ObjectMetadataItems {
objects(paging: { first: 1000 }) {
edges {
node {
id
@ -50,55 +47,45 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
}
}
}
fields(paging: { first: 1000 }, filter: $fieldFilter) {
edges {
node {
fieldsList {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relationDefinition {
relationId
direction
sourceObjectMetadata {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relationDefinition {
relationId
direction
sourceObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetObjectMetadata {
id
nameSingular
namePlural
}
targetFieldMetadata {
id
name
}
}
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetObjectMetadata {
id
nameSingular
namePlural
}
targetFieldMetadata {
id
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}

View File

@ -1,116 +1,6 @@
import { gql } from '@apollo/client';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
export const query = gql`
query ObjectMetadataItems($objectFilter: ObjectFilter, $fieldFilter: FieldFilter) {
objects(paging: {first: 1000}, filter: $objectFilter) {
edges {
node {
id
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
shortcut
isLabelSyncedWithName
indexMetadatas(paging: {first: 100}) {
edges {
node {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
indexFieldMetadatas(paging: {first: 100}) {
edges {
node {
id
createdAt
updatedAt
order
fieldMetadataId
}
}
}
}
}
}
fields(paging: {first: 1000}, filter: $fieldFilter) {
edges {
node {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
relationDefinition {
relationId
direction
sourceObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetObjectMetadata {
id
nameSingular
namePlural
}
targetFieldMetadata {
id
name
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
export const variables = { objectFilter: undefined, fieldFilter: undefined };
export const query = FIND_MANY_OBJECT_METADATA_ITEMS;
export const responseData = mockedStandardObjectMetadataQueryResult;

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
@ -9,14 +9,12 @@ import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/Sn
import {
query,
responseData,
variables,
} from '../__mocks__/useFindManyObjectMetadataItems';
const mocks = [
{
request: {
query,
variables,
},
result: jest.fn(() => ({
data: {

View File

@ -4,8 +4,6 @@ import { useMemo } from 'react';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
FieldFilter,
ObjectFilter,
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables,
} from '~/generated-metadata/graphql';
@ -18,12 +16,8 @@ import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useFindManyObjectMetadataItems = ({
skip,
objectFilter,
fieldFilter,
}: {
skip?: boolean;
objectFilter?: ObjectFilter;
fieldFilter?: FieldFilter;
} = {}) => {
const apolloMetadataClient = useApolloMetadataClient();
@ -33,10 +27,6 @@ export const useFindManyObjectMetadataItems = ({
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables
>(FIND_MANY_OBJECT_METADATA_ITEMS, {
variables: {
objectFilter,
fieldFilter,
},
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient,
onError: (error) => {

View File

@ -28,6 +28,8 @@ export const useRefreshObjectMetadataItems = (
});
replaceObjectMetadataItemIfDifferent(objectMetadataItems);
return objectMetadataItems;
};
const replaceObjectMetadataItemIfDifferent = useRecoilCallback(
@ -45,6 +47,7 @@ export const useRefreshObjectMetadataItems = (
},
[],
);
return {
refreshObjectMetadataItems,
};

View File

@ -10,6 +10,7 @@ export type ObjectMetadataItem = Omit<
| 'dataSourceId'
| 'indexMetadatas'
| 'labelIdentifierFieldMetadataId'
| 'fieldsList'
> & {
__typename?: string;
fields: FieldMetadataItem[];

View File

@ -14,9 +14,11 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
object.node.labelIdentifierFieldMetadataId,
);
const { fieldsList, ...objectWithoutFieldsList } = object.node;
return {
...object.node,
fields: object.node.fields.edges.map((field) => field.node),
...objectWithoutFieldsList,
fields: fieldsList,
labelIdentifierFieldMetadataId,
indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({
...index.node,

View File

@ -66,7 +66,8 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
noteId
opportunityId
personId
rocketId
petId
surveyResultId
taskId
type
updatedAt
@ -173,8 +174,9 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
noteId
opportunityId
personId
petId
position
rocketId
surveyResultId
taskId
updatedAt
viewId
@ -225,7 +227,8 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
noteId
opportunityId
personId
rocketId
petId
surveyResultId
updatedAt
}
}
@ -275,7 +278,8 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
id
opportunityId
personId
rocketId
petId
surveyResultId
taskId
updatedAt
}
@ -297,8 +301,9 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
noteId
opportunityId
personId
petId
properties
rocketId
surveyResultId
taskId
updatedAt
workflowId

View File

@ -201,7 +201,7 @@ const mocks: MockedResponse[] = [
}
}
personId
rocket {
pet {
__typename
createdAt
createdBy {
@ -216,7 +216,8 @@ const mocks: MockedResponse[] = [
position
updatedAt
}
rocketId
petId
surveyResultId
updatedAt
}
}
@ -361,7 +362,7 @@ const mocks: MockedResponse[] = [
}
}
personId
rocket {
pet {
__typename
createdAt
createdBy {
@ -376,7 +377,8 @@ const mocks: MockedResponse[] = [
position
updatedAt
}
rocketId
petId
surveyResultId
task {
__typename
assigneeId

View File

@ -75,7 +75,8 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
rocketId
petId
surveyResultId
taskId
type
updatedAt
@ -108,8 +109,9 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
petId
position
rocketId
surveyResultId
taskId
updatedAt
viewId
@ -144,7 +146,8 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
rocketId
petId
surveyResultId
updatedAt
}
}
@ -243,7 +246,8 @@ const mocks: MockedResponse[] = [
id
opportunityId
personId
rocketId
petId
surveyResultId
taskId
updatedAt
}
@ -265,8 +269,9 @@ const mocks: MockedResponse[] = [
noteId
opportunityId
personId
petId
properties
rocketId
surveyResultId
taskId
updatedAt
workflowId

View File

@ -74,7 +74,8 @@ const companyMocks = [
noteId
opportunityId
personId
rocketId
petId
surveyResultId
taskId
type
updatedAt
@ -107,8 +108,9 @@ const companyMocks = [
noteId
opportunityId
personId
petId
position
rocketId
surveyResultId
taskId
updatedAt
viewId
@ -143,7 +145,8 @@ const companyMocks = [
noteId
opportunityId
personId
rocketId
petId
surveyResultId
updatedAt
}
}
@ -242,7 +245,8 @@ const companyMocks = [
id
opportunityId
personId
rocketId
petId
surveyResultId
taskId
updatedAt
}
@ -264,8 +268,9 @@ const companyMocks = [
noteId
opportunityId
personId
petId
properties
rocketId
surveyResultId
taskId
updatedAt
workflowId

View File

@ -24,8 +24,10 @@ export const mockedFavoritesData = [
view: null,
taskId: null,
task: null,
rocketId: null,
rocket: null,
petId: null,
pet: null,
surveyResultId: null,
surveyResult: null,
personId: null,
person: null,
opportunityId: null,
@ -55,8 +57,10 @@ export const mockedFavoritesData = [
view: mockedViewsData[0],
taskId: null,
task: null,
rocketId: null,
rocket: null,
petId: null,
pet: null,
surveyResultId: null,
surveyResult: null,
personId: null,
person: null,
opportunityId: null,
@ -86,8 +90,10 @@ export const mockedFavoritesData = [
view: mockedViewsData[1],
taskId: null,
task: null,
rocketId: null,
rocket: null,
petId: null,
pet: null,
surveyResultId: null,
surveyResult: null,
personId: null,
person: null,
opportunityId: null,
@ -117,8 +123,10 @@ export const mockedFavoritesData = [
view: mockedViewsData[1],
taskId: null,
task: null,
rocketId: null,
rocket: null,
petId: null,
pet: null,
surveyResultId: null,
surveyResult: null,
personId: null,
person: null,
opportunityId: null,

View File

@ -1,5 +1,6 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result';
export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
@ -9,9 +10,11 @@ export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
edge.node.labelIdentifierFieldMetadataId,
);
const { fieldsList, ...objectWithoutFieldsList } = edge.node;
return {
...edge.node,
fields: edge.node.fields.edges.map((edge) => edge.node),
...objectWithoutFieldsList,
fields: fieldsList,
labelIdentifierFieldMetadataId,
indexMetadatas: edge.node.indexMetadatas.edges.map((index) => ({
...index.node,

View File

@ -9,6 +9,7 @@ import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-wo
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
import { UpgradeTo0_41CommandModule } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module';
import { UpgradeTo0_42CommandModule } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@ -51,6 +52,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
WorkspaceMetadataVersionModule,
UpgradeTo0_40CommandModule,
UpgradeTo0_41CommandModule,
UpgradeTo0_42CommandModule,
FeatureFlagModule,
],
providers: [

View File

@ -0,0 +1,117 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { CommandLogger } from 'src/database/commands/logger';
import { settings } from 'src/engine/constants/settings';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
@Command({
name: 'upgrade-0.42:limit-amount-of-view-field',
description: 'Limit amount of view field.',
})
export class LimitAmountOfViewFieldCommand extends ActiveWorkspacesCommandRunner {
protected readonly logger: CommandLogger;
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
this.logger = new CommandLogger({
constructorName: this.constructor.name,
verbose: false,
});
this.logger.setVerbose(false);
}
async execute(workspaceId: string, dryRun?: boolean): Promise<void> {
this.logger.log(
`Processing workspace ${workspaceId} for view field limitation`,
);
try {
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
ViewWorkspaceEntity,
);
const views = await viewRepository.find({});
for (const view of views) {
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
ViewFieldWorkspaceEntity,
);
const viewFields = await viewFieldRepository.find({
where: {
viewId: view.id,
isVisible: true,
},
order: {
position: 'ASC',
},
});
if (viewFields.length > settings.maxVisibleViewFields) {
const extraFields = viewFields.slice(settings.maxVisibleViewFields);
for (const field of extraFields) {
this.logger.log(
`Workspace ${workspaceId} - Hiding field ${field.id} in view ${view.id} (position ${field.position})`,
);
if (!dryRun) {
await viewFieldRepository.update(
{ id: field.id },
{ isVisible: false },
);
}
}
}
}
} catch (error) {
this.logger.error(
`Error limiting view fields in workspace ${workspaceId}`,
error,
);
throw error;
}
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(`Running limit-amount-of-view-field command`);
if (options?.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
for (const [index, workspaceId] of workspaceIds.entries()) {
try {
await this.execute(workspaceId, options?.dryRun);
this.logger.verbose(
`Processed workspace: ${workspaceId} (${index + 1}/${
workspaceIds.length
})`,
);
} catch (error) {
this.logger.error(`Error for workspace: ${workspaceId}`, error);
}
}
}
}

View File

@ -0,0 +1,37 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Command({
name: 'upgrade-0.42',
description: 'Upgrade to 0.42',
})
export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
passedParam: string[],
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log('Running command to upgrade to 0.42');
await this.limitAmountOfViewFieldCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
}
}

View File

@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
TypeORMModule,
DataSourceModule,
ObjectMetadataModule,
WorkspaceSyncMetadataCommandsModule,
WorkspaceSyncMetadataModule,
WorkspaceHealthModule,
WorkspaceDataSourceModule,
WorkspaceMetadataVersionModule,
],
providers: [
SyncWorkspaceLoggerService,
SyncWorkspaceMetadataCommand,
LimitAmountOfViewFieldCommand,
],
})
export class UpgradeTo0_42CommandModule {}

View File

@ -10,4 +10,5 @@ export const settings: Settings = {
maxFileSize: '10MB',
},
minLengthOfStringForDuplicateCheck: 3,
maxVisibleViewFields: 30,
};

View File

@ -12,4 +12,5 @@ export interface Settings {
maxFileSize: `${number}MB`;
};
minLengthOfStringForDuplicateCheck: number;
maxVisibleViewFields: number;
}

View File

@ -1,6 +1,7 @@
import DataLoader from 'dataloader';
import {
FieldMetadataLoaderPayload,
RelationLoaderPayload,
RelationMetadataLoaderPayload,
} from 'src/engine/dataloaders/dataloader.service';
@ -23,4 +24,9 @@ export interface IDataloaders {
targetFieldMetadata: FieldMetadataEntity;
}
>;
fieldMetadataLoader: DataLoader<
FieldMetadataLoaderPayload,
FieldMetadataEntity[]
>;
}

View File

@ -3,9 +3,11 @@ import { Injectable } from '@nestjs/common';
import DataLoader from 'dataloader';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -31,20 +33,28 @@ export type RelationLoaderPayload = {
>;
};
export type FieldMetadataLoaderPayload = {
workspaceId: string;
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
};
@Injectable()
export class DataloaderService {
constructor(
private readonly relationMetadataService: RelationMetadataService,
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
private readonly fieldMetadataService: FieldMetadataService,
) {}
createLoaders(): IDataloaders {
const relationMetadataLoader = this.createRelationMetadataLoader();
const relationLoader = this.createRelationLoader();
const fieldMetadataLoader = this.createFieldMetadataLoader();
return {
relationMetadataLoader,
relationLoader,
fieldMetadataLoader,
};
}
@ -92,4 +102,23 @@ export class DataloaderService {
return fieldMetadataRelationCollection;
});
}
private createFieldMetadataLoader() {
return new DataLoader<FieldMetadataLoaderPayload, FieldMetadataEntity[]>(
async (dataLoaderParams: FieldMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const objectMetadataItems = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.objectMetadata,
);
const fieldMetadataCollection =
await this.fieldMetadataService.getFieldMetadataItemsByBatch(
objectMetadataItems.map((item) => item.id),
workspaceId,
);
return fieldMetadataCollection;
},
);
}
}

View File

@ -9,6 +9,7 @@ import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4, v4 } from 'uuid';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { settings } from 'src/engine/constants/settings';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@ -860,6 +861,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
`SELECT * FROM ${dataSourceMetadata.schema}."viewField"
WHERE "viewId" = '${view[0].id}'`,
)) as ViewFieldWorkspaceEntity[];
const isVisible =
existingViewFields.length < settings.maxVisibleViewFields;
const createdFieldIsAlreadyInView = existingViewFields.some(
(existingViewField) =>
@ -882,7 +885,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
("fieldMetadataId", "position", "isVisible", "size", "viewId")
VALUES ('${createdFieldMetadata.id}', '${
lastPosition + 1
}', true, 180, '${view[0].id}')`,
}', ${isVisible}, 180, '${view[0].id}')`,
);
}
}
@ -895,4 +898,20 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
await workspaceQueryRunner.release();
}
}
async getFieldMetadataItemsByBatch(
objectMetadataIds: string[],
workspaceId: string,
) {
const fieldMetadataItems = await this.fieldMetadataRepository.find({
where: { objectMetadataId: In(objectMetadataIds), workspaceId },
});
return objectMetadataIds.map((objectMetadataId) =>
fieldMetadataItems.filter(
(fieldMetadataItem) =>
fieldMetadataItem.objectMetadataId === objectMetadataId,
),
);
}
}

View File

@ -12,9 +12,11 @@ import { SettingsFeatures } from 'twenty-shared';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import {
@ -104,4 +106,26 @@ export class ObjectMetadataResolver {
objectMetadataGraphqlApiExceptionHandler(error);
}
}
@ResolveField(() => [FieldMetadataDTO], { nullable: false })
async fieldsList(
@AuthWorkspace() workspace: Workspace,
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: { loaders: IDataloaders },
): Promise<FieldMetadataDTO[]> {
try {
const fieldMetadataItems = await context.loaders.fieldMetadataLoader.load(
{
objectMetadata,
workspaceId: workspace.id,
},
);
return fieldMetadataItems;
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
return [];
}
}
}

View File

@ -23,7 +23,8 @@ describe('attachmentsResolver (e2e)', () => {
personId
companyId
opportunityId
rocketId
petId
surveyResultId
}
}
}
@ -64,7 +65,8 @@ describe('attachmentsResolver (e2e)', () => {
expect(attachments).toHaveProperty('personId');
expect(attachments).toHaveProperty('companyId');
expect(attachments).toHaveProperty('opportunityId');
expect(attachments).toHaveProperty('rocketId');
expect(attachments).toHaveProperty('petId');
expect(attachments).toHaveProperty('surveyResultId');
}
});
});

View File

@ -25,7 +25,8 @@ describe('favoritesResolver (e2e)', () => {
taskId
noteId
viewId
rocketId
petId
surveyResultId
}
}
}
@ -68,7 +69,8 @@ describe('favoritesResolver (e2e)', () => {
expect(favorites).toHaveProperty('taskId');
expect(favorites).toHaveProperty('noteId');
expect(favorites).toHaveProperty('viewId');
expect(favorites).toHaveProperty('rocketId');
expect(favorites).toHaveProperty('petId');
expect(favorites).toHaveProperty('surveyResultId');
}
});
});

View File

@ -18,7 +18,8 @@ describe('noteTargetsResolver (e2e)', () => {
personId
companyId
opportunityId
rocketId
petId
surveyResultId
}
}
}
@ -54,7 +55,8 @@ describe('noteTargetsResolver (e2e)', () => {
expect(noteTargets).toHaveProperty('personId');
expect(noteTargets).toHaveProperty('companyId');
expect(noteTargets).toHaveProperty('opportunityId');
expect(noteTargets).toHaveProperty('rocketId');
expect(noteTargets).toHaveProperty('petId');
expect(noteTargets).toHaveProperty('surveyResultId');
}
});
});

View File

@ -2,12 +2,12 @@ import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('rocketsResolver (e2e)', () => {
it('should find many rockets', () => {
describe('petsResolver (e2e)', () => {
it('should find many pets', () => {
const queryData = {
query: `
query rockets {
rockets {
query pets {
pets {
edges {
node {
id
@ -34,7 +34,7 @@ describe('rocketsResolver (e2e)', () => {
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.rockets;
const data = res.body.data.pets;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
@ -42,15 +42,15 @@ describe('rocketsResolver (e2e)', () => {
const edges = data.edges;
if (edges.length > 0) {
const rockets = edges[0].node;
const pets = edges[0].node;
expect(rockets).toHaveProperty('id');
expect(rockets).toHaveProperty('name');
expect(rockets).toHaveProperty('createdAt');
expect(rockets).toHaveProperty('updatedAt');
expect(rockets).toHaveProperty('deletedAt');
expect(rockets).toHaveProperty('position');
expect(rockets).toHaveProperty('searchVector');
expect(pets).toHaveProperty('id');
expect(pets).toHaveProperty('name');
expect(pets).toHaveProperty('createdAt');
expect(pets).toHaveProperty('updatedAt');
expect(pets).toHaveProperty('deletedAt');
expect(pets).toHaveProperty('position');
expect(pets).toHaveProperty('searchVector');
}
});
});

View File

@ -23,7 +23,8 @@ describe('searchAttachmentsResolver (e2e)', () => {
personId
companyId
opportunityId
rocketId
petId
surveyResultId
}
}
}
@ -64,7 +65,8 @@ describe('searchAttachmentsResolver (e2e)', () => {
expect(searchAttachments).toHaveProperty('personId');
expect(searchAttachments).toHaveProperty('companyId');
expect(searchAttachments).toHaveProperty('opportunityId');
expect(searchAttachments).toHaveProperty('rocketId');
expect(searchAttachments).toHaveProperty('petId');
expect(searchAttachments).toHaveProperty('surveyResultId');
}
});
});

View File

@ -25,7 +25,8 @@ describe('searchFavoritesResolver (e2e)', () => {
taskId
noteId
viewId
rocketId
petId
surveyResultId
}
}
}
@ -68,7 +69,8 @@ describe('searchFavoritesResolver (e2e)', () => {
expect(searchFavorites).toHaveProperty('taskId');
expect(searchFavorites).toHaveProperty('noteId');
expect(searchFavorites).toHaveProperty('viewId');
expect(searchFavorites).toHaveProperty('rocketId');
expect(searchFavorites).toHaveProperty('petId');
expect(searchFavorites).toHaveProperty('surveyResultId');
}
});
});

View File

@ -18,7 +18,8 @@ describe('searchNoteTargetsResolver (e2e)', () => {
personId
companyId
opportunityId
rocketId
petId
surveyResultId
}
}
}
@ -54,7 +55,8 @@ describe('searchNoteTargetsResolver (e2e)', () => {
expect(searchNoteTargets).toHaveProperty('personId');
expect(searchNoteTargets).toHaveProperty('companyId');
expect(searchNoteTargets).toHaveProperty('opportunityId');
expect(searchNoteTargets).toHaveProperty('rocketId');
expect(searchNoteTargets).toHaveProperty('petId');
expect(searchNoteTargets).toHaveProperty('surveyResultId');
}
});
});

View File

@ -2,12 +2,12 @@ import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('searchRocketsResolver (e2e)', () => {
it('should find many searchRockets', () => {
describe('searchPetsResolver (e2e)', () => {
it('should find many searchPets', () => {
const queryData = {
query: `
query searchRockets {
searchRockets {
query searchPets {
searchPets {
edges {
node {
id
@ -34,7 +34,7 @@ describe('searchRocketsResolver (e2e)', () => {
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.searchRockets;
const data = res.body.data.searchPets;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
@ -42,15 +42,15 @@ describe('searchRocketsResolver (e2e)', () => {
const edges = data.edges;
if (edges.length > 0) {
const searchRockets = edges[0].node;
const searchPets = edges[0].node;
expect(searchRockets).toHaveProperty('id');
expect(searchRockets).toHaveProperty('name');
expect(searchRockets).toHaveProperty('createdAt');
expect(searchRockets).toHaveProperty('updatedAt');
expect(searchRockets).toHaveProperty('deletedAt');
expect(searchRockets).toHaveProperty('position');
expect(searchRockets).toHaveProperty('searchVector');
expect(searchPets).toHaveProperty('id');
expect(searchPets).toHaveProperty('name');
expect(searchPets).toHaveProperty('createdAt');
expect(searchPets).toHaveProperty('updatedAt');
expect(searchPets).toHaveProperty('deletedAt');
expect(searchPets).toHaveProperty('position');
expect(searchPets).toHaveProperty('searchVector');
}
});
});

View File

@ -18,7 +18,8 @@ describe('searchTaskTargetsResolver (e2e)', () => {
personId
companyId
opportunityId
rocketId
petId
surveyResultId
}
}
}
@ -54,7 +55,8 @@ describe('searchTaskTargetsResolver (e2e)', () => {
expect(searchTaskTargets).toHaveProperty('personId');
expect(searchTaskTargets).toHaveProperty('companyId');
expect(searchTaskTargets).toHaveProperty('opportunityId');
expect(searchTaskTargets).toHaveProperty('rocketId');
expect(searchTaskTargets).toHaveProperty('petId');
expect(searchTaskTargets).toHaveProperty('surveyResultId');
}
});
});

View File

@ -29,7 +29,8 @@ describe('searchTimelineActivitiesResolver (e2e)', () => {
workflowId
workflowVersionId
workflowRunId
rocketId
petId
surveyResultId
}
}
}
@ -80,7 +81,8 @@ describe('searchTimelineActivitiesResolver (e2e)', () => {
expect(searchTimelineActivities).toHaveProperty('workflowId');
expect(searchTimelineActivities).toHaveProperty('workflowVersionId');
expect(searchTimelineActivities).toHaveProperty('workflowRunId');
expect(searchTimelineActivities).toHaveProperty('rocketId');
expect(searchTimelineActivities).toHaveProperty('petId');
expect(searchTimelineActivities).toHaveProperty('surveyResultId');
}
});
});

View File

@ -18,7 +18,8 @@ describe('taskTargetsResolver (e2e)', () => {
personId
companyId
opportunityId
rocketId
petId
surveyResultId
}
}
}
@ -54,7 +55,8 @@ describe('taskTargetsResolver (e2e)', () => {
expect(taskTargets).toHaveProperty('personId');
expect(taskTargets).toHaveProperty('companyId');
expect(taskTargets).toHaveProperty('opportunityId');
expect(taskTargets).toHaveProperty('rocketId');
expect(taskTargets).toHaveProperty('petId');
expect(taskTargets).toHaveProperty('surveyResultId');
}
});
});

View File

@ -29,7 +29,8 @@ describe('timelineActivitiesResolver (e2e)', () => {
workflowId
workflowVersionId
workflowRunId
rocketId
petId
surveyResultId
}
}
}
@ -76,7 +77,8 @@ describe('timelineActivitiesResolver (e2e)', () => {
expect(timelineActivities).toHaveProperty('workflowId');
expect(timelineActivities).toHaveProperty('workflowVersionId');
expect(timelineActivities).toHaveProperty('workflowRunId');
expect(timelineActivities).toHaveProperty('rocketId');
expect(timelineActivities).toHaveProperty('petId');
expect(timelineActivities).toHaveProperty('surveyResultId');
}
});
});