Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfef76cd20 | |||
| 7c632c4884 | |||
| d2694a0865 | |||
| fdfdc44891 |
@@ -30,3 +30,6 @@ AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-a
|
||||
CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
|
||||
IS_WORKSPACE_CREATION_V2_ENABLED=true
|
||||
SHOULD_SEED_STANDARD_RECORD_PAGE_LAYOUTS=true
|
||||
|
||||
APPLICATION_REGISTRY_URL=http://127.0.0.1:4444
|
||||
OUTBOUND_HTTP_SAFE_MODE_ENABLED=false
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"semver": "7.6.3",
|
||||
"sharp": "0.32.6",
|
||||
"stripe": "19.3.1",
|
||||
"tar": "^7.5.9",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"transliteration": "2.3.5",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
|
||||
throw new NotFoundError(exception);
|
||||
case ApplicationExceptionCode.FORBIDDEN:
|
||||
case ApplicationExceptionCode.INVALID_INPUT:
|
||||
case ApplicationExceptionCode.INSTALL_HOOK_EXECUTION_FAILED:
|
||||
throw new UserInputError(exception);
|
||||
default: {
|
||||
assertUnreachable(exception.code);
|
||||
|
||||
@@ -5,12 +5,15 @@ import { ApplicationModule } from 'src/engine/core-modules/application/applicati
|
||||
import { ApplicationDevelopmentResolver } from 'src/engine/core-modules/application/resolvers/application-development.resolver';
|
||||
import { ApplicationResolver } from 'src/engine/core-modules/application/resolvers/application.resolver';
|
||||
import { MarketplaceResolver } from 'src/engine/core-modules/application/resolvers/marketplace.resolver';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/services/application-install.service';
|
||||
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/services/application-manifest-migration.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
|
||||
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -36,11 +39,14 @@ import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workf
|
||||
FileStorageModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
SecureHttpClientModule,
|
||||
LogicFunctionExecutorModule,
|
||||
],
|
||||
providers: [
|
||||
ApplicationResolver,
|
||||
ApplicationDevelopmentResolver,
|
||||
MarketplaceResolver,
|
||||
ApplicationInstallService,
|
||||
ApplicationManifestMigrationService,
|
||||
ApplicationSyncService,
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum ApplicationExceptionCode {
|
||||
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
INSTALL_HOOK_EXECUTION_FAILED = 'INSTALL_HOOK_EXECUTION_FAILED',
|
||||
}
|
||||
|
||||
const getApplicationExceptionUserFriendlyMessage = (
|
||||
@@ -35,6 +36,8 @@ const getApplicationExceptionUserFriendlyMessage = (
|
||||
return msg`You do not have permission to perform this action.`;
|
||||
case ApplicationExceptionCode.INVALID_INPUT:
|
||||
return msg`Invalid input provided.`;
|
||||
case ApplicationExceptionCode.INSTALL_HOOK_EXECUTION_FAILED:
|
||||
return msg`Install hook execution failed.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+10
-19
@@ -18,8 +18,8 @@ import {
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/dtos/application-token-pair.dto';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { InstallApplicationInput } from 'src/engine/core-modules/application/dtos/install-application.input';
|
||||
import { UninstallApplicationInput } from 'src/engine/core-modules/application/dtos/uninstallApplicationInput';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/services/application-install.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
|
||||
@@ -33,7 +33,6 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@MetadataResolver()
|
||||
@@ -42,7 +41,7 @@ import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/wo
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
export class ApplicationResolver {
|
||||
constructor(
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly applicationTokenService: ApplicationTokenService,
|
||||
@@ -102,7 +101,11 @@ export class ApplicationResolver {
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
async installApplication(
|
||||
@Args() { workspaceMigration: { actions } }: InstallApplicationInput,
|
||||
@Args('applicationUniversalIdentifier', {
|
||||
type: () => UUIDScalarType,
|
||||
})
|
||||
applicationUniversalIdentifier: string,
|
||||
@Args('version', { type: () => String }) version: string,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute(
|
||||
@@ -121,23 +124,11 @@ export class ApplicationResolver {
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceMigration: {
|
||||
actions,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
return this.applicationInstallService.installApplication({
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
|
||||
+522
@@ -0,0 +1,522 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { basename, join } from 'path';
|
||||
|
||||
import { extract, type ReadEntry } from 'tar';
|
||||
import {
|
||||
type LogicFunctionManifest,
|
||||
type Manifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
|
||||
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
|
||||
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
const MAX_EXTRACTED_SIZE_BYTES = 500 * 1024 * 1024;
|
||||
|
||||
type FileFolderMapping = {
|
||||
fileFolder: FileFolder;
|
||||
resourcePath: string;
|
||||
};
|
||||
|
||||
type InstallHookIdentifiers = {
|
||||
preInstallLogicFunctionUniversalIdentifier: string | undefined;
|
||||
postInstallLogicFunctionUniversalIdentifier: string | undefined;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationInstallService {
|
||||
private readonly logger = new Logger(ApplicationInstallService.name);
|
||||
|
||||
constructor(
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async installApplication({
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
workspaceId,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
version: string;
|
||||
workspaceId: string;
|
||||
}): Promise<boolean> {
|
||||
const temporaryDir = join(tmpdir(), `application-install-${v4()}`);
|
||||
|
||||
try {
|
||||
await fs.mkdir(temporaryDir, { recursive: true });
|
||||
|
||||
const tarballPath = await this.downloadTarball({
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
temporaryDir,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.extractTarball({ tarballPath, temporaryDir });
|
||||
|
||||
const manifest = await this.readManifest(temporaryDir);
|
||||
|
||||
await this.writeExtractedFilesToStorage({
|
||||
extractedDir: temporaryDir,
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const hookIdentifiers = this.resolveInstallHookIdentifiers(manifest);
|
||||
|
||||
const hookLogicFunctions = this.extractHookLogicFunctions({
|
||||
manifest,
|
||||
hookIdentifiers,
|
||||
});
|
||||
|
||||
if (hookLogicFunctions.length > 0) {
|
||||
const hooksOnlyManifest = this.buildHooksOnlyManifest({
|
||||
manifest,
|
||||
hookLogicFunctions,
|
||||
});
|
||||
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId,
|
||||
manifest: hooksOnlyManifest,
|
||||
});
|
||||
}
|
||||
|
||||
await this.executeInstallHook({
|
||||
hookUniversalIdentifier:
|
||||
hookIdentifiers.preInstallLogicFunctionUniversalIdentifier,
|
||||
hookName: 'preInstall',
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
});
|
||||
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId,
|
||||
manifest,
|
||||
});
|
||||
|
||||
await this.executeInstallHook({
|
||||
hookUniversalIdentifier:
|
||||
hookIdentifiers.postInstallLogicFunctionUniversalIdentifier,
|
||||
hookName: 'postInstall',
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Application ${applicationUniversalIdentifier}@${version} installed successfully`,
|
||||
);
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
await fs.rm(temporaryDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadTarball({
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
temporaryDir,
|
||||
workspaceId,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
version: string;
|
||||
temporaryDir: string;
|
||||
workspaceId: string;
|
||||
}): Promise<string> {
|
||||
const registryUrl = this.twentyConfigService.get(
|
||||
'APPLICATION_REGISTRY_URL',
|
||||
);
|
||||
const tarballUrl = `${registryUrl}/${applicationUniversalIdentifier}@${version}/app.tar.gz`;
|
||||
|
||||
this.logger.log(`Downloading tarball from ${tarballUrl}`);
|
||||
|
||||
const httpClient = this.secureHttpClientService.getHttpClient(
|
||||
{ responseType: 'arraybuffer', timeout: 60_000 },
|
||||
{
|
||||
workspaceId,
|
||||
source: 'application-install',
|
||||
},
|
||||
);
|
||||
|
||||
let response: { data: ArrayBuffer; status: number };
|
||||
|
||||
try {
|
||||
response = await httpClient.get(tarballUrl);
|
||||
} catch (error: unknown) {
|
||||
const isAxiosError =
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
'response' in error &&
|
||||
typeof (error as { response?: { status?: number } }).response
|
||||
?.status === 'number';
|
||||
|
||||
if (isAxiosError) {
|
||||
const status = (error as { response: { status: number } }).response
|
||||
.status;
|
||||
|
||||
if (status === 404) {
|
||||
throw new ApplicationException(
|
||||
`Tarball not found for ${applicationUniversalIdentifier}@${version}`,
|
||||
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApplicationException(
|
||||
`Failed to download tarball for ${applicationUniversalIdentifier}@${version}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const tarballPath = join(temporaryDir, 'app.tar.gz');
|
||||
|
||||
await fs.writeFile(tarballPath, Buffer.from(response.data));
|
||||
|
||||
return tarballPath;
|
||||
}
|
||||
|
||||
private async extractTarball({
|
||||
tarballPath,
|
||||
temporaryDir,
|
||||
}: {
|
||||
tarballPath: string;
|
||||
temporaryDir: string;
|
||||
}): Promise<void> {
|
||||
let totalExtractedSize = 0;
|
||||
|
||||
try {
|
||||
await extract({
|
||||
file: tarballPath,
|
||||
cwd: temporaryDir,
|
||||
filter: (path, entry) => {
|
||||
if (path.includes('..')) {
|
||||
this.logger.warn(`Skipping path traversal entry: ${path}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('type' in entry) {
|
||||
const readEntry = entry as ReadEntry;
|
||||
|
||||
if (
|
||||
readEntry.type === 'SymbolicLink' ||
|
||||
readEntry.type === 'Link'
|
||||
) {
|
||||
this.logger.warn(`Skipping symlink entry: ${path}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
totalExtractedSize += entry.size ?? 0;
|
||||
|
||||
if (totalExtractedSize > MAX_EXTRACTED_SIZE_BYTES) {
|
||||
throw new ApplicationException(
|
||||
`Extracted tarball exceeds maximum allowed size of ${MAX_EXTRACTED_SIZE_BYTES} bytes`,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApplicationException(
|
||||
`Failed to extract tarball: ${error instanceof Error ? error.message : String(error)}`,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
await fs.rm(tarballPath);
|
||||
}
|
||||
|
||||
private async readManifest(extractedDir: string): Promise<Manifest> {
|
||||
const manifestPath = join(extractedDir, 'manifest.json');
|
||||
|
||||
let manifestContent: string;
|
||||
|
||||
try {
|
||||
manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
||||
} catch {
|
||||
throw new ApplicationException(
|
||||
'manifest.json not found in tarball',
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(manifestContent) as Manifest;
|
||||
} catch {
|
||||
throw new ApplicationException(
|
||||
'Failed to parse manifest.json',
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveFileFolderMapping(
|
||||
relativePath: string,
|
||||
): FileFolderMapping | null {
|
||||
if (relativePath === 'manifest.json') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (relativePath === 'package.json') {
|
||||
return {
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'package.json',
|
||||
};
|
||||
}
|
||||
|
||||
if (relativePath === 'yarn.lock') {
|
||||
return {
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'yarn.lock',
|
||||
};
|
||||
}
|
||||
|
||||
if (relativePath.startsWith('public/')) {
|
||||
return {
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
resourcePath: relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
if (relativePath.startsWith('src/')) {
|
||||
if (
|
||||
relativePath.endsWith('.front-component.mjs') ||
|
||||
relativePath.endsWith('.front-component.mjs.map')
|
||||
) {
|
||||
return {
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
resourcePath: relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
relativePath.endsWith('.function.mjs') ||
|
||||
relativePath.endsWith('.function.mjs.map')
|
||||
) {
|
||||
return {
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
resourcePath: relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fileFolder: FileFolder.Source,
|
||||
resourcePath: relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fileFolder: FileFolder.Source,
|
||||
resourcePath: relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
private async writeExtractedFilesToStorage({
|
||||
extractedDir,
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
}: {
|
||||
extractedDir: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const filePaths = await this.collectFilePaths(extractedDir, '');
|
||||
|
||||
for (const relativePath of filePaths) {
|
||||
const mapping = this.resolveFileFolderMapping(relativePath);
|
||||
|
||||
if (mapping === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absolutePath = join(extractedDir, relativePath);
|
||||
const fileContent = await fs.readFile(absolutePath);
|
||||
|
||||
const { mimeType } = await extractFileInfo({
|
||||
file: fileContent,
|
||||
filename: basename(relativePath),
|
||||
});
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
sourceFile: fileContent,
|
||||
mimeType,
|
||||
fileFolder: mapping.fileFolder,
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
resourcePath: mapping.resourcePath,
|
||||
settings: {
|
||||
isTemporaryFile: false,
|
||||
toDelete: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async collectFilePaths(
|
||||
directory: string,
|
||||
prefix: string,
|
||||
): Promise<string[]> {
|
||||
const entries = await fs.readdir(directory, { withFileTypes: true });
|
||||
const filePaths: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const nestedPaths = await this.collectFilePaths(
|
||||
join(directory, entry.name),
|
||||
relativePath,
|
||||
);
|
||||
|
||||
filePaths.push(...nestedPaths);
|
||||
} else if (entry.isFile()) {
|
||||
filePaths.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
private resolveInstallHookIdentifiers(
|
||||
manifest: Manifest,
|
||||
): InstallHookIdentifiers {
|
||||
return {
|
||||
preInstallLogicFunctionUniversalIdentifier:
|
||||
manifest.application.preInstallLogicFunctionUniversalIdentifier,
|
||||
postInstallLogicFunctionUniversalIdentifier:
|
||||
manifest.application.postInstallLogicFunctionUniversalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
private extractHookLogicFunctions({
|
||||
manifest,
|
||||
hookIdentifiers,
|
||||
}: {
|
||||
manifest: Manifest;
|
||||
hookIdentifiers: InstallHookIdentifiers;
|
||||
}): LogicFunctionManifest[] {
|
||||
const hookUniversalIdentifiers = [
|
||||
hookIdentifiers.preInstallLogicFunctionUniversalIdentifier,
|
||||
hookIdentifiers.postInstallLogicFunctionUniversalIdentifier,
|
||||
].filter(isDefined);
|
||||
|
||||
if (hookUniversalIdentifiers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hookUniversalIdentifierSet = new Set(hookUniversalIdentifiers);
|
||||
|
||||
return manifest.logicFunctions.filter((logicFunction) =>
|
||||
hookUniversalIdentifierSet.has(logicFunction.universalIdentifier),
|
||||
);
|
||||
}
|
||||
|
||||
private buildHooksOnlyManifest({
|
||||
manifest,
|
||||
hookLogicFunctions,
|
||||
}: {
|
||||
manifest: Manifest;
|
||||
hookLogicFunctions: LogicFunctionManifest[];
|
||||
}): Manifest {
|
||||
return {
|
||||
application: manifest.application,
|
||||
logicFunctions: hookLogicFunctions,
|
||||
objects: [],
|
||||
fields: [],
|
||||
frontComponents: [],
|
||||
roles: [],
|
||||
skills: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async executeInstallHook({
|
||||
hookUniversalIdentifier,
|
||||
hookName,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
}: {
|
||||
hookUniversalIdentifier: string | undefined;
|
||||
hookName: 'preInstall' | 'postInstall';
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
version: string;
|
||||
}): Promise<void> {
|
||||
if (!isDefined(hookUniversalIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatLogicFunctionMaps',
|
||||
]);
|
||||
|
||||
const flatLogicFunction =
|
||||
flatLogicFunctionMaps.byUniversalIdentifier[hookUniversalIdentifier];
|
||||
|
||||
if (!isDefined(flatLogicFunction)) {
|
||||
throw new ApplicationException(
|
||||
`${hookName} logic function with universalIdentifier ${hookUniversalIdentifier} not found after manifest sync`,
|
||||
ApplicationExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Executing ${hookName} hook (${hookUniversalIdentifier}) for ${applicationUniversalIdentifier}@${version}`,
|
||||
);
|
||||
|
||||
const result = await this.logicFunctionExecutorService.execute({
|
||||
logicFunctionId: flatLogicFunction.id,
|
||||
workspaceId,
|
||||
payload: {
|
||||
applicationUniversalIdentifier,
|
||||
version,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === LogicFunctionExecutionStatus.ERROR) {
|
||||
throw new ApplicationException(
|
||||
`${hookName} hook failed for ${applicationUniversalIdentifier}@${version}: ${result.error?.errorMessage ?? 'Unknown error'}`,
|
||||
ApplicationExceptionCode.INSTALL_HOOK_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${hookName} hook completed successfully for ${applicationUniversalIdentifier}@${version}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
export type OutboundRequestSource =
|
||||
| 'webhook'
|
||||
| 'workflow-http'
|
||||
| 'logic-function';
|
||||
| 'logic-function'
|
||||
| 'application-install';
|
||||
|
||||
@@ -1606,6 +1606,16 @@ export class ConfigVariables {
|
||||
@CastToPositiveNumber()
|
||||
@IsOptional()
|
||||
PG_DATABASE_REPLICA_TIMEOUT_MS: number = 10000;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.OTHER,
|
||||
description:
|
||||
'Base URL for downloading application tarballs. The full URL is {APPLICATION_REGISTRY_URL}/{id}@{version}/app.tar.gz',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsOptional()
|
||||
APPLICATION_REGISTRY_URL: string =
|
||||
'https://github.com/twentyhq/twenty-apps/releases/download';
|
||||
}
|
||||
|
||||
export const validate = (config: Record<string, unknown>): ConfigVariables => {
|
||||
|
||||
-59
@@ -1,60 +1 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail when a role has an invalid universalIdentifier 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"role": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "ENTITY_MALFORMED",
|
||||
"message": "Invalid universalIdentifier: "not-a-valid-uuid" is not a valid UUID v4",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "role",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 role",
|
||||
"summary": {
|
||||
"role": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail with execution error when deleting non-existent field metadata 1`] = `
|
||||
{
|
||||
"eventId": Any<String>,
|
||||
"extensions": {
|
||||
"action": {
|
||||
"metadataName": "fieldMetadata",
|
||||
"type": "delete",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"code": "APPLICATION_INSTALLATION_FAILED",
|
||||
"errors": {
|
||||
"actionTranspilation": {
|
||||
"code": "ENTITY_NOT_FOUND",
|
||||
"message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f",
|
||||
},
|
||||
},
|
||||
"exceptionEventId": Any<String>,
|
||||
"userFriendlyMessage": "Migration execution failed.",
|
||||
},
|
||||
"message": "Migration action 'delete' for 'fieldMetadata' failed",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
+2
-9
@@ -6,15 +6,8 @@ describe('Install application should fail when feature flag is disabled', () =>
|
||||
const { errors } = await installApplication({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
workspaceMigration: {
|
||||
actions: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
universalIdentifier: '20202020-784f-4042-b58f-ae8dbf718f6e',
|
||||
},
|
||||
],
|
||||
},
|
||||
applicationUniversalIdentifier: '20202020-784f-4042-b58f-ae8dbf718f6e',
|
||||
version: '0.0.1',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+115
-12
@@ -1,21 +1,87 @@
|
||||
import http from 'http';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { buildTestTarball } from 'test/integration/metadata/suites/application/utils/build-test-tarball.util';
|
||||
import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
const INVALID_UUID_APP_ID = uuidv4();
|
||||
const INVALID_UUID_ROLE_ID = uuidv4();
|
||||
|
||||
describe('Install application should fail when entity does not exist', () => {
|
||||
const TARBALL_SERVER_PORT = 4444;
|
||||
|
||||
const buildTarballServerUrl = (path: string) => `/${path}/app.tar.gz`;
|
||||
|
||||
const startTarballServer = (
|
||||
routes: Record<string, Buffer>,
|
||||
): Promise<http.Server> => {
|
||||
return new Promise((resolve) => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const tarball = routes[req.url ?? ''];
|
||||
|
||||
if (tarball) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Content-Length': tarball.length,
|
||||
});
|
||||
res.end(tarball);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
});
|
||||
|
||||
server.listen(TARBALL_SERVER_PORT, '127.0.0.1', () => {
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const stopServer = (server: http.Server): Promise<void> => {
|
||||
return new Promise((resolve) => server.close(() => resolve()));
|
||||
};
|
||||
|
||||
describe('Install application should fail', () => {
|
||||
let appCreated = false;
|
||||
let tarballServer: http.Server;
|
||||
|
||||
const NO_MANIFEST_APP_ID = uuidv4();
|
||||
const NO_MANIFEST_VERSION = '0.1.0';
|
||||
|
||||
const INVALID_MANIFEST_APP_ID = uuidv4();
|
||||
const INVALID_MANIFEST_VERSION = '0.1.0';
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const tarballWithoutManifest = await buildTestTarball({
|
||||
packageJson: { name: 'no-manifest', version: '0.1.0' },
|
||||
});
|
||||
|
||||
const tarballWithInvalidManifest = await buildTestTarball({
|
||||
rawManifestContent: '{ this is not valid json !!!',
|
||||
packageJson: { name: 'invalid-manifest', version: '0.1.0' },
|
||||
});
|
||||
|
||||
const routes: Record<string, Buffer> = {
|
||||
[buildTarballServerUrl(`${NO_MANIFEST_APP_ID}@${NO_MANIFEST_VERSION}`)]:
|
||||
tarballWithoutManifest,
|
||||
[buildTarballServerUrl(
|
||||
`${INVALID_MANIFEST_APP_ID}@${INVALID_MANIFEST_VERSION}`,
|
||||
)]: tarballWithInvalidManifest,
|
||||
};
|
||||
|
||||
tarballServer = await startTarballServer(routes);
|
||||
|
||||
await updateFeatureFlag({
|
||||
featureFlag:
|
||||
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED,
|
||||
@@ -31,15 +97,23 @@ describe('Install application should fail when entity does not exist', () => {
|
||||
});
|
||||
|
||||
appCreated = true;
|
||||
|
||||
jest.useFakeTimers();
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
await stopServer(tarballServer);
|
||||
|
||||
await updateFeatureFlag({
|
||||
featureFlag:
|
||||
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -53,23 +127,52 @@ describe('Install application should fail when entity does not exist', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with execution error when deleting non-existent field metadata', async () => {
|
||||
it('should fail when tarball is not found on registry (404)', async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const { errors } = await installApplication({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
workspaceMigration: {
|
||||
actions: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
universalIdentifier: '20202020-6110-4547-9fd0-2525257a2c3f',
|
||||
},
|
||||
],
|
||||
},
|
||||
applicationUniversalIdentifier: uuidv4(),
|
||||
version: '9.9.9',
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should fail when tarball does not contain manifest.json', async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const { errors } = await installApplication({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
applicationUniversalIdentifier: NO_MANIFEST_APP_ID,
|
||||
version: NO_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should fail when manifest.json is not valid JSON', async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const { errors } = await installApplication({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
applicationUniversalIdentifier: INVALID_MANIFEST_APP_ID,
|
||||
version: INVALID_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should fail when a role has an invalid universalIdentifier', async () => {
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import http from 'http';
|
||||
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { buildTestTarball } from 'test/integration/metadata/suites/application/utils/build-test-tarball.util';
|
||||
import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_VERSION = '0.1.0';
|
||||
|
||||
const TARBALL_SERVER_PORT = 4444;
|
||||
|
||||
const manifest: Manifest = {
|
||||
application: {
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
|
||||
displayName: 'Test Install App',
|
||||
description: 'An application installed from tarball',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Default Role',
|
||||
description: 'Default role for the test app',
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
};
|
||||
|
||||
const packageJson = {
|
||||
name: 'test-install-app',
|
||||
version: TEST_VERSION,
|
||||
};
|
||||
|
||||
const startTarballServer = (tarballBuffer: Buffer): Promise<http.Server> => {
|
||||
return new Promise((resolve) => {
|
||||
const expectedPath = `/${TEST_APP_ID}@${TEST_VERSION}/app.tar.gz`;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === expectedPath) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Content-Length': tarballBuffer.length,
|
||||
});
|
||||
res.end(tarballBuffer);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
});
|
||||
|
||||
server.listen(TARBALL_SERVER_PORT, '127.0.0.1', () => {
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const stopServer = (server: http.Server): Promise<void> => {
|
||||
return new Promise((resolve) => server.close(() => resolve()));
|
||||
};
|
||||
|
||||
describe('Install application from tarball', () => {
|
||||
let tarballServer: http.Server;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const tarballBuffer = await buildTestTarball({
|
||||
manifest,
|
||||
packageJson,
|
||||
});
|
||||
|
||||
tarballServer = await startTarballServer(tarballBuffer);
|
||||
|
||||
await updateFeatureFlag({
|
||||
featureFlag:
|
||||
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
await stopServer(tarballServer);
|
||||
|
||||
await updateFeatureFlag({
|
||||
featureFlag:
|
||||
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully install an application from a tarball', async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
const { data, errors } = await installApplication({
|
||||
input: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
version: TEST_VERSION,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(data?.installApplication).toBe(true);
|
||||
|
||||
jest.useFakeTimers();
|
||||
}, 60000);
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
import { create } from 'tar';
|
||||
import { v4 } from 'uuid';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export const buildTestTarball = async ({
|
||||
manifest,
|
||||
rawManifestContent,
|
||||
packageJson,
|
||||
}: {
|
||||
manifest?: Manifest;
|
||||
rawManifestContent?: string;
|
||||
packageJson: Record<string, unknown>;
|
||||
}): Promise<Buffer> => {
|
||||
const tempDir = join(tmpdir(), `test-tarball-${v4()}`);
|
||||
|
||||
try {
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
const fileNames: string[] = [];
|
||||
|
||||
const manifestContent =
|
||||
rawManifestContent ??
|
||||
(manifest ? JSON.stringify(manifest, null, 2) : undefined);
|
||||
|
||||
if (manifestContent) {
|
||||
await fs.writeFile(join(tempDir, 'manifest.json'), manifestContent);
|
||||
fileNames.push('manifest.json');
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
join(tempDir, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
);
|
||||
fileNames.push('package.json');
|
||||
|
||||
const tarballPath = join(tempDir, 'app.tar.gz');
|
||||
|
||||
await create(
|
||||
{
|
||||
gzip: true,
|
||||
file: tarballPath,
|
||||
cwd: tempDir,
|
||||
},
|
||||
fileNames,
|
||||
);
|
||||
|
||||
return await fs.readFile(tarballPath);
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
+12
-10
@@ -1,13 +1,8 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export type InstallApplicationFactoryInput = {
|
||||
workspaceMigration: {
|
||||
actions: {
|
||||
type: 'delete';
|
||||
metadataName: string;
|
||||
universalIdentifier: string;
|
||||
}[];
|
||||
};
|
||||
applicationUniversalIdentifier: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export const installApplicationQueryFactory = ({
|
||||
@@ -16,11 +11,18 @@ export const installApplicationQueryFactory = ({
|
||||
input: InstallApplicationFactoryInput;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation InstallApplication($workspaceMigration: WorkspaceMigrationInput!) {
|
||||
installApplication(workspaceMigration: $workspaceMigration)
|
||||
mutation InstallApplication(
|
||||
$applicationUniversalIdentifier: UUID!
|
||||
$version: String!
|
||||
) {
|
||||
installApplication(
|
||||
applicationUniversalIdentifier: $applicationUniversalIdentifier
|
||||
version: $version
|
||||
)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
workspaceMigration: input.workspaceMigration,
|
||||
applicationUniversalIdentifier: input.applicationUniversalIdentifier,
|
||||
version: input.version,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8720,6 +8720,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/fs-minipass@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "@isaacs/fs-minipass@npm:4.0.1"
|
||||
dependencies:
|
||||
minipass: "npm:^7.0.4"
|
||||
checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/string-locale-compare@npm:^1.0.1":
|
||||
version: 1.1.0
|
||||
resolution: "@isaacs/string-locale-compare@npm:1.1.0"
|
||||
@@ -30972,6 +30981,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "chownr@npm:3.0.0"
|
||||
checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chromatic@npm:^13.3.3":
|
||||
version: 13.3.5
|
||||
resolution: "chromatic@npm:13.3.5"
|
||||
@@ -46527,6 +46543,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass@npm:^7.0.4":
|
||||
version: 7.1.3
|
||||
resolution: "minipass@npm:7.1.3"
|
||||
checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "minizlib@npm:2.1.2"
|
||||
@@ -46537,6 +46560,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minizlib@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "minizlib@npm:3.1.0"
|
||||
dependencies:
|
||||
minipass: "npm:^7.1.2"
|
||||
checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mintlify@npm:latest":
|
||||
version: 4.2.179
|
||||
resolution: "mintlify@npm:4.2.179"
|
||||
@@ -56561,6 +56593,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^7.5.9":
|
||||
version: 7.5.9
|
||||
resolution: "tar@npm:7.5.9"
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass": "npm:^4.0.0"
|
||||
chownr: "npm:^3.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
minizlib: "npm:^3.1.0"
|
||||
yallist: "npm:^5.0.0"
|
||||
checksum: 10c0/e870beb1b2477135ca2abe86b2d18f7b35d0a4e3a37bbc523d3b8f7adca268dfab543f26528a431d569897f8c53a7cac745cdfbc4411c2f89aeeacc652b81b0a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"temp@npm:^0.8.4":
|
||||
version: 0.8.4
|
||||
resolution: "temp@npm:0.8.4"
|
||||
@@ -57954,6 +57999,7 @@ __metadata:
|
||||
semver: "npm:7.6.3"
|
||||
sharp: "npm:0.32.6"
|
||||
stripe: "npm:19.3.1"
|
||||
tar: "npm:^7.5.9"
|
||||
temporal-polyfill: "npm:^0.3.0"
|
||||
transliteration: "npm:2.3.5"
|
||||
tsconfig-paths: "npm:^4.2.0"
|
||||
@@ -61276,6 +61322,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yallist@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "yallist@npm:5.0.0"
|
||||
checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml-ast-parser@npm:^0.0.43":
|
||||
version: 0.0.43
|
||||
resolution: "yaml-ast-parser@npm:0.0.43"
|
||||
|
||||
Reference in New Issue
Block a user