From 14dacd8d358522a75e4039f808e790c8b14923fb Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:04:22 +0200
Subject: [PATCH] `[Slow db query]` Resolve applicationId from cache in
FileStorageService (#22870)
## Context
Sentry flagged a recurring slow DB query (TWENTY-SERVER-HZJ): `SELECT
... FROM core.application WHERE workspaceId = $1 AND universalIdentifier
= $2 AND deletedAt IS NULL LIMIT 1`, emitted on every file write under
`POST /graphql` (record avatar/file fields) and `POST /metadata`.
`FileStorageService` re-resolved the owning application row from
`core.application` by `(workspaceId, universalIdentifier)` on every file
write, uncached and synchronously in the request path. The row was only
used to recover `application.id`. The workspace cache already exposes
this mapping via `flatApplicationMaps.idByUniversalIdentifier`.
Closes twentyhq/core-team-issues#2668.
## Changes
- Injected `WorkspaceCacheService` into `FileStorageService` in place of
the `ApplicationEntity` repository.
- Added `resolveApplicationIdOrThrow`: resolves `applicationId` from
`flatApplicationMaps.idByUniversalIdentifier` on the normal
(already-committed) path, throwing
`FileStorageException(FILE_NOT_FOUND)` on a cache miss. When a
`queryRunner` is provided (application-creating transactions, where the
freshly created row is not yet in cache), it keeps the DB read through
`queryRunner.manager` so it can see uncommitted rows.
- Added `resolveApplicationUniversalIdentifierOrThrow` for the by-id
lookup in `deleteByFileId`, resolved from `flatApplicationMaps.byId`.
- Applied the cache path to `writeFile`, `createPendingFile`,
`deleteFile`, `deleteFolder`, and `deleteByFileId`. Only `writeFile`
carries a `queryRunner`; the others never do.
- Updated `FileStorageModule` to import `WorkspaceCacheModule` and drop
the now-unused `ApplicationEntity` repository registration.
No migration needed: a partial unique composite index on
`(universalIdentifier, workspaceId) WHERE deletedAt IS NULL AND
universalIdentifier IS NOT NULL` already exists on `ApplicationEntity`
and covers the query.
## Tests
Extended `file-storage.service.spec.ts`:
- cache hit resolves `applicationId` without a DB call,
- cache miss throws `FILE_NOT_FOUND`,
- the `queryRunner` path still reads from the DB and skips the cache.
All 95 file-storage unit tests pass; typecheck, oxlint, and oxfmt are
clean on the touched files.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018GUrJ26xvZpjtrGGev9jsk)_
---
.../application/application.service.ts | 2 +
...find-active-flat-application-by-id.util.ts | 17 ++
...pplication-by-universal-identifier.util.ts | 17 ++
.../file-storage/file-storage.module.ts | 5 +-
.../__tests__/file-storage.service.spec.ts | 203 ++++++++++++++++--
.../services/file-storage.service.ts | 152 +++++++++----
.../utils/setup-application-for-sync.util.ts | 98 +++++----
7 files changed, 386 insertions(+), 108 deletions(-)
create mode 100644 packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-id.util.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-universal-identifier.util.ts
diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts
index 2326c62042..929ad92b89 100644
--- a/packages/twenty-server/src/engine/core-modules/application/application.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts
@@ -492,6 +492,7 @@ export class ApplicationService {
sourceFile: defaultPackageFields.packageJsonContent,
fileFolder: FileFolder.Dependencies,
applicationUniversalIdentifier: application.universalIdentifier,
+ applicationId: application.id,
workspaceId: application.workspaceId,
resourcePath: 'package.json',
settings: { isTemporaryFile: false, toDelete: false },
@@ -502,6 +503,7 @@ export class ApplicationService {
sourceFile: defaultPackageFields.yarnLockContent,
fileFolder: FileFolder.Dependencies,
applicationUniversalIdentifier: application.universalIdentifier,
+ applicationId: application.id,
workspaceId: application.workspaceId,
resourcePath: 'yarn.lock',
settings: { isTemporaryFile: false, toDelete: false },
diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-id.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-id.util.ts
new file mode 100644
index 0000000000..877906aa6c
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-id.util.ts
@@ -0,0 +1,17 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
+import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
+
+export const findActiveFlatApplicationById = (
+ flatApplicationMaps: FlatApplicationCacheMaps,
+ applicationId: string,
+): FlatApplication | undefined => {
+ const application = flatApplicationMaps.byId[applicationId];
+
+ if (!isDefined(application) || isDefined(application.deletedAt)) {
+ return undefined;
+ }
+
+ return application;
+};
diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-universal-identifier.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-universal-identifier.util.ts
new file mode 100644
index 0000000000..fa5fd1fbd3
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/utils/find-active-flat-application-by-universal-identifier.util.ts
@@ -0,0 +1,17 @@
+import { isDefined } from 'twenty-shared/utils';
+
+import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
+import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
+import { findActiveFlatApplicationById } from 'src/engine/core-modules/application/utils/find-active-flat-application-by-id.util';
+
+export const findActiveFlatApplicationByUniversalIdentifier = (
+ flatApplicationMaps: FlatApplicationCacheMaps,
+ applicationUniversalIdentifier: string,
+): FlatApplication | undefined => {
+ const applicationId =
+ flatApplicationMaps.idByUniversalIdentifier[applicationUniversalIdentifier];
+
+ return isDefined(applicationId)
+ ? findActiveFlatApplicationById(flatApplicationMaps, applicationId)
+ : undefined;
+};
diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.module.ts b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.module.ts
index 9cb2d3a638..1445524ef6 100644
--- a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.module.ts
@@ -2,7 +2,6 @@ import { type DynamicModule, Global } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
-import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageExceptionFilter } from 'src/engine/core-modules/file-storage/file-storage-exception-filter';
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
import { FileStorageService } from 'src/engine/core-modules/file-storage/services/file-storage.service';
@@ -10,6 +9,7 @@ import { ServerFileStorageService } from 'src/engine/core-modules/file-storage/s
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository';
+import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Global()
export class FileStorageModule {
static forRoot(): DynamicModule {
@@ -17,7 +17,8 @@ export class FileStorageModule {
module: FileStorageModule,
imports: [
TwentyConfigModule,
- TypeOrmModule.forFeature([FileEntity, ApplicationEntity]),
+ TypeOrmModule.forFeature([FileEntity]),
+ WorkspaceCacheModule,
],
providers: [
FileStorageDriverFactory,
diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/services/__tests__/file-storage.service.spec.ts b/packages/twenty-server/src/engine/core-modules/file-storage/services/__tests__/file-storage.service.spec.ts
index c34fb9eec0..6a477d9467 100644
--- a/packages/twenty-server/src/engine/core-modules/file-storage/services/__tests__/file-storage.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/file-storage/services/__tests__/file-storage.service.spec.ts
@@ -1,5 +1,4 @@
import { Test, type TestingModule } from '@nestjs/testing';
-import { getRepositoryToken } from '@nestjs/typeorm';
import { FileFolder } from 'twenty-shared/types';
import {
@@ -7,12 +6,12 @@ import {
eachTestingContextFilter,
} from 'twenty-shared/testing';
-import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
import { FileStorageService } from 'src/engine/core-modules/file-storage/services/file-storage.service';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { getWorkspaceScopedRepositoryToken } from 'src/engine/twenty-orm/workspace-scoped-repository/get-workspace-scoped-repository-token.util';
+import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
describe('FileStorageService', () => {
let service: FileStorageService;
let fileStorageDriverFactory: FileStorageDriverFactory;
@@ -26,10 +25,11 @@ describe('FileStorageService', () => {
upsertAndReturnOne: jest.fn(),
findOneOrFail: jest.fn(),
delete: jest.fn(),
+ withManager: jest.fn(),
};
- const mockApplicationRepository = {
- findOneOrFail: jest.fn(),
+ const mockWorkspaceCacheService = {
+ getOrRecompute: jest.fn(),
};
beforeEach(async () => {
@@ -45,8 +45,8 @@ describe('FileStorageService', () => {
useValue: mockFileRepository,
},
{
- provide: getRepositoryToken(ApplicationEntity),
- useValue: mockApplicationRepository,
+ provide: WorkspaceCacheService,
+ useValue: mockWorkspaceCacheService,
},
],
}).compile();
@@ -123,10 +123,6 @@ describe('FileStorageService', () => {
'https://s3.example.com/signed',
);
- mockApplicationRepository.findOneOrFail.mockResolvedValue({
- universalIdentifier: 'app-uid',
- });
-
const result = await service.getPresignedUrl({
resourcePath: 'file.txt',
fileFolder: 'workflow' as any,
@@ -143,10 +139,6 @@ describe('FileStorageService', () => {
it('should return null when driver returns null', async () => {
mockDriver.getPresignedUrl.mockResolvedValue(null);
- mockApplicationRepository.findOneOrFail.mockResolvedValue({
- universalIdentifier: 'app-uid',
- });
-
const result = await service.getPresignedUrl({
resourcePath: 'file.txt',
fileFolder: 'workflow' as any,
@@ -175,9 +167,13 @@ describe('FileStorageService', () => {
};
mockFileStorageDriverFactory.getCurrentDriver.mockReturnValue(mockDriver);
- mockApplicationRepository.findOneOrFail.mockResolvedValue({
- id: 'app-id',
- universalIdentifier: 'app-uid',
+ mockWorkspaceCacheService.getOrRecompute.mockResolvedValue({
+ flatApplicationMaps: {
+ byId: {
+ 'app-id': { id: 'app-id', universalIdentifier: 'app-456' },
+ },
+ idByUniversalIdentifier: { 'app-456': 'app-id' },
+ },
});
mockFileRepository.upsertAndReturnOne.mockResolvedValue({
id: 'file-id',
@@ -409,6 +405,174 @@ describe('FileStorageService', () => {
);
});
+ describe('applicationId resolution', () => {
+ it('should resolve applicationId from the workspace cache by universalIdentifier', async () => {
+ await service.writeFile({
+ ...validResourceIdentifier,
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ });
+
+ expect(mockWorkspaceCacheService.getOrRecompute).toHaveBeenCalledWith(
+ 'workspace-123',
+ ['flatApplicationMaps'],
+ );
+ expect(mockFileRepository.upsertAndReturnOne).toHaveBeenCalledWith(
+ 'workspace-123',
+ expect.objectContaining({ applicationId: 'app-id' }),
+ ['path', 'workspaceId', 'applicationId'],
+ );
+ });
+
+ it('should throw FILE_NOT_FOUND when the application is not in the cache', async () => {
+ mockWorkspaceCacheService.getOrRecompute.mockResolvedValueOnce({
+ flatApplicationMaps: { byId: {}, idByUniversalIdentifier: {} },
+ });
+
+ await expect(
+ service.writeFile({
+ ...validResourceIdentifier,
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ }),
+ ).rejects.toMatchObject({
+ code: FileStorageExceptionCode.FILE_NOT_FOUND,
+ });
+
+ expect(mockDriver.writeFile).not.toHaveBeenCalled();
+ });
+
+ it('should throw FILE_NOT_FOUND when the cached application is soft-deleted', async () => {
+ mockWorkspaceCacheService.getOrRecompute.mockResolvedValueOnce({
+ flatApplicationMaps: {
+ byId: {
+ 'app-id': {
+ id: 'app-id',
+ universalIdentifier: 'app-456',
+ deletedAt: new Date(),
+ },
+ },
+ idByUniversalIdentifier: { 'app-456': 'app-id' },
+ },
+ });
+
+ await expect(
+ service.writeFile({
+ ...validResourceIdentifier,
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ }),
+ ).rejects.toMatchObject({
+ code: FileStorageExceptionCode.FILE_NOT_FOUND,
+ });
+
+ expect(mockDriver.writeFile).not.toHaveBeenCalled();
+ });
+
+ it('should use the passed applicationId without consulting the cache', async () => {
+ await service.writeFile({
+ ...validResourceIdentifier,
+ applicationId: 'passed-app-id',
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ });
+
+ expect(
+ mockWorkspaceCacheService.getOrRecompute,
+ ).not.toHaveBeenCalled();
+ expect(mockFileRepository.upsertAndReturnOne).toHaveBeenCalledWith(
+ 'workspace-123',
+ expect.objectContaining({ applicationId: 'passed-app-id' }),
+ ['path', 'workspaceId', 'applicationId'],
+ );
+ });
+
+ it('should use the passed applicationId and write through the queryRunner without consulting the cache', async () => {
+ const queryRunner = { manager: {} };
+
+ mockFileRepository.withManager.mockReturnValue(mockFileRepository);
+
+ await service.writeFile({
+ ...validResourceIdentifier,
+ applicationId: 'passed-app-id',
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ queryRunner: queryRunner as any,
+ });
+
+ expect(mockFileRepository.withManager).toHaveBeenCalledWith(
+ queryRunner.manager,
+ );
+ expect(
+ mockWorkspaceCacheService.getOrRecompute,
+ ).not.toHaveBeenCalled();
+ expect(mockFileRepository.upsertAndReturnOne).toHaveBeenCalledWith(
+ 'workspace-123',
+ expect.objectContaining({ applicationId: 'passed-app-id' }),
+ ['path', 'workspaceId', 'applicationId'],
+ );
+ });
+
+ it('should fall back to a queryRunner DB read when the application is not yet in the cache', async () => {
+ mockWorkspaceCacheService.getOrRecompute.mockResolvedValueOnce({
+ flatApplicationMaps: { byId: {}, idByUniversalIdentifier: {} },
+ });
+
+ const findOne = jest.fn().mockResolvedValue({ id: 'uncommitted-id' });
+ const queryRunner = {
+ manager: { getRepository: jest.fn().mockReturnValue({ findOne }) },
+ };
+
+ mockFileRepository.withManager.mockReturnValue(mockFileRepository);
+
+ await service.writeFile({
+ ...validResourceIdentifier,
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ queryRunner: queryRunner as any,
+ });
+
+ expect(mockWorkspaceCacheService.getOrRecompute).toHaveBeenCalled();
+ expect(findOne).toHaveBeenCalledWith({
+ where: {
+ universalIdentifier: 'app-456',
+ workspaceId: 'workspace-123',
+ },
+ });
+ expect(mockFileRepository.upsertAndReturnOne).toHaveBeenCalledWith(
+ 'workspace-123',
+ expect.objectContaining({ applicationId: 'uncommitted-id' }),
+ ['path', 'workspaceId', 'applicationId'],
+ );
+ });
+
+ it('should throw FILE_NOT_FOUND when the application is missing from both the cache and the queryRunner DB read', async () => {
+ mockWorkspaceCacheService.getOrRecompute.mockResolvedValueOnce({
+ flatApplicationMaps: { byId: {}, idByUniversalIdentifier: {} },
+ });
+
+ const findOne = jest.fn().mockResolvedValue(null);
+ const queryRunner = {
+ manager: { getRepository: jest.fn().mockReturnValue({ findOne }) },
+ };
+
+ mockFileRepository.withManager.mockReturnValue(mockFileRepository);
+
+ await expect(
+ service.writeFile({
+ ...validResourceIdentifier,
+ sourceFile: Buffer.from('valid content'),
+ settings: { isTemporaryFile: false, toDelete: false },
+ queryRunner: queryRunner as any,
+ }),
+ ).rejects.toMatchObject({
+ code: FileStorageExceptionCode.FILE_NOT_FOUND,
+ });
+
+ expect(mockDriver.writeFile).not.toHaveBeenCalled();
+ });
+ });
+
describe('magic-byte backstop', () => {
const pngBuffer = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
@@ -696,11 +860,6 @@ describe('FileStorageService', () => {
workspaceId: 'workspace-123',
});
- mockApplicationRepository.findOneOrFail.mockResolvedValue({
- id: 'app-id',
- universalIdentifier: 'app-456',
- });
-
await service.deleteByFileId({
fileId: 'file-id',
workspaceId: 'workspace-123',
diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/services/file-storage.service.ts b/packages/twenty-server/src/engine/core-modules/file-storage/services/file-storage.service.ts
index aa59d44253..3d46698d85 100644
--- a/packages/twenty-server/src/engine/core-modules/file-storage/services/file-storage.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/file-storage/services/file-storage.service.ts
@@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
import { basename, dirname, join } from 'path';
import { type Readable } from 'stream';
import { FileFolder } from 'twenty-shared/types';
-import { Like, Repository, type QueryRunner } from 'typeorm';
+import { isDefined } from 'twenty-shared/utils';
+import { Like, type QueryRunner } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
+import { findActiveFlatApplicationById } from 'src/engine/core-modules/application/utils/find-active-flat-application-by-id.util';
+import { findActiveFlatApplicationByUniversalIdentifier } from 'src/engine/core-modules/application/utils/find-active-flat-application-by-universal-identifier.util';
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
import {
FileStorageException,
@@ -23,6 +25,7 @@ import { FILE_STATUS } from 'src/engine/core-modules/file/types/file-status.type
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
+import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
export type ResourceIdentifier = {
workspaceId: string;
applicationUniversalIdentifier: string;
@@ -36,10 +39,80 @@ export class FileStorageService {
private readonly fileStorageDriverFactory: FileStorageDriverFactory,
@InjectWorkspaceScopedRepository(FileEntity)
private readonly fileRepository: WorkspaceScopedRepository,
- @InjectRepository(ApplicationEntity)
- private readonly applicationRepository: Repository,
+ private readonly workspaceCacheService: WorkspaceCacheService,
) {}
+ private async resolveApplicationIdOrThrow({
+ applicationUniversalIdentifier,
+ workspaceId,
+ queryRunner,
+ }: {
+ applicationUniversalIdentifier: string;
+ workspaceId: string;
+ queryRunner?: QueryRunner;
+ }): Promise {
+ const { flatApplicationMaps } =
+ await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'flatApplicationMaps',
+ ]);
+
+ const cachedApplication = findActiveFlatApplicationByUniversalIdentifier(
+ flatApplicationMaps,
+ applicationUniversalIdentifier,
+ );
+
+ if (isDefined(cachedApplication)) {
+ return cachedApplication.id;
+ }
+
+ if (isDefined(queryRunner)) {
+ const application = await queryRunner.manager
+ .getRepository(ApplicationEntity)
+ .findOne({
+ where: {
+ universalIdentifier: applicationUniversalIdentifier,
+ workspaceId,
+ },
+ });
+
+ if (isDefined(application)) {
+ return application.id;
+ }
+ }
+
+ throw new FileStorageException(
+ `Application with universalIdentifier "${applicationUniversalIdentifier}" not found`,
+ FileStorageExceptionCode.FILE_NOT_FOUND,
+ );
+ }
+
+ private async resolveApplicationUniversalIdentifierOrThrow({
+ applicationId,
+ workspaceId,
+ }: {
+ applicationId: string;
+ workspaceId: string;
+ }): Promise {
+ const { flatApplicationMaps } =
+ await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'flatApplicationMaps',
+ ]);
+
+ const application = findActiveFlatApplicationById(
+ flatApplicationMaps,
+ applicationId,
+ );
+
+ if (!isDefined(application)) {
+ throw new FileStorageException(
+ `Application with id "${applicationId}" not found`,
+ FileStorageExceptionCode.FILE_NOT_FOUND,
+ );
+ }
+
+ return application.universalIdentifier;
+ }
+
private buildStoragePathWithinWorkspaceOrThrow({
workspaceId,
applicationUniversalIdentifier,
@@ -124,6 +197,7 @@ export class FileStorageService {
sourceFile,
fileFolder,
applicationUniversalIdentifier,
+ applicationId,
workspaceId,
resourcePath,
fileId,
@@ -131,26 +205,25 @@ export class FileStorageService {
queryRunner,
}: ResourceIdentifier & {
sourceFile: string | Buffer | Uint8Array;
+ applicationId?: string;
fileId?: string;
settings: FileSettings;
queryRunner?: QueryRunner;
}): Promise {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
- const applicationRepository = queryRunner
- ? queryRunner.manager.getRepository(ApplicationEntity)
- : this.applicationRepository;
- const fileRepository = queryRunner
+ const resolvedApplicationId =
+ applicationId ??
+ (await this.resolveApplicationIdOrThrow({
+ applicationUniversalIdentifier,
+ workspaceId,
+ queryRunner,
+ }));
+
+ const fileRepository = isDefined(queryRunner)
? this.fileRepository.withManager(queryRunner.manager)
: this.fileRepository;
- const application = await applicationRepository.findOneOrFail({
- where: {
- universalIdentifier: applicationUniversalIdentifier,
- workspaceId,
- },
- });
-
const { onStorageFilePath, filePath } =
this.validateAndBuildFileStoragePathOrThrow({
workspaceId,
@@ -175,7 +248,7 @@ export class FileStorageService {
workspaceId,
{
path: filePath,
- applicationId: application.id,
+ applicationId: resolvedApplicationId,
id: fileId,
mimeType,
size:
@@ -206,11 +279,9 @@ export class FileStorageService {
mimeType: string;
settings: FileSettings;
}): Promise {
- const application = await this.applicationRepository.findOneOrFail({
- where: {
- universalIdentifier: applicationUniversalIdentifier,
- workspaceId,
- },
+ const applicationId = await this.resolveApplicationIdOrThrow({
+ applicationUniversalIdentifier,
+ workspaceId,
});
const { filePath } = this.validateAndBuildFileStoragePathOrThrow({
@@ -224,7 +295,7 @@ export class FileStorageService {
workspaceId,
{
path: filePath,
- applicationId: application.id,
+ applicationId,
id: fileId,
mimeType,
size,
@@ -354,7 +425,9 @@ export class FileStorageService {
});
}
- async deleteFile(params: ResourceIdentifier): Promise {
+ async deleteFile(
+ params: ResourceIdentifier & { applicationId?: string },
+ ): Promise {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
const { onStorageFilePath, filePath } =
this.validateAndBuildFileStoragePathOrThrow(params);
@@ -364,16 +437,16 @@ export class FileStorageService {
filename: basename(onStorageFilePath),
});
- const application = await this.applicationRepository.findOneOrFail({
- where: {
- universalIdentifier: params.applicationUniversalIdentifier,
+ const applicationId =
+ params.applicationId ??
+ (await this.resolveApplicationIdOrThrow({
+ applicationUniversalIdentifier: params.applicationUniversalIdentifier,
workspaceId: params.workspaceId,
- },
- });
+ }));
await this.fileRepository.delete(params.workspaceId, {
path: filePath,
- applicationId: application.id,
+ applicationId,
});
}
@@ -399,16 +472,14 @@ export class FileStorageService {
await driver.delete({ folderPath: onStorageFolderPath });
- const application = await this.applicationRepository.findOneOrFail({
- where: {
- universalIdentifier: applicationUniversalIdentifier,
- workspaceId,
- },
+ const applicationId = await this.resolveApplicationIdOrThrow({
+ applicationUniversalIdentifier,
+ workspaceId,
});
await this.fileRepository.delete(workspaceId, {
path: Like(`${validatedFolderPath}%`),
- applicationId: application.id,
+ applicationId,
});
}
@@ -428,13 +499,16 @@ export class FileStorageService {
},
});
- const application = await this.applicationRepository.findOneOrFail({
- where: { id: file.applicationId, workspaceId },
- });
+ const applicationUniversalIdentifier =
+ await this.resolveApplicationUniversalIdentifierOrThrow({
+ applicationId: file.applicationId,
+ workspaceId,
+ });
await this.deleteFile({
workspaceId,
- applicationUniversalIdentifier: application.universalIdentifier,
+ applicationUniversalIdentifier,
+ applicationId: file.applicationId,
fileFolder,
resourcePath: removeFileFolderFromFileEntityPath(file.path),
});
diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/setup-application-for-sync.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/setup-application-for-sync.util.ts
index 22c5a6e327..7f3031967c 100644
--- a/packages/twenty-server/test/integration/metadata/suites/application/utils/setup-application-for-sync.util.ts
+++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/setup-application-for-sync.util.ts
@@ -1,15 +1,12 @@
-import crypto from 'crypto';
+import gql from 'graphql-tag';
+import { isDefined } from 'twenty-shared/utils';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
-
-import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
-
-const TEST_WORKSPACE_ID = SEED_APPLE_WORKSPACE_ID;
+import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const setupApplicationForSync = async ({
applicationUniversalIdentifier,
name,
- description,
sourcePath,
}: {
applicationUniversalIdentifier: string;
@@ -17,47 +14,58 @@ export const setupApplicationForSync = async ({
description: string;
sourcePath: string;
}) => {
- const registrationId = crypto.randomUUID();
- const applicationId = crypto.randomUUID();
- const oAuthClientId = crypto.randomUUID();
-
- await globalThis.testDataSource.query(
- `INSERT INTO core."applicationRegistration"
- (id, "universalIdentifier", name, "oAuthClientId",
- "oAuthRedirectUris", "oAuthScopes", "workspaceId", "sourceType")
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
- [
- registrationId,
- applicationUniversalIdentifier,
- name,
- oAuthClientId,
- [],
- [],
- TEST_WORKSPACE_ID,
- 'local',
- ],
- );
-
- await globalThis.testDataSource.query(
- `INSERT INTO core."application"
- (id, "universalIdentifier", name, description, version, "sourcePath",
- "sourceType", "workspaceId", "applicationRegistrationId")
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
- [
- applicationId,
- applicationUniversalIdentifier,
- name,
- description,
- '1.0.0',
- sourcePath,
- 'local',
- TEST_WORKSPACE_ID,
- registrationId,
- ],
- );
-
jest.useRealTimers();
+ const registrationResponse = await makeMetadataAPIRequest({
+ query: gql`
+ mutation CreateApplicationRegistration(
+ $input: CreateApplicationRegistrationInput!
+ ) {
+ createApplicationRegistration(input: $input) {
+ applicationRegistration {
+ id
+ }
+ }
+ }
+ `,
+ variables: {
+ input: { name, universalIdentifier: applicationUniversalIdentifier },
+ },
+ });
+
+ if (isDefined(registrationResponse.body.errors)) {
+ throw new Error(
+ `Failed to create application registration: ${JSON.stringify(
+ registrationResponse.body.errors,
+ )}`,
+ );
+ }
+
+ const developmentApplicationResponse = await makeMetadataAPIRequest({
+ query: gql`
+ mutation CreateDevelopmentApplication(
+ $universalIdentifier: String!
+ $name: String!
+ ) {
+ createDevelopmentApplication(
+ universalIdentifier: $universalIdentifier
+ name: $name
+ ) {
+ id
+ }
+ }
+ `,
+ variables: { universalIdentifier: applicationUniversalIdentifier, name },
+ });
+
+ if (isDefined(developmentApplicationResponse.body.errors)) {
+ throw new Error(
+ `Failed to create development application: ${JSON.stringify(
+ developmentApplicationResponse.body.errors,
+ )}`,
+ );
+ }
+
const packageJson = JSON.stringify({
name: sourcePath,
version: '1.0.0',