Refactor client auto heal (#19032)
This commit is contained in:
+1
@@ -721,6 +721,7 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
const sdkArchiveBuffer =
|
||||
await this.sdkClientArchiveService.downloadArchiveBuffer({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -113,6 +113,7 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
|
||||
await this.sdkClientArchiveService.downloadAndExtractToPackage({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath: sdkPackagePath,
|
||||
});
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ export class SdkClientController {
|
||||
const fileBuffer =
|
||||
await this.sdkClientArchiveService.getClientModuleFromArchive({
|
||||
workspaceId: workspace.id,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
moduleName,
|
||||
});
|
||||
|
||||
+45
-41
@@ -1,11 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { createWriteStream } from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { type Readable } from 'stream';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -16,12 +12,12 @@ import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
|
||||
import { type SdkModuleName } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules';
|
||||
import {
|
||||
SdkClientException,
|
||||
SdkClientExceptionCode,
|
||||
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@@ -29,75 +25,74 @@ const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
|
||||
|
||||
@Injectable()
|
||||
export class SdkClientArchiveService {
|
||||
private readonly logger = new Logger(SdkClientArchiveService.name);
|
||||
|
||||
constructor(
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
) {}
|
||||
|
||||
async downloadAndExtractToPackage({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
targetPackagePath: string;
|
||||
}): Promise<void> {
|
||||
const temporaryDirManager = new TemporaryDirManager();
|
||||
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
try {
|
||||
const { sourceTemporaryDir } = await temporaryDirManager.init();
|
||||
const archivePath = join(sourceTemporaryDir, SDK_CLIENT_ARCHIVE_NAME);
|
||||
await fs.rm(targetPackagePath, { recursive: true, force: true });
|
||||
await fs.mkdir(targetPackagePath, { recursive: true });
|
||||
|
||||
const archiveStream = await this.readArchiveStream({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
const { default: unzipper } = await import('unzipper');
|
||||
const directory = await unzipper.Open.buffer(archiveBuffer);
|
||||
|
||||
await pipeline(archiveStream, createWriteStream(archivePath));
|
||||
|
||||
await fs.rm(targetPackagePath, { recursive: true, force: true });
|
||||
await fs.mkdir(targetPackagePath, { recursive: true });
|
||||
|
||||
const { default: unzipper } = await import('unzipper');
|
||||
const directory = await unzipper.Open.file(archivePath);
|
||||
|
||||
await directory.extract({ path: targetPackagePath });
|
||||
} finally {
|
||||
await temporaryDirManager.clean();
|
||||
}
|
||||
await directory.extract({ path: targetPackagePath });
|
||||
}
|
||||
|
||||
async downloadArchiveBuffer({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Buffer> {
|
||||
const archiveStream = await this.readArchiveStream({
|
||||
return this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
return streamToBuffer(archiveStream);
|
||||
}
|
||||
|
||||
async getClientModuleFromArchive({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
moduleName,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
moduleName: SdkModuleName;
|
||||
}): Promise<Buffer> {
|
||||
const filePath = `dist/${moduleName}.mjs`;
|
||||
|
||||
const archiveBuffer = await this.downloadArchiveBuffer({
|
||||
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
@@ -135,32 +130,41 @@ export class SdkClientArchiveService {
|
||||
]);
|
||||
}
|
||||
|
||||
private async readArchiveStream({
|
||||
private async downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Readable> {
|
||||
}): Promise<Buffer> {
|
||||
try {
|
||||
return await this.fileStorageService.readFile({
|
||||
const stream = await this.fileStorageService.readFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.GeneratedSdkClient,
|
||||
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
|
||||
});
|
||||
|
||||
return await streamToBuffer(stream);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
!(error instanceof FileStorageException) ||
|
||||
error.code !== FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
throw new SdkClientException(
|
||||
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
|
||||
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`SDK client archive missing for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}", generating on-the-fly`,
|
||||
);
|
||||
|
||||
return this.sdkClientGenerationService.generateSdkClientForApplication({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -44,13 +44,13 @@ export class SdkClientGenerationService {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<Buffer> {
|
||||
const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema(
|
||||
{ id: workspaceId } as WorkspaceEntity,
|
||||
applicationId,
|
||||
);
|
||||
|
||||
await this.generateAndStore({
|
||||
const archiveBuffer = await this.generateAndStore({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
@@ -60,6 +60,8 @@ export class SdkClientGenerationService {
|
||||
this.logger.log(
|
||||
`Generated SDK client for application ${applicationUniversalIdentifier}`,
|
||||
);
|
||||
|
||||
return archiveBuffer;
|
||||
}
|
||||
|
||||
private async generateAndStore({
|
||||
@@ -72,7 +74,7 @@ export class SdkClientGenerationService {
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
schema: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<Buffer> {
|
||||
const temporaryDirManager = new TemporaryDirManager();
|
||||
|
||||
try {
|
||||
@@ -101,12 +103,14 @@ export class SdkClientGenerationService {
|
||||
|
||||
await createZipFile(tempPackageRoot, archivePath);
|
||||
|
||||
const archiveBuffer = await fs.readFile(archivePath);
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.GeneratedSdkClient,
|
||||
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
|
||||
sourceFile: await fs.readFile(archivePath),
|
||||
sourceFile: archiveBuffer,
|
||||
mimeType: 'application/zip',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
});
|
||||
@@ -119,6 +123,8 @@ export class SdkClientGenerationService {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
return archiveBuffer;
|
||||
} catch (error) {
|
||||
throw new SdkClientException(
|
||||
`Failed to generate SDK client for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
|
||||
Reference in New Issue
Block a user