Remove DataSourceService and clean up datasource migration logic (#19532)

## Summary

- **Drop the `objectMetadata.dataSourceId` foreign key and index** via a
1-22 fast instance command — column kept nullable for data preservation
- **Delete `DataSourceService`, `DataSourceModule`, and
`DataSourceException`** — all code now uses `workspace.databaseSchema`
directly
- **Remove `IS_DATASOURCE_MIGRATED` feature flag** from default flags
and all branching logic
- **Simplify workspace/object creation pipelines** —
`WorkspaceManagerService`, `DevSeederService`, and the object creation
action handler no longer route through `DataSourceService`
- **Keep `DataSourceEntity` and the `dataSource` table** for historical
data — entity stripped of all ORM relations
This commit is contained in:
Charles Bochet
2026-04-10 09:34:05 +02:00
committed by GitHub
parent 4fd721a3c9
commit f6423f5925
55 changed files with 75 additions and 388 deletions
@@ -4,19 +4,16 @@ import {
type DataSourceOptions,
Entity,
Index,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
export type DataSourceType = DataSourceOptions['type'];
// @deprecated - This entity is being deprecated in favor of storing
// databaseSchema directly on WorkspaceEntity.
// During the transition, writes go to both tables (dual-write).
// @deprecated - This entity is kept only to preserve the dataSource table.
// All code should use workspace.databaseSchema instead.
@Entity('dataSource')
@Index('IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT', ['workspaceId', 'createdAt'])
export class DataSourceEntity extends WorkspaceRelatedEntity {
@@ -38,11 +35,6 @@ export class DataSourceEntity extends WorkspaceRelatedEntity {
@Column({ default: false })
isRemote: boolean;
@OneToMany(() => ObjectMetadataEntity, (object) => object.dataSource, {
cascade: true,
})
objects: ObjectMetadataEntity[];
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -1,33 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { CustomException } from 'src/utils/custom-exception';
export enum DataSourceExceptionCode {
DATA_SOURCE_NOT_FOUND = 'DATA_SOURCE_NOT_FOUND',
}
const getDataSourceExceptionUserFriendlyMessage = (
code: DataSourceExceptionCode,
) => {
switch (code) {
case DataSourceExceptionCode.DATA_SOURCE_NOT_FOUND:
return STANDARD_ERROR_MESSAGE;
default:
assertUnreachable(code);
}
};
export class DataSourceException extends CustomException<DataSourceExceptionCode> {
constructor(
message: string,
code: DataSourceExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ?? getDataSourceExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from './data-source.entity';
import { DataSourceService } from './data-source.service';
@Module({
imports: [TypeOrmModule.forFeature([DataSourceEntity, WorkspaceEntity])],
providers: [DataSourceService],
exports: [DataSourceService],
})
export class DataSourceModule {}
@@ -1,106 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type FindManyOptions, Repository } from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import {
DataSourceException,
DataSourceExceptionCode,
} from 'src/engine/metadata-modules/data-source/data-source.exception';
import { DataSourceEntity } from './data-source.entity';
// @deprecated - This service is being deprecated. During the transition,
// writes go to both the dataSource table and workspace table (dual-write).
// Reads should progressively migrate to use workspace.databaseSchema
// or the deterministic getWorkspaceSchemaName(workspaceId) utility.
@Injectable()
export class DataSourceService {
constructor(
@InjectRepository(DataSourceEntity)
private readonly dataSourceMetadataRepository: Repository<DataSourceEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
async createDataSourceMetadata(
workspaceId: string,
workspaceSchema: string,
): Promise<DataSourceEntity> {
const dataSource = await this.dataSourceMetadataRepository.findOne({
where: { workspaceId },
});
// Dual-write: always keep workspace.databaseSchema in sync
await this.workspaceRepository.update(workspaceId, {
databaseSchema: workspaceSchema,
});
if (dataSource) {
return dataSource;
}
return this.dataSourceMetadataRepository.save({
workspaceId,
schema: workspaceSchema,
});
}
// @deprecated - Use workspace.activationStatus or workspace.databaseSchema
// to check if a workspace has been initialized instead.
async getManyDataSourceMetadata(
options: FindManyOptions<DataSourceEntity> = {},
): Promise<DataSourceEntity[]> {
return this.dataSourceMetadataRepository.find(options);
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getDataSourcesMetadataFromWorkspaceId(
workspaceId: string,
): Promise<DataSourceEntity[]> {
return this.dataSourceMetadataRepository.find({
where: { workspaceId },
order: { createdAt: 'DESC' },
});
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getLastDataSourceMetadataFromWorkspaceId(
workspaceId: string,
): Promise<DataSourceEntity | null> {
return this.dataSourceMetadataRepository.findOne({
where: { workspaceId },
order: { createdAt: 'DESC' },
});
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId: string,
): Promise<DataSourceEntity> {
try {
return this.dataSourceMetadataRepository.findOneOrFail({
where: { workspaceId },
order: { createdAt: 'DESC' },
});
} catch (error) {
throw new DataSourceException(
`Data source not found for workspace ${workspaceId}: ${error}`,
DataSourceExceptionCode.DATA_SOURCE_NOT_FOUND,
);
}
}
async delete(workspaceId: string): Promise<void> {
await this.dataSourceMetadataRepository.delete({ workspaceId });
// Dual-write: clear workspace.databaseSchema on delete
await this.workspaceRepository.update(workspaceId, {
databaseSchema: null,
});
}
}
@@ -12,7 +12,6 @@ import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver';
import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor';
@@ -49,7 +48,6 @@ import { UpdateFieldInput } from './dtos/update-field.input';
WorkspaceMetadataVersionModule,
WorkspaceCacheStorageModule,
ObjectMetadataModule,
DataSourceModule,
TypeORMModule,
ActorModule,
ApplicationModule,
@@ -79,7 +79,6 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
},
},
objectMetadata: {
dataSource: null,
workspace: null,
application: null,
},
@@ -139,7 +139,6 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
},
},
objectMetadata: {
dataSource: null,
workspace: null,
application: null,
},
@@ -8,7 +8,6 @@ import { AiGenerateTextModule } from 'src/engine/metadata-modules/ai/ai-generate
import { CalendarChannelMetadataModule } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module';
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { FlatEntityMapsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/flat-entity/filters/flat-entity-maps-graphql-api-exception.filter';
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
@@ -30,7 +29,6 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
@Module({
imports: [
DataSourceModule,
FieldMetadataModule,
FrontComponentModule,
ObjectMetadataModule,
@@ -63,7 +61,6 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
},
],
exports: [
DataSourceModule,
FieldMetadataModule,
FrontComponentModule,
ObjectMetadataModule,
@@ -60,9 +60,6 @@ export class CreateObjectInput {
@Field({ nullable: true })
color?: string;
@HideField()
dataSourceId: string;
@HideField()
applicationId?: string;
@@ -2,8 +2,6 @@ import {
Column,
CreateDateColumn,
Entity,
Index,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
type Relation,
@@ -12,7 +10,6 @@ import {
} from 'typeorm';
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
@@ -31,7 +28,6 @@ import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migra
'namePlural',
'workspaceId',
])
@Index('IDX_OBJECT_METADATA_DATA_SOURCE_ID', ['dataSourceId'])
export class ObjectMetadataEntity
extends SyncableEntity
implements Required<ObjectMetadataEntity>
@@ -39,7 +35,8 @@ export class ObjectMetadataEntity
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
// @deprecated - FK dropped, column kept for data preservation only
@Column({ nullable: true, type: 'uuid' })
dataSourceId: string;
@Column({ nullable: false })
@@ -120,11 +117,6 @@ export class ObjectMetadataEntity
})
indexMetadatas: Relation<IndexMetadataEntity[]>;
@ManyToOne(() => DataSourceEntity, (dataSource) => dataSource.objects, {
onDelete: 'CASCADE',
})
dataSource: Relation<DataSourceEntity>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -13,7 +13,6 @@ import { ApplicationModule } from 'src/engine/core-modules/application/applicati
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
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 { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
@@ -50,7 +49,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
]),
TypeOrmModule.forFeature([FeatureFlagEntity, ViewEntity]),
ApplicationModule,
DataSourceModule,
WorkspaceMetadataVersionModule,
IndexMetadataModule,
PermissionsModule,