Fix relation field unknown target object (#13129)
Fixes https://github.com/twentyhq/twenty/issues/12867 Issue: when you have a variable `toto` which is: `Record<string, MyType>` and you do toto['xxx'], this will be typed as `MyType` instead of `MyType | undefined` Solutions: - activate `noUncheckedIndexedAccess` check in tsconfig, this is the preferred solution but will take time to get there (this raises 600+ errors) - use a Map: cf https://github.com/twentyhq/twenty/pull/13125/files - set the type to Partial<Record<string, MyType>>. Drawback is that when you do Object.values(toto), you'll get `Array<MyType | undefined>`. Hence why we have to filter these behind <img width="1512" alt="image" src="https://github.com/user-attachments/assets/d0a0bfed-c441-4e53-84c2-2da98ccbcf50" />
This commit is contained in:
@ -19,9 +19,9 @@ import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace
|
|||||||
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
|
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
|
||||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
|
||||||
|
|
||||||
// TODO: find a way to prevent conflict between handlers executing logic on object relations
|
// TODO: find a way to prevent conflict between handlers executing logic on object relations
|
||||||
// And this factory that is also executing logic on object relations
|
// And this factory that is also executing logic on object relations
|
||||||
@ -121,6 +121,10 @@ export class QueryResultGettersFactory {
|
|||||||
): Promise<ObjectRecord> {
|
): Promise<ObjectRecord> {
|
||||||
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
|
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadataMapItem)) {
|
||||||
|
throw new Error('Object metadata map item is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
|
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
|
||||||
|
|
||||||
const relationFields = Object.keys(record)
|
const relationFields = Object.keys(record)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { IResolvers } from '@graphql-tools/utils';
|
import { IResolvers } from '@graphql-tools/utils';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||||
@ -75,7 +76,9 @@ export class WorkspaceResolverFactory {
|
|||||||
Mutation: {},
|
Mutation: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const objectMetadata of Object.values(objectMetadataMaps.byId)) {
|
for (const objectMetadata of Object.values(objectMetadataMaps.byId).filter(
|
||||||
|
isDefined,
|
||||||
|
)) {
|
||||||
// Generate query resolvers
|
// Generate query resolvers
|
||||||
for (const methodName of workspaceResolverBuilderMethods.queries) {
|
for (const methodName of workspaceResolverBuilderMethods.queries) {
|
||||||
const resolverName = getResolverName(objectMetadata, methodName);
|
const resolverName = getResolverName(objectMetadata, methodName);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||||
import { GraphQLSchema, printSchema } from 'graphql';
|
import { GraphQLSchema, printSchema } from 'graphql';
|
||||||
import { gql } from 'graphql-tag';
|
import { gql } from 'graphql-tag';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
||||||
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||||
@ -56,13 +57,13 @@ export class WorkspaceSchemaFactory {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map(
|
const objectMetadataCollection = Object.values(objectMetadataMaps.byId)
|
||||||
(objectMetadataItem) => ({
|
.filter(isDefined)
|
||||||
|
.map((objectMetadataItem) => ({
|
||||||
...objectMetadataItem,
|
...objectMetadataItem,
|
||||||
fields: Object.values(objectMetadataItem.fieldsById),
|
fields: Object.values(objectMetadataItem.fieldsById),
|
||||||
indexes: objectMetadataItem.indexMetadatas,
|
indexes: objectMetadataItem.indexMetadatas,
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
// Get typeDefs from cache
|
// Get typeDefs from cache
|
||||||
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
|
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
|
||||||
|
|||||||
@ -171,6 +171,12 @@ export abstract class RestApiBaseHandler {
|
|||||||
objectMetadata.objectMetadataMaps.byId[
|
objectMetadata.objectMetadataMaps.byId[
|
||||||
field.relationTargetObjectMetadataId
|
field.relationTargetObjectMetadataId
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isDefined(relationTargetObjectMetadata)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Object metadata relation target not found for relation creation payload`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const depth2Relations = this.getRelations({
|
const depth2Relations = this.getRelations({
|
||||||
objectMetadata: {
|
objectMetadata: {
|
||||||
objectMetadataMaps: objectMetadata.objectMetadataMaps,
|
objectMetadataMaps: objectMetadata.objectMetadataMaps,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
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 { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
@ -58,6 +59,10 @@ export const mapFieldMetadataToGraphqlQuery = (
|
|||||||
const relationMetadataItem =
|
const relationMetadataItem =
|
||||||
objectMetadataMaps.byId[targetObjectMetadataId];
|
objectMetadataMaps.byId[targetObjectMetadataId];
|
||||||
|
|
||||||
|
if (!isDefined(relationMetadataItem)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return `${field.name}
|
return `${field.name}
|
||||||
{
|
{
|
||||||
id
|
id
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
|
|||||||
export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMaps[] =
|
export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMaps[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: '',
|
id: '20202020-8dec-43d5-b2ff-6eef05095bec',
|
||||||
standardId: '',
|
standardId: '',
|
||||||
nameSingular: 'person',
|
nameSingular: 'person',
|
||||||
namePlural: 'people',
|
namePlural: 'people',
|
||||||
@ -52,7 +52,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
|||||||
fieldIdByJoinColumnName: {},
|
fieldIdByJoinColumnName: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '',
|
id: '20202020-c03c-45d6-a4b0-04afe1357c5c',
|
||||||
standardId: '',
|
standardId: '',
|
||||||
nameSingular: 'company',
|
nameSingular: 'company',
|
||||||
namePlural: 'companies',
|
namePlural: 'companies',
|
||||||
@ -112,7 +112,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
|||||||
fieldIdByJoinColumnName: {},
|
fieldIdByJoinColumnName: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '',
|
id: '20202020-3d75-4aab-bacd-ee176c5f63ca',
|
||||||
standardId: '',
|
standardId: '',
|
||||||
nameSingular: 'regular-custom-object',
|
nameSingular: 'regular-custom-object',
|
||||||
namePlural: 'regular-custom-objects',
|
namePlural: 'regular-custom-objects',
|
||||||
@ -172,7 +172,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
|||||||
fieldIdByJoinColumnName: {},
|
fieldIdByJoinColumnName: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '',
|
id: '20202020-540c-4397-b872-2a90ea2fb809',
|
||||||
standardId: '',
|
standardId: '',
|
||||||
nameSingular: 'non-searchable-object',
|
nameSingular: 'non-searchable-object',
|
||||||
namePlural: 'non-searchable-objects',
|
namePlural: 'non-searchable-objects',
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||||
import { Args, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||||
import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
|
import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
|
||||||
@ -10,12 +12,16 @@ import { SearchService } from 'src/engine/core-modules/search/services/search.se
|
|||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
|
@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
|
||||||
@UsePipes(ResolverValidationPipe)
|
@UsePipes(ResolverValidationPipe)
|
||||||
export class SearchResolver {
|
export class SearchResolver {
|
||||||
constructor(private readonly searchService: SearchService) {}
|
constructor(
|
||||||
|
private readonly searchService: SearchService,
|
||||||
|
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Query(() => SearchResultConnectionDTO)
|
@Query(() => SearchResultConnectionDTO)
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
@ -31,12 +37,16 @@ export class SearchResolver {
|
|||||||
after,
|
after,
|
||||||
}: SearchArgs,
|
}: SearchArgs,
|
||||||
) {
|
) {
|
||||||
const objectMetadataItemWithFieldMaps =
|
const objectMetadataMaps =
|
||||||
await this.searchService.getObjectMetadataItemWithFieldMaps(workspace);
|
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
const filteredObjectMetadataItems =
|
const filteredObjectMetadataItems =
|
||||||
this.searchService.filterObjectMetadataItems({
|
this.searchService.filterObjectMetadataItems({
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps: Object.values(
|
||||||
|
objectMetadataMaps.byId,
|
||||||
|
).filter(isDefined),
|
||||||
includedObjectNameSingulars: includedObjectNameSingulars ?? [],
|
includedObjectNameSingulars: includedObjectNameSingulars ?? [],
|
||||||
excludedObjectNameSingulars: excludedObjectNameSingulars ?? [],
|
excludedObjectNameSingulars: excludedObjectNameSingulars ?? [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,13 +29,11 @@ import {
|
|||||||
} from 'src/engine/core-modules/search/exceptions/search.exception';
|
} from 'src/engine/core-modules/search/exceptions/search.exception';
|
||||||
import { RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item';
|
import { RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item';
|
||||||
import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms';
|
import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
||||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
|
||||||
|
|
||||||
type LastRanks = { tsRankCD: number; tsRank: number };
|
type LastRanks = { tsRankCD: number; tsRank: number };
|
||||||
|
|
||||||
@ -51,18 +49,8 @@ export class SearchService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
private readonly fileService: FileService,
|
private readonly fileService: FileService,
|
||||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getObjectMetadataItemWithFieldMaps(workspace: Workspace) {
|
|
||||||
const objectMetadataMaps =
|
|
||||||
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.values(objectMetadataMaps.byId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllRecordsWithObjectMetadataItems({
|
async getAllRecordsWithObjectMetadataItems({
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
includedObjectNameSingulars,
|
includedObjectNameSingulars,
|
||||||
|
|||||||
@ -114,8 +114,14 @@ export class DataloaderService {
|
|||||||
{ workspaceId },
|
{ workspaceId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const indexMetadataCollection = objectMetadataIds.map((id) =>
|
const indexMetadataCollection = objectMetadataIds.map((id) => {
|
||||||
Object.values(objectMetadataMaps.byId[id].indexMetadatas).map(
|
const objectMetadata = objectMetadataMaps.byId[id];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadata)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(objectMetadata.indexMetadatas).map(
|
||||||
(indexMetadata) => {
|
(indexMetadata) => {
|
||||||
return {
|
return {
|
||||||
...indexMetadata,
|
...indexMetadata,
|
||||||
@ -127,8 +133,8 @@ export class DataloaderService {
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
return indexMetadataCollection;
|
return indexMetadataCollection;
|
||||||
},
|
},
|
||||||
@ -148,8 +154,14 @@ export class DataloaderService {
|
|||||||
{ workspaceId },
|
{ workspaceId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const fieldMetadataCollection = objectMetadataIds.map((id) =>
|
const fieldMetadataCollection = objectMetadataIds.map((id) => {
|
||||||
Object.values(objectMetadataMaps.byId[id].fieldsById).map(
|
const objectMetadata = objectMetadataMaps.byId[id];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadata)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(objectMetadata.fieldsById).map(
|
||||||
// TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface
|
// TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface
|
||||||
(fieldMetadata) => {
|
(fieldMetadata) => {
|
||||||
const overridesFieldToCompute = [
|
const overridesFieldToCompute = [
|
||||||
@ -182,8 +194,8 @@ export class DataloaderService {
|
|||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
return fieldMetadataCollection;
|
return fieldMetadataCollection;
|
||||||
},
|
},
|
||||||
@ -207,9 +219,13 @@ export class DataloaderService {
|
|||||||
objectMetadata: { id: objectMetadataId },
|
objectMetadata: { id: objectMetadataId },
|
||||||
indexMetadata: { id: indexMetadataId },
|
indexMetadata: { id: indexMetadataId },
|
||||||
}) => {
|
}) => {
|
||||||
const indexMetadataEntity = objectMetadataMaps.byId[
|
const objectMetadata = objectMetadataMaps.byId[objectMetadataId];
|
||||||
objectMetadataId
|
|
||||||
].indexMetadatas.find(
|
if (!isDefined(objectMetadata)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexMetadataEntity = objectMetadata.indexMetadatas.find(
|
||||||
(indexMetadata) => indexMetadata.id === indexMetadataId,
|
(indexMetadata) => indexMetadata.id === indexMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -166,6 +166,13 @@ export class FieldMetadataRelationService {
|
|||||||
relationCreationPayload.targetObjectMetadataId
|
relationCreationPayload.targetObjectMetadataId
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadataTarget)) {
|
||||||
|
throw new FieldMetadataException(
|
||||||
|
`Object metadata relation target not found for relation creation payload`,
|
||||||
|
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
validateFieldNameAvailabilityOrThrow(
|
validateFieldNameAvailabilityOrThrow(
|
||||||
computedMetadataNameFromLabel,
|
computedMetadataNameFromLabel,
|
||||||
objectMetadataTarget,
|
objectMetadataTarget,
|
||||||
|
|||||||
@ -107,7 +107,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
|
|
||||||
let existingFieldMetadata: FieldMetadataInterface | undefined;
|
let existingFieldMetadata: FieldMetadataInterface | undefined;
|
||||||
|
|
||||||
for (const objectMetadataItem of Object.values(objectMetadataMaps.byId)) {
|
for (const objectMetadataItem of Object.values(
|
||||||
|
objectMetadataMaps.byId,
|
||||||
|
).filter(isDefined)) {
|
||||||
const fieldMetadata = objectMetadataItem.fieldsById[id];
|
const fieldMetadata = objectMetadataItem.fieldsById[id];
|
||||||
|
|
||||||
if (fieldMetadata) {
|
if (fieldMetadata) {
|
||||||
@ -126,6 +128,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
const objectMetadataItemWithFieldMaps =
|
const objectMetadataItemWithFieldMaps =
|
||||||
objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId];
|
objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadataItemWithFieldMaps)) {
|
||||||
|
throw new FieldMetadataException(
|
||||||
|
'Object metadata does not exist',
|
||||||
|
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||||
|
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
@ -703,7 +712,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
isRemoteCreation,
|
isRemoteCreation,
|
||||||
}: {
|
}: {
|
||||||
createdFieldMetadataItems: FieldMetadataEntity[];
|
createdFieldMetadataItems: FieldMetadataEntity[];
|
||||||
objectMetadataMap: Record<string, ObjectMetadataItemWithFieldMaps>;
|
objectMetadataMap: ObjectMetadataMaps['byId'];
|
||||||
isRemoteCreation: boolean;
|
isRemoteCreation: boolean;
|
||||||
}): Promise<WorkspaceMigrationTableAction[]> {
|
}): Promise<WorkspaceMigrationTableAction[]> {
|
||||||
if (isRemoteCreation) {
|
if (isRemoteCreation) {
|
||||||
@ -726,10 +735,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const objectMetadata =
|
||||||
|
objectMetadataMap[createdFieldMetadata.objectMetadataId];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadata)) {
|
||||||
|
throw new FieldMetadataException(
|
||||||
|
'Object metadata does not exist',
|
||||||
|
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
migrationActions.push({
|
migrationActions.push({
|
||||||
name: computeObjectTargetTable(
|
name: computeObjectTargetTable(objectMetadata),
|
||||||
objectMetadataMap[createdFieldMetadata.objectMetadataId],
|
|
||||||
),
|
|
||||||
action: WorkspaceMigrationTableActionType.ALTER,
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||||
WorkspaceMigrationColumnActionType.CREATE,
|
WorkspaceMigrationColumnActionType.CREATE,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { capitalize } from 'twenty-shared/utils';
|
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||||
import { QueryRunner, Repository } from 'typeorm';
|
import { QueryRunner, Repository } from 'typeorm';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
@ -82,10 +82,12 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
relationObjectMetadataStandardId: string;
|
relationObjectMetadataStandardId: string;
|
||||||
queryRunner?: QueryRunner;
|
queryRunner?: QueryRunner;
|
||||||
}) {
|
}) {
|
||||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find(
|
const targetObjectMetadata = Object.values(objectMetadataMaps.byId)
|
||||||
(objectMetadata) =>
|
.filter(isDefined)
|
||||||
objectMetadata.standardId === relationObjectMetadataStandardId,
|
.find(
|
||||||
);
|
(objectMetadata) =>
|
||||||
|
objectMetadata.standardId === relationObjectMetadataStandardId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!targetObjectMetadata) {
|
if (!targetObjectMetadata) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
PermissionsExceptionMessage,
|
PermissionsExceptionMessage,
|
||||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ export class FieldPermissionService {
|
|||||||
role,
|
role,
|
||||||
}: {
|
}: {
|
||||||
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
||||||
objectMetadataMapsById: Record<string, ObjectMetadataItemWithFieldMaps>;
|
objectMetadataMapsById: ObjectMetadataMaps['byId'];
|
||||||
rolesPermissions: ObjectRecordsPermissionsByRoleId;
|
rolesPermissions: ObjectRecordsPermissionsByRoleId;
|
||||||
role: RoleEntity;
|
role: RoleEntity;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
|
||||||
export type ObjectMetadataMaps = {
|
export type ObjectMetadataMaps = {
|
||||||
byId: Record<string, ObjectMetadataItemWithFieldMaps>;
|
byId: Partial<Record<string, ObjectMetadataItemWithFieldMaps>>;
|
||||||
idByNameSingular: Record<string, string>;
|
idByNameSingular: Partial<Record<string, string>>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
|
|
||||||
@ -5,7 +7,9 @@ export const getObjectMetadataMapItemByNamePlural = (
|
|||||||
objectMetadataMaps: ObjectMetadataMaps,
|
objectMetadataMaps: ObjectMetadataMaps,
|
||||||
namePlural: string,
|
namePlural: string,
|
||||||
): ObjectMetadataItemWithFieldMaps | undefined => {
|
): ObjectMetadataItemWithFieldMaps | undefined => {
|
||||||
const objectMetadataItems = Object.values(objectMetadataMaps.byId);
|
const objectMetadataItems = Object.values(objectMetadataMaps.byId).filter(
|
||||||
|
isDefined,
|
||||||
|
);
|
||||||
|
|
||||||
return objectMetadataItems.find(
|
return objectMetadataItems.find(
|
||||||
(objectMetadata) => objectMetadata.namePlural === namePlural,
|
(objectMetadata) => objectMetadata.namePlural === namePlural,
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
|
|
||||||
@ -5,7 +7,11 @@ export const getObjectMetadataMapItemByNameSingular = (
|
|||||||
objectMetadataMaps: ObjectMetadataMaps,
|
objectMetadataMaps: ObjectMetadataMaps,
|
||||||
nameSingular: string,
|
nameSingular: string,
|
||||||
): ObjectMetadataItemWithFieldMaps | undefined => {
|
): ObjectMetadataItemWithFieldMaps | undefined => {
|
||||||
return objectMetadataMaps.byId[
|
const objectMetadataId = objectMetadataMaps.idByNameSingular[nameSingular];
|
||||||
objectMetadataMaps.idByNameSingular[nameSingular]
|
|
||||||
];
|
if (!isNonEmptyString(objectMetadataId)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectMetadataMaps.byId[objectMetadataId];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ObjectMetadataException,
|
ObjectMetadataException,
|
||||||
@ -19,14 +20,16 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({
|
|||||||
existingObjectMetadataId,
|
existingObjectMetadataId,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => {
|
}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => {
|
||||||
const objectAlreadyExists = Object.values(objectMetadataMaps.byId).find(
|
const objectAlreadyExists = Object.values(objectMetadataMaps.byId)
|
||||||
(objectMetadata) =>
|
.filter(isDefined)
|
||||||
(objectMetadata.nameSingular === objectMetadataNameSingular ||
|
.find(
|
||||||
objectMetadata.namePlural === objectMetadataNamePlural ||
|
(objectMetadata) =>
|
||||||
objectMetadata.nameSingular === objectMetadataNamePlural ||
|
(objectMetadata.nameSingular === objectMetadataNameSingular ||
|
||||||
objectMetadata.namePlural === objectMetadataNameSingular) &&
|
objectMetadata.namePlural === objectMetadataNamePlural ||
|
||||||
objectMetadata.id !== existingObjectMetadataId,
|
objectMetadata.nameSingular === objectMetadataNamePlural ||
|
||||||
);
|
objectMetadata.namePlural === objectMetadataNameSingular) &&
|
||||||
|
objectMetadata.id !== existingObjectMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
if (objectAlreadyExists) {
|
if (objectAlreadyExists) {
|
||||||
throw new ObjectMetadataException(
|
throw new ObjectMetadataException(
|
||||||
|
|||||||
@ -166,15 +166,16 @@ export class WorkspaceDatasourceFactory {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const entitySchemas = await Promise.all(
|
const entitySchemas = await Promise.all(
|
||||||
Object.values(cachedObjectMetadataMaps.byId).map(
|
Object.values(cachedObjectMetadataMaps.byId)
|
||||||
(objectMetadata) =>
|
.filter(isDefined)
|
||||||
|
.map((objectMetadata) =>
|
||||||
this.entitySchemaFactory.create(
|
this.entitySchemaFactory.create(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
dataSourceMetadataVersion,
|
dataSourceMetadataVersion,
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
cachedObjectMetadataMaps,
|
cachedObjectMetadataMaps,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceCacheStorageService.setORMEntitySchema(
|
await this.workspaceCacheStorageService.setORMEntitySchema(
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -40,8 +42,23 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
const objectMetadataIdForEntity =
|
const objectMetadataIdForEntity =
|
||||||
objectMetadataMaps.idByNameSingular[entityName];
|
objectMetadataMaps.idByNameSingular[entityName];
|
||||||
|
|
||||||
const objectMetadataIsSystem =
|
if (!isNonEmptyString(objectMetadataIdForEntity)) {
|
||||||
objectMetadataMaps.byId[objectMetadataIdForEntity]?.isSystem === true;
|
throw new PermissionsException(
|
||||||
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||||
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectMetadata = objectMetadataMaps.byId[objectMetadataIdForEntity];
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadata)) {
|
||||||
|
throw new PermissionsException(
|
||||||
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||||
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectMetadataIsSystem = objectMetadata.isSystem === true;
|
||||||
|
|
||||||
if (objectMetadataIsSystem) {
|
if (objectMetadataIsSystem) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -30,10 +30,11 @@ export const generateFakeFormResponse = async ({
|
|||||||
formFieldMetadata?.settings?.objectName,
|
formFieldMetadata?.settings?.objectName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!objectMetadataItemWithFieldsMaps)
|
if (!isDefined(objectMetadataItemWithFieldsMaps)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`,
|
`Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[formFieldMetadata.name]: {
|
[formFieldMetadata.name]: {
|
||||||
|
|||||||
@ -43,6 +43,10 @@ export const generateObjectRecordFields = ({
|
|||||||
field.relationTargetObjectMetadataId
|
field.relationTargetObjectMetadataId
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isDefined(relationTargetObjectMetadata)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
acc[field.name] = {
|
acc[field.name] = {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
icon: field.icon,
|
icon: field.icon,
|
||||||
|
|||||||
@ -196,7 +196,11 @@ export class DatabaseEventTriggerListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relatedObjectMetadataNameSingular =
|
const relatedObjectMetadataNameSingular =
|
||||||
objectMetadataMaps.byId[relatedObjectMetadataId].nameSingular;
|
objectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
|
||||||
|
|
||||||
|
if (!isDefined(relatedObjectMetadataNameSingular)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const relatedObjectRepository =
|
const relatedObjectRepository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
|
|||||||
@ -60,7 +60,7 @@ exports[`Field metadata relation creation should fail relation when targetObject
|
|||||||
"exceptionEventId": "mocked-exception-id",
|
"exceptionEventId": "mocked-exception-id",
|
||||||
"userFriendlyMessage": "An error occurred.",
|
"userFriendlyMessage": "An error occurred.",
|
||||||
},
|
},
|
||||||
"message": "Cannot read properties of undefined (reading 'fieldsById')",
|
"message": "Object metadata relation target not found for relation creation payload",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user