Metadata api create entity in workspace custom app (#15911)

# Introduction
Cleaner and fewer scope version of
https://github.com/twentyhq/twenty/pull/15745 ( removed sync-metadata
hack through, too ambitious migration and upgrade )

Please note that this PR won't have any interaction with the existing
sync-metadata
Which mean that the sync metadata does not update the standard entities
applicationId and universalIdentifier, and it won't we will deprecate it
on favor of a workspace migration aka twenty-standard app installation

## API Metadata
Any operation going through the api metadata nows automatically scope
the related entity to the workspace custom application instance. (
optionally passing an applicationId to allow current hacky implem of app
sync service )

We need to either ignore the tests or remove the cli status check from
the blocking status badges for a PR to be merged

## New workspace
Already handled in previous
https://github.com/twentyhq/twenty/pull/15625, when a workspace is
created it gets created a twenty standard and custom workspace instance

All his views and permissions will be prefilled to the its twenty
standard app instance with a specific universalIdentifier

## New universalIdentifier
At the contrary as before with standardIds, universalIdentifier are
unique for a given workspace
This means that createdAt field of both object company and opportunity
will have a unique universalIdentifier whereas they share the same
standardId

## FlatApplication
Introduced the flatApplication and cache. Will migrate existing
`MetadataName` to be `SyncableMetadataName` in a following PR

## What's next
Next we will describe a twenty standard app configuration as json that
will be used to generate a workspace migration that will be run instead
of the sync metadata, in a nutshell we aim to deprecated the sync
metadata
So we can standardize any entity to have a non nullable applicationId
and universalIdentifier

## Upgrade command
Introduced an upgrade command that will create a custom workspace
instance for any workspace that do not have one in order to align with
the new behavior when creating a new workspace
This commit is contained in:
Paul Rastoin
2025-11-23 22:35:17 +01:00
committed by GitHub
parent 4848bc03f3
commit f9ab09c404
99 changed files with 3458 additions and 730 deletions
@@ -4,6 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -79,9 +80,10 @@ export class WorkspaceManagerService {
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.applicationService.createTwentyStandardApplication({
workspaceId,
});
const twentyStandardApplication =
await this.applicationService.createTwentyStandardApplication({
workspaceId,
});
// TODO later replace by twenty-standard installation aka workspaceMigration run
await this.workspaceSyncMetadataService.synchronize({
@@ -96,7 +98,18 @@ export class WorkspaceManagerService {
`Metadata creation took ${dataSourceMetadataCreationEnd - dataSourceMetadataCreationStart}ms`,
);
await this.setupDefaultRoles(workspaceId, userId);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
await this.setupDefaultRoles({
workspaceId,
userId,
workspaceCustomFlatApplication,
});
const prefillStandardObjectsStart = performance.now();
@@ -104,6 +117,7 @@ export class WorkspaceManagerService {
dataSourceMetadata,
workspaceId,
featureFlags,
twentyStandardApplication,
);
const prefillStandardObjectsEnd = performance.now();
@@ -117,6 +131,7 @@ export class WorkspaceManagerService {
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
featureFlags: Record<string, boolean>,
twentyStandardFlatApplication: FlatApplication,
) {
const createdObjectMetadata =
await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId);
@@ -128,6 +143,7 @@ export class WorkspaceManagerService {
);
await prefillCoreViews({
twentyStandardFlatApplication,
coreDataSource: this.coreDataSource,
workspaceId,
objectMetadataItems: createdObjectMetadata,
@@ -155,10 +171,15 @@ export class WorkspaceManagerService {
await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId);
}
private async setupDefaultRoles(
workspaceId: string,
userId: string,
): Promise<void> {
private async setupDefaultRoles({
userId,
workspaceId,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
userId: string;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const adminRole = await this.roleRepository.findOne({
where: {
standardId: ADMIN_ROLE.standardId,
@@ -180,6 +201,7 @@ export class WorkspaceManagerService {
const memberRole = await this.roleService.createMemberRole({
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
});
await this.workspaceRepository.update(workspaceId, {