[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)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22870?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-07-15 12:04:22 +02:00
committed by GitHub
parent 2201917f33
commit 14dacd8d35
7 changed files with 386 additions and 108 deletions
@@ -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 },
@@ -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;
};
@@ -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;
};
@@ -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,
@@ -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',
@@ -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<FileEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
private async resolveApplicationIdOrThrow({
applicationUniversalIdentifier,
workspaceId,
queryRunner,
}: {
applicationUniversalIdentifier: string;
workspaceId: string;
queryRunner?: QueryRunner;
}): Promise<string> {
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<string> {
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<FileEntity> {
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<FileEntity> {
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<void> {
async deleteFile(
params: ResourceIdentifier & { applicationId?: string },
): Promise<void> {
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),
});
@@ -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',