Fix applications query cartesian product causing read timeouts (#19892)

## Summary

Same fix pattern as #19511 (`rolesPermissions` cartesian product).

The `Settings > Applications` page was hitting query read timeouts in
production. The offending SQL came from
`ApplicationService.findManyApplications` / `findOneApplication`, which
loaded **5 `OneToMany` children** in a single query via TypeORM
`relations`:

```
logicFunctions × agents × frontComponents × objects × applicationVariables
```

Postgres returns the Cartesian product of all five — e.g. 20 logic
functions × 5 agents × 30 front components × 100 objects × 10 variables
= **3M rows for ~165 distinct records**, which trivially exceeds the
read timeout.

## Changes

- **`findManyApplications`** — dropped all `OneToMany` relations. The
frontend `FIND_MANY_APPLICATIONS` query only selects scalar fields and
the `applicationRegistration` ManyToOne, so joining the children was
pure waste at the list level.
- **`findOneApplication`** — kept the cheap `ManyToOne` / `OneToOne`
joins (`packageJsonFile`, `yarnLockFile`, `applicationRegistration`) on
the main query and fetched the 5 `OneToMany` children in parallel via
`Promise.all`, reattaching them on the entity. Same shape as
`WorkspaceRolesPermissionsCacheService.computeForCache` after #19511.
- **`application.module.ts`** — registered the 5 child entity
repositories via `TypeOrmModule.forFeature`.

The other internal caller (`front-component.service.ts →
findOneApplicationOrThrow`) only reads
`application.universalIdentifier`, so the extra parallel single-key
lookups remain far cheaper than the previous 8-way join with row
explosion.
This commit is contained in:
Charles Bochet
2026-04-20 19:04:10 +02:00
committed by GitHub
parent 27e1caf9cc
commit 9a95cd02ed
2 changed files with 68 additions and 22 deletions
@@ -4,15 +4,28 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceFlatApplicationMapCacheService } from 'src/engine/core-modules/application/workspace-flat-application-map-cache.service';
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
TypeOrmModule.forFeature([ApplicationEntity, WorkspaceEntity]),
TypeOrmModule.forFeature([
ApplicationEntity,
WorkspaceEntity,
LogicFunctionEntity,
AgentEntity,
FrontComponentEntity,
ObjectMetadataEntity,
ApplicationVariableEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceCacheModule,
TwentyConfigModule,
@@ -12,10 +12,15 @@ import {
} from 'src/engine/core-modules/application/application.exception';
import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application/application-package/utils/get-default-application-package-fields.util';
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/application-package/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
@@ -28,6 +33,16 @@ export class ApplicationService {
private readonly fileStorageService: FileStorageService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(FrontComponentEntity)
private readonly frontComponentRepository: Repository<FrontComponentEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(ApplicationVariableEntity)
private readonly applicationVariableRepository: Repository<ApplicationVariableEntity>,
) {}
async findApplicationRoleId(
@@ -119,16 +134,7 @@ export class ApplicationService {
): Promise<ApplicationEntity[]> {
return this.applicationRepository.find({
where: { workspaceId },
relations: [
'logicFunctions',
'agents',
'frontComponents',
'objects',
'applicationVariables',
'packageJsonFile',
'yarnLockFile',
'applicationRegistration',
],
relations: ['applicationRegistration'],
});
}
@@ -153,19 +159,46 @@ export class ApplicationService {
...(isDefined(id) ? { id } : { universalIdentifier }),
};
return await this.applicationRepository.findOne({
const application = await this.applicationRepository.findOne({
where,
relations: [
'logicFunctions',
'agents',
'frontComponents',
'objects',
'applicationVariables',
'packageJsonFile',
'yarnLockFile',
'applicationRegistration',
],
relations: ['packageJsonFile', 'yarnLockFile', 'applicationRegistration'],
});
if (!isDefined(application)) {
return null;
}
const [
logicFunctions,
agents,
frontComponents,
objects,
applicationVariables,
] = await Promise.all([
this.logicFunctionRepository.find({
where: { applicationId: application.id, workspaceId },
}),
this.agentRepository.find({
where: { applicationId: application.id, workspaceId },
}),
this.frontComponentRepository.find({
where: { applicationId: application.id, workspaceId },
}),
this.objectMetadataRepository.find({
where: { applicationId: application.id, workspaceId },
}),
this.applicationVariableRepository.find({
where: { applicationId: application.id, workspaceId },
}),
]);
application.logicFunctions = logicFunctions;
application.agents = agents;
application.frontComponents = frontComponents;
application.objects = objects;
application.applicationVariables = applicationVariables;
return application;
}
async findOneApplicationOrThrow({