Migrate metadata cache (#16030)

## Context
Deprecating legacy ObjectMetadata from cache in favor of flat entities.
Introducing utils to build byName/byNameSingular/byNamePlural in
isolated cases

## Next
- I had to introduce a util to build from flat to legacy
objectMetadataMaps, we should instead use flat maps directly when needed
(datasource, schema generation, etc)
- Deprecate metadata version in the cache
- Use the new cache strategy for flat entities with permissions and
feature flags and inject in the global datasource context
This commit is contained in:
Weiko
2025-11-24 19:50:47 +01:00
committed by GitHub
parent 26e2fe349f
commit 04562b11fb
49 changed files with 764 additions and 598 deletions
@@ -33,7 +33,6 @@ import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-fie
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
import { ViewGroupModule } from 'src/engine/metadata-modules/view-group/view-group.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -68,7 +67,6 @@ import { UpdateFieldInput } from './dtos/update-field.input';
ViewFilterModule,
ViewGroupModule,
PermissionsModule,
WorkspaceMetadataCacheModule,
WorkspaceMigrationV2Module,
FlatFieldMetadataModule,
IndexMetadataModule,
@@ -0,0 +1,45 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
export type FieldMapsForObject = {
fieldIdByName: Record<string, string>;
fieldIdByJoinColumnName: Record<string, string>;
};
export const buildFieldMapsForObject = (
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
objectMetadataId: string,
): FieldMapsForObject => {
const fieldIdByName: Record<string, string> = {};
const fieldIdByJoinColumnName: Record<string, string> = {};
for (const [fieldId, field] of Object.entries(flatFieldMetadataMaps.byId)) {
if (!isDefined(field)) {
continue;
}
if (field.objectMetadataId !== objectMetadataId) {
continue;
}
fieldIdByName[field.name] = fieldId;
if (isMorphOrRelationFlatFieldMetadata(field)) {
const joinColumnName = (
field.settings as { joinColumnName?: string } | undefined
)?.joinColumnName;
if (joinColumnName) {
fieldIdByJoinColumnName[joinColumnName] = fieldId;
}
}
}
return {
fieldIdByName,
fieldIdByJoinColumnName,
};
};
@@ -0,0 +1,27 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const buildObjectIdByNameMaps = (
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
): {
idByNameSingular: Record<string, string>;
idByNamePlural: Record<string, string>;
} => {
const idByNameSingular: Record<string, string> = {};
const idByNamePlural: Record<string, string> = {};
for (const [objectId, objectMetadata] of Object.entries(
flatObjectMetadataMaps.byId,
)) {
if (!isDefined(objectMetadata)) {
continue;
}
idByNameSingular[objectMetadata.nameSingular] = objectId;
idByNamePlural[objectMetadata.namePlural] = objectId;
}
return { idByNameSingular, idByNamePlural };
};
@@ -0,0 +1,45 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { buildFieldMapsForObject } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const buildObjectMetadataItemWithFieldMaps = (
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
): ObjectMetadataItemWithFieldMaps => {
const fieldsById: Record<string, FlatFieldMetadata> = {};
for (const fieldId of flatObjectMetadata.fieldMetadataIds) {
const field = flatFieldMetadataMaps.byId[fieldId];
if (isDefined(field)) {
fieldsById[fieldId] = field;
}
}
const { fieldIdByName, fieldIdByJoinColumnName } = buildFieldMapsForObject(
flatFieldMetadataMaps,
flatObjectMetadata.id,
);
const indexMetadatas = flatObjectMetadata.indexMetadataIds
.map((indexId) => flatIndexMaps.byId[indexId])
.filter(isDefined)
.map((flatIndex) => ({
...flatIndex,
indexFieldMetadatas: flatIndex.flatIndexFieldMetadatas,
}));
return {
...flatObjectMetadata,
fieldsById,
fieldIdByName,
fieldIdByJoinColumnName,
indexMetadatas,
} as unknown as ObjectMetadataItemWithFieldMaps;
};
@@ -31,7 +31,6 @@ import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-s
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@@ -61,7 +60,6 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
PermissionsModule,
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
WorkspaceDataSourceModule,
FeatureFlagModule,
WorkspaceMigrationV2Module,
@@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
@@ -13,9 +14,7 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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';
describe('ObjectPermissionService', () => {
let service: ObjectPermissionService;
@@ -24,7 +23,7 @@ describe('ObjectPermissionService', () => {
>;
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
let workspaceCacheStorageService: jest.Mocked<WorkspaceCacheStorageService>;
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -56,9 +55,9 @@ describe('ObjectPermissionService', () => {
},
},
{
provide: WorkspaceCacheStorageService,
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
useValue: {
getObjectMetadataMapsOrThrow: jest.fn(),
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
},
},
],
@@ -72,7 +71,9 @@ describe('ObjectPermissionService', () => {
workspacePermissionsCacheService = module.get(
WorkspacePermissionsCacheService,
);
workspaceCacheStorageService = module.get(WorkspaceCacheStorageService);
workspaceManyOrAllFlatEntityMapsCacheService = module.get(
WorkspaceManyOrAllFlatEntityMapsCacheService,
);
});
describe('upsertObjectPermissions', () => {
@@ -106,18 +107,26 @@ describe('ObjectPermissionService', () => {
],
};
// Mock object metadata maps with a system object
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
// Mock flat object metadata maps with a system object
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {
[systemObjectMetadataId]: {
id: systemObjectMetadataId,
isSystem: true,
workspaceId,
} as ObjectMetadataItemWithFieldMaps,
flatObjectMetadataMaps: {
byId: {
[systemObjectMetadataId]: {
id: systemObjectMetadataId,
isSystem: true,
workspaceId,
fieldMetadataIds: [],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: systemObjectMetadataId,
applicationId: null,
} as any,
},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
idByNameSingular: {},
},
} as any,
);
// Act & Assert
@@ -155,18 +164,26 @@ describe('ObjectPermissionService', () => {
],
};
// Mock object metadata maps with a custom object
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
// Mock flat object metadata maps with a custom object
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {
[customObjectMetadataId]: {
id: customObjectMetadataId,
isSystem: false,
workspaceId,
} as ObjectMetadataItemWithFieldMaps,
flatObjectMetadataMaps: {
byId: {
[customObjectMetadataId]: {
id: customObjectMetadataId,
isSystem: false,
workspaceId,
fieldMetadataIds: [],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: customObjectMetadataId,
applicationId: null,
} as any,
},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
idByNameSingular: {},
},
} as any,
);
// Mock successful upsert
@@ -236,12 +253,15 @@ describe('ObjectPermissionService', () => {
],
};
// Mock empty object metadata maps
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
// Mock empty flat object metadata maps
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {},
idByNameSingular: {},
},
flatObjectMetadataMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
} as any,
);
// Act & Assert
@@ -10,6 +10,7 @@ import {
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
@@ -20,7 +21,6 @@ import {
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
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 { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('FieldPermissionService', () => {
@@ -31,7 +31,7 @@ describe('FieldPermissionService', () => {
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
let fieldMetadataRepository: jest.Mocked<Repository<FieldMetadataEntity>>;
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
let workspaceCacheStorageService: jest.Mocked<WorkspaceCacheStorageService>;
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
const testWorkspaceId = '20202020-0000-0000-0000-000000000000';
const testRoleId = '20202020-0000-0000-0000-000000000001';
@@ -98,9 +98,9 @@ describe('FieldPermissionService', () => {
},
},
{
provide: WorkspaceCacheStorageService,
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
useValue: {
getObjectMetadataMapsOrThrow: jest.fn(),
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
},
},
{
@@ -123,7 +123,9 @@ describe('FieldPermissionService', () => {
workspacePermissionsCacheService = module.get(
WorkspacePermissionsCacheService,
);
workspaceCacheStorageService = module.get(WorkspaceCacheStorageService);
workspaceManyOrAllFlatEntityMapsCacheService = module.get(
WorkspaceManyOrAllFlatEntityMapsCacheService,
);
// Setup default mocks
roleRepository.findOne.mockResolvedValue(mockRole);
@@ -137,39 +139,49 @@ describe('FieldPermissionService', () => {
data: mockRolesPermissions,
},
);
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
const testFieldMetadata = getMockFieldMetadataEntity({
...fieldTextMock,
label: 'Test Field',
objectMetadataId: testObjectMetadataId,
workspaceId: testWorkspaceId,
id: testFieldMetadataId,
});
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {
[testObjectMetadataId]: {
...objectMetadataItemMock,
fieldsById: {
[fieldTextMock.id]: getMockFieldMetadataEntity({
...fieldTextMock,
label: 'Test Field',
objectMetadataId: testObjectMetadataId,
workspaceId: testWorkspaceId,
id: '20202020-0000-0000-0000-000000000003',
}),
},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
indexMetadatas: [],
flatObjectMetadataMaps: {
byId: {
[testObjectMetadataId]: {
...objectMetadataItemMock,
id: testObjectMetadataId,
fieldMetadataIds: [testFieldMetadataId],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: testObjectMetadataId,
applicationId: null,
} as any,
[fieldRelationMock.objectMetadataId]: {
...objectMetadataItemMock,
id: fieldRelationMock.objectMetadataId,
fieldMetadataIds: [fieldRelationMock.id],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: fieldRelationMock.objectMetadataId,
applicationId: null,
} as any,
},
[fieldRelationMock.objectMetadataId]: {
...objectMetadataItemMock,
id: fieldRelationMock.objectMetadataId,
fieldsById: {
[fieldRelationMock.id]: fieldRelationMock,
},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
indexMetadatas: [],
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
flatFieldMetadataMaps: {
byId: {
[testFieldMetadataId]: testFieldMetadata as any,
[fieldRelationMock.id]: fieldRelationMock as any,
},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
idByNameSingular: {
testObject: testObjectMetadataId,
},
},
} as any,
);
fieldPermissionsRepository.find.mockResolvedValue([]);
fieldPermissionsRepository.upsert.mockResolvedValue({} as any);
@@ -386,11 +398,19 @@ describe('FieldPermissionService', () => {
});
it('should throw error when object metadata is not found', async () => {
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {},
idByNameSingular: {},
},
flatObjectMetadataMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
flatFieldMetadataMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
} as any,
);
const input = createUpsertInput([
@@ -420,21 +440,29 @@ describe('FieldPermissionService', () => {
isSystem: true,
};
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {
[testObjectMetadataId]: {
...systemObjectMetadata,
fieldsById: {},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
indexMetadatas: [],
flatObjectMetadataMaps: {
byId: {
[testObjectMetadataId]: {
...systemObjectMetadata,
id: testObjectMetadataId,
fieldMetadataIds: [],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: testObjectMetadataId,
applicationId: null,
} as any,
},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
idByNameSingular: {
testObject: testObjectMetadataId,
flatFieldMetadataMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
},
} as any,
);
const input = createUpsertInput([
@@ -458,23 +486,29 @@ describe('FieldPermissionService', () => {
});
it('should throw error when field metadata is not found', async () => {
const objectMetadataWithoutField = {
...objectMetadataItemMock,
fieldsById: {},
fieldIdByJoinColumnName: {},
fieldIdByName: {},
indexMetadatas: [],
};
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
byId: {
[testObjectMetadataId]: objectMetadataWithoutField,
flatObjectMetadataMaps: {
byId: {
[testObjectMetadataId]: {
...objectMetadataItemMock,
id: testObjectMetadataId,
fieldMetadataIds: [],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: testObjectMetadataId,
applicationId: null,
} as any,
},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
idByNameSingular: {
testObject: testObjectMetadataId,
flatFieldMetadataMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
},
} as any,
);
const input = createUpsertInput([
@@ -14,6 +14,9 @@ import {
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util';
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import {
@@ -24,7 +27,6 @@ import {
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type 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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class FieldPermissionService {
@@ -36,7 +38,7 @@ export class FieldPermissionService {
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionsRepository: Repository<FieldPermissionEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
public async upsertFieldPermissions({
@@ -60,11 +62,26 @@ export class FieldPermissionService {
role,
});
const { byId: objectMetadataMapsById } =
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
workspaceId,
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
},
);
const objectMetadataMapsById: ObjectMetadataMaps['byId'] = {};
for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) {
if (isDefined(flatObj)) {
objectMetadataMapsById[id] = buildObjectMetadataItemWithFieldMaps(
flatObj,
flatFieldMetadataMaps,
createEmptyFlatEntityMaps(),
);
}
}
const existingFieldPermissions = await this.fieldPermissionsRepository.find(
{
where: {
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
@@ -9,7 +10,6 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [
@@ -20,8 +20,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
FieldPermissionEntity,
FieldMetadataEntity,
]),
WorkspaceCacheStorageModule,
WorkspacePermissionsCacheModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [ObjectPermissionService, FieldPermissionService],
exports: [ObjectPermissionService, FieldPermissionService],
@@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
type ObjectPermissionInput,
@@ -17,7 +18,6 @@ import {
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
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';
export class ObjectPermissionService {
constructor(
@@ -28,7 +28,7 @@ export class ObjectPermissionService {
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
public async upsertObjectPermissions({
@@ -53,14 +53,17 @@ export class ObjectPermissionService {
roleWithObjectPermissions: role,
});
const { byId: objectMetadataMapsById } =
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
workspaceId,
const { flatObjectMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
input.objectPermissions.forEach((objectPermission) => {
const objectMetadataForObjectPermission =
objectMetadataMapsById[objectPermission.objectMetadataId];
flatObjectMetadataMaps.byId[objectPermission.objectMetadataId];
if (!isDefined(objectMetadataForObjectPermission)) {
throw new PermissionsException(
@@ -24,6 +24,7 @@ import { RequestLocale } from 'src/engine/decorators/locale/request-locale.decor
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
import { CreateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-permission.guard';
import { DeleteViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-permission.guard';
@@ -41,7 +42,6 @@ import {
import { ViewRestApiExceptionFilter } from 'src/engine/metadata-modules/view/filters/view-rest-api-exception.filter';
import { ViewV2Service } from 'src/engine/metadata-modules/view/services/view-v2.service';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
@Controller('rest/metadata/views')
@UseGuards(WorkspaceAuthGuard)
@@ -51,7 +51,7 @@ export class ViewController {
private readonly viewService: ViewService,
private readonly viewV2Service: ViewV2Service,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly i18nService: I18nService,
) {}
@@ -226,16 +226,20 @@ export class ViewController {
return views;
}
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{ workspaceId },
const { flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
return views.map((view) => {
let processedName = view.name;
if (view.name.includes('{objectLabelPlural}')) {
const objectMetadata = objectMetadataMaps.byId[view.objectMetadataId];
const objectMetadata =
flatObjectMetadataMaps.byId[view.objectMetadataId];
if (objectMetadata) {
const i18n = this.i18nService.getI18nInstance(locale ?? 'en');
@@ -76,7 +76,7 @@ export class ViewResolver {
workspaceId: workspace.id,
});
if (objectMetadata) {
if (isDefined(objectMetadata)) {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
const translatedObjectLabel = resolveObjectMetadataStandardOverride(
{
@@ -19,7 +19,6 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
import { ViewResolver } from 'src/engine/metadata-modules/view/resolvers/view.resolver';
import { ViewV2Service } from 'src/engine/metadata-modules/view/services/view-v2.service';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@@ -37,7 +36,6 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
FeatureFlagModule,
PermissionsModule,
UserRoleModule,
WorkspaceMetadataCacheModule,
WorkspaceCacheStorageModule,
WorkspaceMigrationV2Module,
FlatViewModule,
@@ -1,10 +0,0 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceMetadataCacheException extends CustomException<WorkspaceMetadataCacheExceptionCode> {}
export enum WorkspaceMetadataCacheExceptionCode {
OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found',
FIELD_METADATA_NOT_FOUND = 'Field Metadata not found',
FIELD_METADATA_INVALID = 'Field Metadata is invalid',
OBJECT_METADATA_COLLECTION_NOT_FOUND = 'Object Metadata collection not found',
}
@@ -1,162 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
type GetExistingOrRecomputeMetadataMapsResult = {
objectMetadataMaps: ObjectMetadataMaps;
metadataVersion: number;
};
@Injectable()
export class WorkspaceMetadataCacheService {
logger = new Logger(WorkspaceMetadataCacheService.name);
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(IndexMetadataEntity)
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
) {}
async getExistingOrRecomputeMetadataMaps({
workspaceId,
}: {
workspaceId: string;
}): Promise<GetExistingOrRecomputeMetadataMapsResult> {
const currentCacheVersion =
await this.getMetadataVersionFromCache(workspaceId);
const currentDatabaseVersion =
await this.getMetadataVersionFromDatabase(workspaceId);
if (!isDefined(currentDatabaseVersion)) {
throw new WorkspaceMetadataVersionException(
'Metadata version not found in the database',
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
const shouldRecompute =
!isDefined(currentCacheVersion) ||
currentCacheVersion !== currentDatabaseVersion;
const existingObjectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
workspaceId,
currentDatabaseVersion,
);
if (isDefined(existingObjectMetadataMaps) && !shouldRecompute) {
return {
objectMetadataMaps: existingObjectMetadataMaps,
metadataVersion: currentDatabaseVersion,
};
}
const { objectMetadataMaps, metadataVersion } =
await this.recomputeMetadataCache({
workspaceId,
});
return {
objectMetadataMaps,
metadataVersion,
};
}
async recomputeMetadataCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<GetExistingOrRecomputeMetadataMapsResult> {
const currentDatabaseVersion =
await this.getMetadataVersionFromDatabase(workspaceId);
if (!isDefined(currentDatabaseVersion)) {
throw new WorkspaceMetadataVersionException(
'Metadata version not found in the database',
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
await this.workspaceCacheStorageService.flushVersionedMetadata(workspaceId);
const objectMetadataItems = await this.objectMetadataRepository.find({
where: { workspaceId },
relations: ['fields'],
});
const objectMetadataItemsIds = objectMetadataItems.map(
(objectMetadataItem) => objectMetadataItem.id,
);
const indexMetadataItems = await this.indexMetadataRepository.find({
where: { objectMetadataId: In(objectMetadataItemsIds) },
relations: ['indexFieldMetadatas'],
});
const objectMetadataItemsWithIndexMetadatas = objectMetadataItems.map(
(objectMetadataItem) => ({
...objectMetadataItem,
indexMetadatas: indexMetadataItems.filter(
(indexMetadataItem) =>
indexMetadataItem.objectMetadataId === objectMetadataItem.id,
),
}),
);
const freshObjectMetadataMaps = generateObjectMetadataMaps(
objectMetadataItemsWithIndexMetadatas,
);
await this.workspaceCacheStorageService.setObjectMetadataMaps(
workspaceId,
currentDatabaseVersion,
freshObjectMetadataMaps,
);
await this.workspaceCacheStorageService.setMetadataVersion(
workspaceId,
currentDatabaseVersion,
);
return {
objectMetadataMaps: freshObjectMetadataMaps,
metadataVersion: currentDatabaseVersion,
};
}
private async getMetadataVersionFromDatabase(
workspaceId: string,
): Promise<number | undefined> {
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
});
return workspace?.metadataVersion;
}
private async getMetadataVersionFromCache(
workspaceId: string,
): Promise<number | undefined> {
return await this.workspaceCacheStorageService.getMetadataVersion(
workspaceId,
);
}
}
@@ -1,22 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [
TypeOrmModule.forFeature([
WorkspaceEntity,
ObjectMetadataEntity,
IndexMetadataEntity,
]),
WorkspaceCacheStorageModule,
],
exports: [WorkspaceMetadataCacheService],
providers: [WorkspaceMetadataCacheService],
})
export class WorkspaceMetadataCacheModule {}
@@ -5,20 +5,22 @@ import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class WorkspaceMetadataVersionService {
logger = new Logger(WorkspaceMetadataCacheService.name);
logger = new Logger(WorkspaceMetadataVersionService.name);
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async incrementMetadataVersion(workspaceId: string): Promise<void> {
@@ -42,8 +44,18 @@ export class WorkspaceMetadataVersionService {
{ metadataVersion: newMetadataVersion },
);
await this.workspaceMetadataCacheService.recomputeMetadataCache({
await this.workspaceCacheStorageService.setMetadataVersion(
workspaceId,
newMetadataVersion,
);
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
workspaceId,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatIndexMaps',
],
});
}
}
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -10,7 +10,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
imports: [
TypeOrmModule.forFeature([WorkspaceEntity]),
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
exports: [WorkspaceMetadataVersionService],
providers: [WorkspaceMetadataVersionService],