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',