Fix breaking change in install app command (#20825)

add backward compatibility for twenty-sdk install command
This commit is contained in:
martmull
2026-05-21 22:58:34 +02:00
committed by GitHub
parent c721fa8502
commit 9a7f50fcb7
17 changed files with 207 additions and 99 deletions
@@ -3294,7 +3294,6 @@ type Mutation {
updateWorkspace(data: UpdateWorkspaceInput!): Workspace!
deleteCurrentWorkspace: Workspace!
checkCustomDomainValidRecords: DomainValidRecords
installApplication(appRegistrationId: String!, version: String): Boolean!
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
uninstallApplication(universalIdentifier: String!): Boolean!
createOIDCIdentityProvider(input: SetupOIDCSsoInput!): SetupSso!
@@ -3315,7 +3314,8 @@ type Mutation {
deleteEmailingDomain(id: String!): Boolean!
verifyEmailingDomain(id: String!): EmailingDomain!
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
installMarketplaceApp(universalIdentifier: String!, version: String): Application!
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean! @deprecated(reason: "Use installApplication instead")
installApplication(universalIdentifier: String!, version: String): Application!
syncMarketplaceCatalog: Boolean!
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
@@ -2827,7 +2827,6 @@ export interface Mutation {
updateWorkspace: Workspace
deleteCurrentWorkspace: Workspace
checkCustomDomainValidRecords?: DomainValidRecords
installApplication: Scalars['Boolean']
runWorkspaceMigration: Scalars['Boolean']
uninstallApplication: Scalars['Boolean']
createOIDCIdentityProvider: SetupSso
@@ -2848,7 +2847,9 @@ export interface Mutation {
deleteEmailingDomain: Scalars['Boolean']
verifyEmailingDomain: EmailingDomain
createOneAppToken: AppToken
installMarketplaceApp: Application
/** @deprecated Use installApplication instead */
installMarketplaceApp: Scalars['Boolean']
installApplication: Application
syncMarketplaceCatalog: Scalars['Boolean']
createDevelopmentApplication: DevelopmentApplication
generateApplicationToken: ApplicationTokenPair
@@ -5879,7 +5880,6 @@ export interface MutationGenqlSelection{
updateWorkspace?: (WorkspaceGenqlSelection & { __args: {data: UpdateWorkspaceInput} })
deleteCurrentWorkspace?: WorkspaceGenqlSelection
checkCustomDomainValidRecords?: DomainValidRecordsGenqlSelection
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
createOIDCIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupOIDCSsoInput} })
@@ -5900,7 +5900,9 @@ export interface MutationGenqlSelection{
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
installMarketplaceApp?: (ApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} })
/** @deprecated Use installApplication instead */
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
installApplication?: (ApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} })
syncMarketplaceCatalog?: boolean | number
createDevelopmentApplication?: (DevelopmentApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], name: Scalars['String']} })
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
@@ -8388,18 +8388,6 @@ export default {
"checkCustomDomainValidRecords": [
228
],
"installApplication": [
6,
{
"appRegistrationId": [
1,
"String!"
],
"version": [
1
]
}
],
"runWorkspaceMigration": [
6,
{
@@ -8602,6 +8590,18 @@ export default {
}
],
"installMarketplaceApp": [
6,
{
"universalIdentifier": [
1,
"String!"
],
"version": [
1
]
}
],
"installApplication": [
58,
{
"universalIdentifier": [
@@ -2457,8 +2457,9 @@ export type Mutation = {
impersonate: Impersonate;
initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioning;
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioning;
installApplication: Scalars['Boolean'];
installMarketplaceApp: Application;
installApplication: Application;
/** @deprecated Use installApplication instead */
installMarketplaceApp: Scalars['Boolean'];
refreshEnterpriseValidityToken: Scalars['Boolean'];
removeQueryFromEventStream: Scalars['Boolean'];
removeRoleFromAgent: Scalars['Boolean'];
@@ -3087,7 +3088,7 @@ export type MutationInitiateOtpProvisioningArgs = {
export type MutationInstallApplicationArgs = {
appRegistrationId: Scalars['String'];
universalIdentifier: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
};
@@ -6620,13 +6621,12 @@ export type MarketplaceAppDetailFieldsFragment = { __typename?: 'MarketplaceAppD
export type MarketplaceAppFieldsFragment = { __typename?: 'MarketplaceApp', id: string, name: string, description: string, author: string, category: string, logo?: string | null, sourcePackage?: string | null, isFeatured: boolean };
export type InstallMarketplaceAppMutationVariables = Exact<{
export type InstallApplicationMutationVariables = Exact<{
universalIdentifier: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
}>;
export type InstallMarketplaceAppMutation = { __typename?: 'Mutation', installMarketplaceApp: { __typename?: 'Application', id: string } };
export type InstallApplicationMutation = { __typename?: 'Mutation', installApplication: { __typename?: 'Application', id: string } };
export type UpgradeApplicationMutationVariables = Exact<{
appRegistrationId: Scalars['String'];
@@ -8014,7 +8014,7 @@ export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions
export const FindManyLogicFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyLogicFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyLogicFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindManyLogicFunctionsQuery, FindManyLogicFunctionsQueryVariables>;
export const FindOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneLogicFunctionQuery, FindOneLogicFunctionQueryVariables>;
export const GetLogicFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLogicFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getLogicFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
export const InstallMarketplaceAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallMarketplaceApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"version"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installMarketplaceApp"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}},{"kind":"Argument","name":{"kind":"Name","value":"version"},"value":{"kind":"Variable","name":{"kind":"Name","value":"version"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<InstallMarketplaceAppMutation, InstallMarketplaceAppMutationVariables>;
export const InstallApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<InstallApplicationMutation, InstallApplicationMutationVariables>;
export const UpgradeApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}}}]}]}}]} as unknown as DocumentNode<UpgradeApplicationMutation, UpgradeApplicationMutationVariables>;
export const FindManyMarketplaceAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}}]}}]} as unknown as DocumentNode<FindManyMarketplaceAppsQuery, FindManyMarketplaceAppsQueryVariables>;
export const FindMarketplaceAppDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindMarketplaceAppDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findMarketplaceAppDetail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppDetailFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppDetailFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceAppDetail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"manifest"}}]}}]} as unknown as DocumentNode<FindMarketplaceAppDetailQuery, FindMarketplaceAppDetailQueryVariables>;
@@ -0,0 +1,9 @@
import gql from 'graphql-tag';
export const INSTALL_APPLICATION = gql`
mutation InstallApplication($universalIdentifier: String!) {
installApplication(universalIdentifier: $universalIdentifier) {
id
}
}
`;
@@ -1,15 +0,0 @@
import gql from 'graphql-tag';
export const INSTALL_MARKETPLACE_APP = gql`
mutation InstallMarketplaceApp(
$universalIdentifier: String!
$version: String
) {
installMarketplaceApp(
universalIdentifier: $universalIdentifier
version: $version
) {
id
}
}
`;
@@ -4,25 +4,23 @@ import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
InstallMarketplaceAppDocument,
type InstallMarketplaceAppMutation,
InstallApplicationDocument,
type InstallApplicationMutation,
} from '~/generated-metadata/graphql';
export const useInstallMarketplaceApp = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [isInstalling, setIsInstalling] = useState(false);
const [installMarketplaceAppMutation] = useMutation(
InstallMarketplaceAppDocument,
);
const [installApplicationMutation] = useMutation(InstallApplicationDocument);
const install = async (variables: {
universalIdentifier: string;
version?: string;
}): Promise<InstallMarketplaceAppMutation | null> => {
}): Promise<InstallApplicationMutation | null> => {
setIsInstalling(true);
try {
const result = await installMarketplaceAppMutation({ variables });
const result = await installApplicationMutation({ variables });
if (isDefined(result.data)) {
enqueueSuccessSnackBar({
@@ -104,7 +104,7 @@ export const SettingsAvailableApplicationDetails = () => {
universalIdentifier: detail.universalIdentifier,
});
const applicationId = data?.installMarketplaceApp?.id;
const applicationId = data?.installApplication?.id;
if (isDefined(applicationId)) {
navigateSettings(SettingsPath.ApplicationDetail, {
@@ -139,8 +139,8 @@ export class FileApi {
}): Promise<ApiResponse<boolean>> {
try {
const mutation = `
mutation InstallMarketplaceApp($universalIdentifier: String!) {
installMarketplaceApp(universalIdentifier: $universalIdentifier) {
mutation InstallApplication($universalIdentifier: String!) {
installApplication(universalIdentifier: $universalIdentifier) {
id
}
}
@@ -169,7 +169,7 @@ export class FileApi {
return {
success: true,
data: response.data.data.installMarketplaceApp,
data: response.data.data.installApplication,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
@@ -4,14 +4,13 @@ import {
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { Args, Mutation, Query } from '@nestjs/graphql';
import { Args, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -27,10 +26,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
@UseGuards(WorkspaceAuthGuard)
export class ApplicationInstallResolver {
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationInstallService: ApplicationInstallService,
) {}
constructor(private readonly applicationService: ApplicationService) {}
@Query(() => [ApplicationDTO])
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
@@ -57,19 +53,4 @@ export class ApplicationInstallResolver {
workspaceId,
});
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async installApplication(
@Args('appRegistrationId') appRegistrationId: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<boolean> {
return this.applicationInstallService.installApplication({
appRegistrationId,
version,
workspaceId,
});
}
}
@@ -48,13 +48,37 @@ export class MarketplaceResolver {
);
}
@Mutation(() => ApplicationDTO)
@Mutation(() => Boolean, {
deprecationReason: 'Use installApplication instead',
})
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
async installMarketplaceApp(
@Args('universalIdentifier') universalIdentifier: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
const registration =
await this.marketplaceQueryService.findRegistrationByUniversalIdentifier(
universalIdentifier,
);
await this.applicationInstallService.installApplication({
appRegistrationId: registration.id,
version,
workspaceId: workspace.id,
});
return true;
}
@Mutation(() => ApplicationDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
async installApplication(
@Args('universalIdentifier') universalIdentifier: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
const registration =
await this.marketplaceQueryService.findRegistrationByUniversalIdentifier(
@@ -1,16 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Install application should fail when entity does not exist should fail with execution error when installing non-existent app registration 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "APPLICATION_NOT_FOUND",
"userFriendlyMessage": "Application not found.",
},
"message": "Application registration with id 20202020-0000-0000-0000-000000000000 not found",
"name": "NotFoundError",
}
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Install application should fail when entity does not exist should fail when a role has an invalid universalIdentifier 1`] = `
{
@@ -46,3 +34,15 @@ exports[`Install application should fail when entity does not exist should fail
"name": "GraphQLError",
}
`;
exports[`Install application should fail when entity does not exist should fail with execution error when installing non-existent app registration 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "APPLICATION_REGISTRATION_NOT_FOUND",
"userFriendlyMessage": "Application registration not found.",
},
"message": "No application registration found for identifier "20202020-0000-0000-0000-000000000000"",
"name": "NotFoundError",
}
`;
@@ -141,13 +141,11 @@ describe('Install application should return structured validation errors', () =>
expect(uploadResult.errors).toBeUndefined();
expect(uploadResult.data?.uploadAppTarball.id).toBeDefined();
const registrationId = uploadResult.data!.uploadAppTarball.id;
createdApplicationUniversalIdentifiers.push(universalIdentifier);
const { errors } = await installApplication({
input: {
appRegistrationId: registrationId,
universalIdentifier,
},
expectToFail: true,
});
@@ -29,7 +29,7 @@ describe('Install application should fail when entity does not exist', () => {
const { errors } = await installApplication({
expectToFail: true,
input: {
appRegistrationId: '20202020-0000-0000-0000-000000000000',
universalIdentifier: '20202020-0000-0000-0000-000000000000',
},
});
@@ -1,6 +1,11 @@
import crypto from 'crypto';
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import request from 'supertest';
import * as tar from 'tar';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { type DataSource } from 'typeorm';
const TEST_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
@@ -21,8 +26,8 @@ const MARKETPLACE_QUERY = `
`;
const INSTALL_MUTATION = `
mutation InstallMarketplaceApp($universalIdentifier: String!) {
installMarketplaceApp(universalIdentifier: $universalIdentifier) {
mutation InstallApplication($universalIdentifier: String!) {
installApplication(universalIdentifier: $universalIdentifier) {
id
}
}
@@ -178,7 +183,7 @@ describe('Marketplace Catalog Sync (integration)', () => {
});
});
describe('installMarketplaceApp', () => {
describe('installApplication', () => {
it('should fail if registration does not exist', async () => {
const res = await gqlRequest(INSTALL_MUTATION, {
universalIdentifier: crypto.randomUUID(),
@@ -189,5 +194,106 @@ describe('Marketplace Catalog Sync (integration)', () => {
'No application registration found',
);
});
it('should install a tarball app and return the application id', async () => {
const universalIdentifier = crypto.randomUUID();
const roleId = crypto.randomUUID();
const manifest = JSON.stringify({
application: {
universalIdentifier,
displayName: 'Install Test App',
description: 'App for testing installApplication',
icon: 'IconTestPipe',
defaultRoleUniversalIdentifier: roleId,
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
},
roles: [
{
universalIdentifier: roleId,
label: 'Default Role',
description: 'Default role',
},
],
skills: [],
agents: [],
objects: [],
fields: [],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
pageLayoutTabs: [],
commandMenuItems: [],
});
const packageJson = JSON.stringify({
name: 'test-install-app',
version: '1.0.0',
});
const tempId = crypto.randomUUID();
const sourceDir = join(tmpdir(), `test-tarball-src-${tempId}`);
const tarballPath = join(tmpdir(), `test-tarball-${tempId}.tar.gz`);
await fs.mkdir(sourceDir, { recursive: true });
await fs.writeFile(join(sourceDir, 'manifest.json'), manifest);
await fs.writeFile(join(sourceDir, 'package.json'), packageJson);
await tar.create(
{ file: tarballPath, gzip: true, cwd: sourceDir },
['manifest.json', 'package.json'],
);
const tarballBuffer = await fs.readFile(tarballPath);
await fs.rm(sourceDir, { recursive: true, force: true });
await fs.rm(tarballPath, { force: true });
const UPLOAD_MUTATION = `
mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) {
uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) {
id
universalIdentifier
name
}
}
`;
const uploadRes = await request(baseUrl)
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.field(
'operations',
JSON.stringify({
query: UPLOAD_MUTATION,
variables: { file: null, universalIdentifier },
}),
)
.field('map', JSON.stringify({ '0': ['variables.file'] }))
.attach('0', tarballBuffer, 'app.tar.gz')
.expect(200);
expect(uploadRes.body.errors).toBeUndefined();
expect(uploadRes.body.data.uploadAppTarball.id).toBeDefined();
try {
const installRes = await gqlRequest(INSTALL_MUTATION, {
universalIdentifier,
}).expect(200);
expect(installRes.body.errors).toBeUndefined();
expect(installRes.body.data.installApplication).toBeDefined();
expect(installRes.body.data.installApplication.id).toBeDefined();
} finally {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: universalIdentifier,
});
}
}, 120000);
});
});
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
export type InstallApplicationFactoryInput = {
appRegistrationId: string;
universalIdentifier: string;
version?: string;
};
@@ -11,15 +11,20 @@ export const installApplicationQueryFactory = ({
input: InstallApplicationFactoryInput;
}) => ({
query: gql`
mutation InstallApplication($appRegistrationId: String!, $version: String) {
mutation InstallApplication(
$universalIdentifier: String!
$version: String
) {
installApplication(
appRegistrationId: $appRegistrationId
universalIdentifier: $universalIdentifier
version: $version
)
) {
id
}
}
`,
variables: {
appRegistrationId: input.appRegistrationId,
universalIdentifier: input.universalIdentifier,
version: input.version,
},
});
@@ -16,7 +16,7 @@ export const installApplication = async ({
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
installApplication: boolean;
installApplication: { id: string };
}> => {
const graphqlOperation = installApplicationQueryFactory({ input });