From 3db09e44239128cb545739c330b409fa92a78a04 Mon Sep 17 00:00:00 2001 From: martmull Date: Sat, 4 Jul 2026 22:45:12 +0200 Subject: [PATCH] feat(server): refresh application registration on install (#22527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of the app settings architecture cleanup (twentyhq/core-team-issues#2456) — unifies registration ingestion across sources. ## Problem The dev sync, catalog sync and tarball upload flows all refresh the `applicationRegistration` row (manifest + display columns) at ingestion time, but the install/upgrade flow never did. Installing or upgrading an app relied on catalog sync having run beforehand, so a registration could serve stale display data (name, logo, description, screenshots…) after an install that shipped a newer manifest. ## Changes - `doInstallApplication` now refreshes the global registration from the resolved manifest after all install steps succeed (post-install hook included, so a hook failure that triggers uninstall can't leave the registration refreshed for a failed install). - Downgrade guard: the refresh is skipped when the installed version is provably older than `latestAvailableVersion` (per-workspace installs of an older version never downgrade the global registration). Extracted as a pure util `shouldRefreshApplicationRegistrationOnInstall` with unit tests: - `latestAvailableVersion` null or invalid semver → refresh - installed ≥ latest → refresh, and `latestAvailableVersion` is bumped to the installed version - installed < latest, or installed not valid semver while latest is → skip - Asset URLs mirror the existing per-source ingestion behavior: NPM registrations get manifest `logoUrl`/`screenshots` resolved to registry CDN URLs (same as catalog sync); tarball and other sources persist the manifest as-is (same as tarball upload). - `updateFromManifest` gains an optional `latestAvailableVersion` param (same conditional-spread style as `sourceType`). - `ApplicationRegistrationModule` added to `ApplicationInstallModule` imports (no cycle: nothing in the registration module's import graph imports the install module). The dev sync flow (`syncRegistrationMetadata`) already goes through `updateFromManifest` and writes the display columns — verified, no change needed. ## Verification - New unit spec: 6 cases on the guard util - `npx jest "application-registration|application-install|marketplace"` → 3 suites, 21 tests passed - `npx nx typecheck twenty-server` → success - `npx nx lint:diff-with-main twenty-server` → clean Review in cubic --------- Co-authored-by: Charles Bochet --- .../src/metadata/generated/schema.graphql | 98 ++--- .../src/metadata/generated/schema.ts | 64 +-- .../src/metadata/generated/types.ts | 406 +++++++++--------- .../application-install.module.ts | 2 + .../application-install.service.ts | 30 ++ ...ation-registration-on-install.util.spec.ts | 57 +++ ...pplication-registration-on-install.util.ts | 23 + .../marketplace-catalog-sync.service.ts | 24 +- .../application-registration.module.ts | 5 + .../application-registration.service.ts | 52 ++- .../manifest-asset-url-resolver.service.ts | 55 +++ 11 files changed, 505 insertions(+), 311 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/should-refresh-application-registration-on-install.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-install/utils/should-refresh-application-registration-on-install.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 17de322b9b..c7879b6eec 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3106,7 +3106,6 @@ type Query { getViewGroup(id: String!): ViewGroup findManyApplications: [Application!]! findOneApplication(id: UUID, universalIdentifier: UUID): Application! - getRoles: [Role!]! findApplicationRegistrationByClientId(clientId: String!): PublicApplicationRegistration findApplicationRegistrationByUniversalIdentifier(universalIdentifier: String!): ApplicationRegistration findManyApplicationRegistrations: [ApplicationRegistration!]! @@ -3114,6 +3113,7 @@ type Query { findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats! findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariableDTO!]! applicationRegistrationTarballUrl(id: String!): String + getRoles: [Role!]! previewMessageCampaignAudience(input: PreviewMessageCampaignAudienceInput!): CampaignAudiencePreviewDTO! unsubscribeTopics: [UnsubscribeTopic!]! unsubscribePagePreviewUrl: String! @@ -3363,6 +3363,15 @@ type Mutation { destroyViewGroup(input: DestroyViewGroupInput!): ViewGroup! runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean! uninstallApplication(universalIdentifier: String!): Boolean! + createApplicationRegistration(input: CreateApplicationRegistrationInput!): CreateApplicationRegistration! + updateApplicationRegistration(input: UpdateApplicationRegistrationInput!): ApplicationRegistration! + deleteApplicationRegistration(id: String!): Boolean! + rotateApplicationRegistrationClientSecret(id: String!): RotateClientSecret! + createApplicationRegistrationVariable(input: CreateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable! + updateApplicationRegistrationVariable(input: UpdateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable! + deleteApplicationRegistrationVariable(id: String!): Boolean! + uploadAppTarball(file: Upload!, universalIdentifier: String): ApplicationRegistration! + transferApplicationRegistrationOwnership(applicationRegistrationId: String!, targetWorkspaceSubdomain: String!): ApplicationRegistration! updateWorkspaceMemberRole(workspaceMemberId: UUID!, roleId: UUID!): WorkspaceMember! createOneRole(createRoleInput: CreateRoleInput!): Role! updateOneRole(updateRoleInput: UpdateRoleInput!): Role! @@ -3373,15 +3382,6 @@ type Mutation { upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult! assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean! removeRoleFromAgent(agentId: UUID!): Boolean! - createApplicationRegistration(input: CreateApplicationRegistrationInput!): CreateApplicationRegistration! - updateApplicationRegistration(input: UpdateApplicationRegistrationInput!): ApplicationRegistration! - deleteApplicationRegistration(id: String!): Boolean! - rotateApplicationRegistrationClientSecret(id: String!): RotateClientSecret! - createApplicationRegistrationVariable(input: CreateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable! - updateApplicationRegistrationVariable(input: UpdateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable! - deleteApplicationRegistrationVariable(id: String!): Boolean! - uploadAppTarball(file: Upload!, universalIdentifier: String): ApplicationRegistration! - transferApplicationRegistrationOwnership(applicationRegistrationId: String!, targetWorkspaceSubdomain: String!): ApplicationRegistration! sendEmailViaEmailingDomain(input: SendEmailViaDomainInput!): SendEmailViaDomainOutput! sendMessageCampaign(input: SendMessageCampaignInput!): SendMessageCampaignOutputDTO! createUnsubscribeTopic(input: CreateUnsubscribeTopicInput!): UnsubscribeTopic! @@ -4279,6 +4279,45 @@ enum WorkspaceMigrationActionType { update } +input CreateApplicationRegistrationInput { + name: String! + universalIdentifier: String + oAuthRedirectUris: [String!] + oAuthScopes: [String!] +} + +input UpdateApplicationRegistrationInput { + id: String! + update: UpdateApplicationRegistrationPayload! +} + +input UpdateApplicationRegistrationPayload { + name: String + oAuthRedirectUris: [String!] + oAuthScopes: [String!] + isListed: Boolean + isPreInstalled: Boolean +} + +input CreateApplicationRegistrationVariableInput { + applicationRegistrationId: String! + key: String! + value: String! + description: String + isSecret: Boolean +} + +input UpdateApplicationRegistrationVariableInput { + id: String! + update: UpdateApplicationRegistrationVariablePayload! +} + +input UpdateApplicationRegistrationVariablePayload { + value: String + resetValue: Boolean + description: String +} + input CreateRoleInput { id: String label: String! @@ -4374,45 +4413,6 @@ input RowLevelPermissionPredicateGroupInput { positionInRowLevelPermissionPredicateGroup: Float } -input CreateApplicationRegistrationInput { - name: String! - universalIdentifier: String - oAuthRedirectUris: [String!] - oAuthScopes: [String!] -} - -input UpdateApplicationRegistrationInput { - id: String! - update: UpdateApplicationRegistrationPayload! -} - -input UpdateApplicationRegistrationPayload { - name: String - oAuthRedirectUris: [String!] - oAuthScopes: [String!] - isListed: Boolean - isPreInstalled: Boolean -} - -input CreateApplicationRegistrationVariableInput { - applicationRegistrationId: String! - key: String! - value: String! - description: String - isSecret: Boolean -} - -input UpdateApplicationRegistrationVariableInput { - id: String! - update: UpdateApplicationRegistrationVariablePayload! -} - -input UpdateApplicationRegistrationVariablePayload { - value: String - resetValue: Boolean - description: String -} - input SendEmailViaDomainInput { emailingDomainId: String! to: [String!]! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 7340a89654..7dfa780513 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2714,7 +2714,6 @@ export interface Query { getViewGroup?: ViewGroup findManyApplications: Application[] findOneApplication: Application - getRoles: Role[] findApplicationRegistrationByClientId?: PublicApplicationRegistration findApplicationRegistrationByUniversalIdentifier?: ApplicationRegistration findManyApplicationRegistrations: ApplicationRegistration[] @@ -2722,6 +2721,7 @@ export interface Query { findApplicationRegistrationStats: ApplicationRegistrationStats findApplicationRegistrationVariables: ApplicationRegistrationVariableDTO[] applicationRegistrationTarballUrl?: Scalars['String'] + getRoles: Role[] previewMessageCampaignAudience: CampaignAudiencePreviewDTO unsubscribeTopics: UnsubscribeTopic[] unsubscribePagePreviewUrl: Scalars['String'] @@ -2891,6 +2891,15 @@ export interface Mutation { destroyViewGroup: ViewGroup runWorkspaceMigration: Scalars['Boolean'] uninstallApplication: Scalars['Boolean'] + createApplicationRegistration: CreateApplicationRegistration + updateApplicationRegistration: ApplicationRegistration + deleteApplicationRegistration: Scalars['Boolean'] + rotateApplicationRegistrationClientSecret: RotateClientSecret + createApplicationRegistrationVariable: ApplicationRegistrationVariable + updateApplicationRegistrationVariable: ApplicationRegistrationVariable + deleteApplicationRegistrationVariable: Scalars['Boolean'] + uploadAppTarball: ApplicationRegistration + transferApplicationRegistrationOwnership: ApplicationRegistration updateWorkspaceMemberRole: WorkspaceMember createOneRole: Role updateOneRole: Role @@ -2901,15 +2910,6 @@ export interface Mutation { upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult assignRoleToAgent: Scalars['Boolean'] removeRoleFromAgent: Scalars['Boolean'] - createApplicationRegistration: CreateApplicationRegistration - updateApplicationRegistration: ApplicationRegistration - deleteApplicationRegistration: Scalars['Boolean'] - rotateApplicationRegistrationClientSecret: RotateClientSecret - createApplicationRegistrationVariable: ApplicationRegistrationVariable - updateApplicationRegistrationVariable: ApplicationRegistrationVariable - deleteApplicationRegistrationVariable: Scalars['Boolean'] - uploadAppTarball: ApplicationRegistration - transferApplicationRegistrationOwnership: ApplicationRegistration sendEmailViaEmailingDomain: SendEmailViaDomainOutput sendMessageCampaign: SendMessageCampaignOutputDTO createUnsubscribeTopic: UnsubscribeTopic @@ -5885,7 +5885,6 @@ export interface QueryGenqlSelection{ getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} }) findManyApplications?: ApplicationGenqlSelection findOneApplication?: (ApplicationGenqlSelection & { __args?: {id?: (Scalars['UUID'] | null), universalIdentifier?: (Scalars['UUID'] | null)} }) - getRoles?: RoleGenqlSelection findApplicationRegistrationByClientId?: (PublicApplicationRegistrationGenqlSelection & { __args: {clientId: Scalars['String']} }) findApplicationRegistrationByUniversalIdentifier?: (ApplicationRegistrationGenqlSelection & { __args: {universalIdentifier: Scalars['String']} }) findManyApplicationRegistrations?: ApplicationRegistrationGenqlSelection @@ -5893,6 +5892,7 @@ export interface QueryGenqlSelection{ findApplicationRegistrationStats?: (ApplicationRegistrationStatsGenqlSelection & { __args: {id: Scalars['String']} }) findApplicationRegistrationVariables?: (ApplicationRegistrationVariableDTOGenqlSelection & { __args: {applicationRegistrationId: Scalars['String']} }) applicationRegistrationTarballUrl?: { __args: {id: Scalars['String']} } + getRoles?: RoleGenqlSelection previewMessageCampaignAudience?: (CampaignAudiencePreviewDTOGenqlSelection & { __args: {input: PreviewMessageCampaignAudienceInput} }) unsubscribeTopics?: UnsubscribeTopicGenqlSelection unsubscribePagePreviewUrl?: boolean | number @@ -6087,6 +6087,15 @@ export interface MutationGenqlSelection{ destroyViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DestroyViewGroupInput} }) runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} } uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} } + createApplicationRegistration?: (CreateApplicationRegistrationGenqlSelection & { __args: {input: CreateApplicationRegistrationInput} }) + updateApplicationRegistration?: (ApplicationRegistrationGenqlSelection & { __args: {input: UpdateApplicationRegistrationInput} }) + deleteApplicationRegistration?: { __args: {id: Scalars['String']} } + rotateApplicationRegistrationClientSecret?: (RotateClientSecretGenqlSelection & { __args: {id: Scalars['String']} }) + createApplicationRegistrationVariable?: (ApplicationRegistrationVariableGenqlSelection & { __args: {input: CreateApplicationRegistrationVariableInput} }) + updateApplicationRegistrationVariable?: (ApplicationRegistrationVariableGenqlSelection & { __args: {input: UpdateApplicationRegistrationVariableInput} }) + deleteApplicationRegistrationVariable?: { __args: {id: Scalars['String']} } + uploadAppTarball?: (ApplicationRegistrationGenqlSelection & { __args: {file: Scalars['Upload'], universalIdentifier?: (Scalars['String'] | null)} }) + transferApplicationRegistrationOwnership?: (ApplicationRegistrationGenqlSelection & { __args: {applicationRegistrationId: Scalars['String'], targetWorkspaceSubdomain: Scalars['String']} }) updateWorkspaceMemberRole?: (WorkspaceMemberGenqlSelection & { __args: {workspaceMemberId: Scalars['UUID'], roleId: Scalars['UUID']} }) createOneRole?: (RoleGenqlSelection & { __args: {createRoleInput: CreateRoleInput} }) updateOneRole?: (RoleGenqlSelection & { __args: {updateRoleInput: UpdateRoleInput} }) @@ -6097,15 +6106,6 @@ export interface MutationGenqlSelection{ upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} }) assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} } removeRoleFromAgent?: { __args: {agentId: Scalars['UUID']} } - createApplicationRegistration?: (CreateApplicationRegistrationGenqlSelection & { __args: {input: CreateApplicationRegistrationInput} }) - updateApplicationRegistration?: (ApplicationRegistrationGenqlSelection & { __args: {input: UpdateApplicationRegistrationInput} }) - deleteApplicationRegistration?: { __args: {id: Scalars['String']} } - rotateApplicationRegistrationClientSecret?: (RotateClientSecretGenqlSelection & { __args: {id: Scalars['String']} }) - createApplicationRegistrationVariable?: (ApplicationRegistrationVariableGenqlSelection & { __args: {input: CreateApplicationRegistrationVariableInput} }) - updateApplicationRegistrationVariable?: (ApplicationRegistrationVariableGenqlSelection & { __args: {input: UpdateApplicationRegistrationVariableInput} }) - deleteApplicationRegistrationVariable?: { __args: {id: Scalars['String']} } - uploadAppTarball?: (ApplicationRegistrationGenqlSelection & { __args: {file: Scalars['Upload'], universalIdentifier?: (Scalars['String'] | null)} }) - transferApplicationRegistrationOwnership?: (ApplicationRegistrationGenqlSelection & { __args: {applicationRegistrationId: Scalars['String'], targetWorkspaceSubdomain: Scalars['String']} }) sendEmailViaEmailingDomain?: (SendEmailViaDomainOutputGenqlSelection & { __args: {input: SendEmailViaDomainInput} }) sendMessageCampaign?: (SendMessageCampaignOutputDTOGenqlSelection & { __args: {input: SendMessageCampaignInput} }) createUnsubscribeTopic?: (UnsubscribeTopicGenqlSelection & { __args: {input: CreateUnsubscribeTopicInput} }) @@ -6474,6 +6474,18 @@ export interface WorkspaceMigrationInput {actions: WorkspaceMigrationDeleteActio export interface WorkspaceMigrationDeleteActionInput {type: WorkspaceMigrationActionType,metadataName: AllMetadataName,universalIdentifier: Scalars['String']} +export interface CreateApplicationRegistrationInput {name: Scalars['String'],universalIdentifier?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null)} + +export interface UpdateApplicationRegistrationInput {id: Scalars['String'],update: UpdateApplicationRegistrationPayload} + +export interface UpdateApplicationRegistrationPayload {name?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),isListed?: (Scalars['Boolean'] | null),isPreInstalled?: (Scalars['Boolean'] | null)} + +export interface CreateApplicationRegistrationVariableInput {applicationRegistrationId: Scalars['String'],key: Scalars['String'],value: Scalars['String'],description?: (Scalars['String'] | null),isSecret?: (Scalars['Boolean'] | null)} + +export interface UpdateApplicationRegistrationVariableInput {id: Scalars['String'],update: UpdateApplicationRegistrationVariablePayload} + +export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),resetValue?: (Scalars['Boolean'] | null),description?: (Scalars['String'] | null)} + export interface CreateRoleInput {id?: (Scalars['String'] | null),label: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),canUpdateAllSettings?: (Scalars['Boolean'] | null),canAccessAllTools?: (Scalars['Boolean'] | null),canReadAllObjectRecords?: (Scalars['Boolean'] | null),canUpdateAllObjectRecords?: (Scalars['Boolean'] | null),canSoftDeleteAllObjectRecords?: (Scalars['Boolean'] | null),canDestroyAllObjectRecords?: (Scalars['Boolean'] | null),canBeAssignedToUsers?: (Scalars['Boolean'] | null),canBeAssignedToAgents?: (Scalars['Boolean'] | null),canBeAssignedToApiKeys?: (Scalars['Boolean'] | null)} export interface UpdateRoleInput {update: UpdateRolePayload, @@ -6498,18 +6510,6 @@ export interface RowLevelPermissionPredicateInput {id?: (Scalars['UUID'] | null) export interface RowLevelPermissionPredicateGroupInput {id?: (Scalars['UUID'] | null),objectMetadataId: Scalars['UUID'],parentRowLevelPermissionPredicateGroupId?: (Scalars['UUID'] | null),logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator,positionInRowLevelPermissionPredicateGroup?: (Scalars['Float'] | null)} -export interface CreateApplicationRegistrationInput {name: Scalars['String'],universalIdentifier?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null)} - -export interface UpdateApplicationRegistrationInput {id: Scalars['String'],update: UpdateApplicationRegistrationPayload} - -export interface UpdateApplicationRegistrationPayload {name?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),isListed?: (Scalars['Boolean'] | null),isPreInstalled?: (Scalars['Boolean'] | null)} - -export interface CreateApplicationRegistrationVariableInput {applicationRegistrationId: Scalars['String'],key: Scalars['String'],value: Scalars['String'],description?: (Scalars['String'] | null),isSecret?: (Scalars['Boolean'] | null)} - -export interface UpdateApplicationRegistrationVariableInput {id: Scalars['String'],update: UpdateApplicationRegistrationVariablePayload} - -export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),resetValue?: (Scalars['Boolean'] | null),description?: (Scalars['String'] | null)} - export interface SendEmailViaDomainInput {emailingDomainId: Scalars['String'],to: Scalars['String'][],cc?: (Scalars['String'][] | null),bcc?: (Scalars['String'][] | null),subject: Scalars['String'],text: Scalars['String'],html?: (Scalars['String'] | null),from: Scalars['String'],replyTo?: (Scalars['String'][] | null)} export interface SendMessageCampaignInput {listId: Scalars['String'],unsubscribeTopicId?: (Scalars['String'] | null),subject: Scalars['String'],body: Scalars['String'],fromAddress: Scalars['String']} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index e5fa748002..a672efb901 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -6400,9 +6400,6 @@ export default { ] } ], - "getRoles": [ - 29 - ], "findApplicationRegistrationByClientId": [ 199, { @@ -6460,6 +6457,9 @@ export default { ] } ], + "getRoles": [ + 29 + ], "previewMessageCampaignAudience": [ 281, { @@ -7994,109 +7994,11 @@ export default { ] } ], - "updateWorkspaceMemberRole": [ - 20, - { - "workspaceMemberId": [ - 3, - "UUID!" - ], - "roleId": [ - 3, - "UUID!" - ] - } - ], - "createOneRole": [ - 29, - { - "createRoleInput": [ - 443, - "CreateRoleInput!" - ] - } - ], - "updateOneRole": [ - 29, - { - "updateRoleInput": [ - 444, - "UpdateRoleInput!" - ] - } - ], - "deleteOneRole": [ - 1, - { - "roleId": [ - 3, - "UUID!" - ] - } - ], - "upsertObjectPermissions": [ - 16, - { - "upsertObjectPermissionsInput": [ - 446, - "UpsertObjectPermissionsInput!" - ] - } - ], - "upsertPermissionFlags": [ - 27, - { - "upsertPermissionFlagsInput": [ - 448, - "UpsertPermissionFlagsInput!" - ] - } - ], - "upsertFieldPermissions": [ - 26, - { - "upsertFieldPermissionsInput": [ - 449, - "UpsertFieldPermissionsInput!" - ] - } - ], - "upsertRowLevelPermissionPredicates": [ - 232, - { - "input": [ - 451, - "UpsertRowLevelPermissionPredicatesInput!" - ] - } - ], - "assignRoleToAgent": [ - 6, - { - "agentId": [ - 3, - "UUID!" - ], - "roleId": [ - 3, - "UUID!" - ] - } - ], - "removeRoleFromAgent": [ - 6, - { - "agentId": [ - 3, - "UUID!" - ] - } - ], "createApplicationRegistration": [ 198, { "input": [ - 454, + 443, "CreateApplicationRegistrationInput!" ] } @@ -8105,7 +8007,7 @@ export default { 8, { "input": [ - 455, + 444, "UpdateApplicationRegistrationInput!" ] } @@ -8132,7 +8034,7 @@ export default { 5, { "input": [ - 457, + 446, "CreateApplicationRegistrationVariableInput!" ] } @@ -8141,7 +8043,7 @@ export default { 5, { "input": [ - 458, + 447, "UpdateApplicationRegistrationVariableInput!" ] } @@ -8180,6 +8082,104 @@ export default { ] } ], + "updateWorkspaceMemberRole": [ + 20, + { + "workspaceMemberId": [ + 3, + "UUID!" + ], + "roleId": [ + 3, + "UUID!" + ] + } + ], + "createOneRole": [ + 29, + { + "createRoleInput": [ + 449, + "CreateRoleInput!" + ] + } + ], + "updateOneRole": [ + 29, + { + "updateRoleInput": [ + 450, + "UpdateRoleInput!" + ] + } + ], + "deleteOneRole": [ + 1, + { + "roleId": [ + 3, + "UUID!" + ] + } + ], + "upsertObjectPermissions": [ + 16, + { + "upsertObjectPermissionsInput": [ + 452, + "UpsertObjectPermissionsInput!" + ] + } + ], + "upsertPermissionFlags": [ + 27, + { + "upsertPermissionFlagsInput": [ + 454, + "UpsertPermissionFlagsInput!" + ] + } + ], + "upsertFieldPermissions": [ + 26, + { + "upsertFieldPermissionsInput": [ + 455, + "UpsertFieldPermissionsInput!" + ] + } + ], + "upsertRowLevelPermissionPredicates": [ + 232, + { + "input": [ + 457, + "UpsertRowLevelPermissionPredicatesInput!" + ] + } + ], + "assignRoleToAgent": [ + 6, + { + "agentId": [ + 3, + "UUID!" + ], + "roleId": [ + 3, + "UUID!" + ] + } + ], + "removeRoleFromAgent": [ + 6, + { + "agentId": [ + 3, + "UUID!" + ] + } + ], "sendEmailViaEmailingDomain": [ 282, { @@ -10950,6 +10950,99 @@ export default { ] }, "WorkspaceMigrationActionType": {}, + "CreateApplicationRegistrationInput": { + "name": [ + 1 + ], + "universalIdentifier": [ + 1 + ], + "oAuthRedirectUris": [ + 1 + ], + "oAuthScopes": [ + 1 + ], + "__typename": [ + 1 + ] + }, + "UpdateApplicationRegistrationInput": { + "id": [ + 1 + ], + "update": [ + 445 + ], + "__typename": [ + 1 + ] + }, + "UpdateApplicationRegistrationPayload": { + "name": [ + 1 + ], + "oAuthRedirectUris": [ + 1 + ], + "oAuthScopes": [ + 1 + ], + "isListed": [ + 6 + ], + "isPreInstalled": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "CreateApplicationRegistrationVariableInput": { + "applicationRegistrationId": [ + 1 + ], + "key": [ + 1 + ], + "value": [ + 1 + ], + "description": [ + 1 + ], + "isSecret": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "UpdateApplicationRegistrationVariableInput": { + "id": [ + 1 + ], + "update": [ + 448 + ], + "__typename": [ + 1 + ] + }, + "UpdateApplicationRegistrationVariablePayload": { + "value": [ + 1 + ], + "resetValue": [ + 6 + ], + "description": [ + 1 + ], + "__typename": [ + 1 + ] + }, "CreateRoleInput": { "id": [ 1 @@ -10996,7 +11089,7 @@ export default { }, "UpdateRoleInput": { "update": [ - 445 + 451 ], "id": [ 3 @@ -11051,7 +11144,7 @@ export default { 3 ], "objectPermissions": [ - 447 + 453 ], "__typename": [ 1 @@ -11093,7 +11186,7 @@ export default { 3 ], "fieldPermissions": [ - 450 + 456 ], "__typename": [ 1 @@ -11124,10 +11217,10 @@ export default { 3 ], "predicates": [ - 452 + 458 ], "predicateGroups": [ - 453 + 459 ], "__typename": [ 1 @@ -11185,99 +11278,6 @@ export default { 1 ] }, - "CreateApplicationRegistrationInput": { - "name": [ - 1 - ], - "universalIdentifier": [ - 1 - ], - "oAuthRedirectUris": [ - 1 - ], - "oAuthScopes": [ - 1 - ], - "__typename": [ - 1 - ] - }, - "UpdateApplicationRegistrationInput": { - "id": [ - 1 - ], - "update": [ - 456 - ], - "__typename": [ - 1 - ] - }, - "UpdateApplicationRegistrationPayload": { - "name": [ - 1 - ], - "oAuthRedirectUris": [ - 1 - ], - "oAuthScopes": [ - 1 - ], - "isListed": [ - 6 - ], - "isPreInstalled": [ - 6 - ], - "__typename": [ - 1 - ] - }, - "CreateApplicationRegistrationVariableInput": { - "applicationRegistrationId": [ - 1 - ], - "key": [ - 1 - ], - "value": [ - 1 - ], - "description": [ - 1 - ], - "isSecret": [ - 6 - ], - "__typename": [ - 1 - ] - }, - "UpdateApplicationRegistrationVariableInput": { - "id": [ - 1 - ], - "update": [ - 459 - ], - "__typename": [ - 1 - ] - }, - "UpdateApplicationRegistrationVariablePayload": { - "value": [ - 1 - ], - "resetValue": [ - 6 - ], - "description": [ - 1 - ], - "__typename": [ - 1 - ] - }, "SendEmailViaDomainInput": { "emailingDomainId": [ 1 diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.module.ts index 75ec535d2b..7cdd031f01 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; +import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { ApplicationManifestModule } from 'src/engine/core-modules/application/application-manifest/application-manifest.module'; import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module'; @@ -19,6 +20,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache imports: [ TypeOrmModule.forFeature([ApplicationRegistrationEntity]), ApplicationModule, + ApplicationRegistrationModule, ApplicationManifestModule, ApplicationPackageModule, CacheLockModule, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts index 57c46c1927..9ac2109645 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts @@ -15,7 +15,9 @@ import { ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; +import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum'; +import { ManifestAssetUrlResolverService } from 'src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationPackageFetcherService } from 'src/engine/core-modules/application/application-package/application-package-fetcher.service'; @@ -55,6 +57,7 @@ export class ApplicationInstallService { @InjectRepository(ApplicationRegistrationEntity) private readonly appRegistrationRepository: Repository, private readonly applicationService: ApplicationService, + private readonly applicationRegistrationService: ApplicationRegistrationService, private readonly applicationPackageFetcherService: ApplicationPackageFetcherService, private readonly applicationVersionValidationService: ApplicationVersionValidationService, private readonly applicationSyncService: ApplicationSyncService, @@ -65,6 +68,7 @@ export class ApplicationInstallService { @InjectMessageQueue(MessageQueue.logicFunctionQueue) private readonly messageQueueService: MessageQueueService, private readonly workspaceCacheService: WorkspaceCacheService, + private readonly manifestAssetUrlResolverService: ManifestAssetUrlResolverService, ) {} async installApplication(params: { @@ -270,6 +274,12 @@ export class ApplicationInstallService { universalIdentifier, }); + await this.refreshRegistrationFromInstall({ + appRegistration, + manifest: resolvedPackage.manifest, + installedVersion: newVersion, + }); + this.logger.log( `Successfully installed app ${universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`, ); @@ -297,6 +307,26 @@ export class ApplicationInstallService { } } + private async refreshRegistrationFromInstall(params: { + appRegistration: ApplicationRegistrationEntity; + manifest: Manifest; + installedVersion: string; + }): Promise { + const { appRegistration, manifest, installedVersion } = params; + + await this.applicationRegistrationService.updateFromManifest({ + applicationRegistrationId: appRegistration.id, + manifest: this.manifestAssetUrlResolverService.resolveFromRegistration({ + sourceType: appRegistration.sourceType, + sourcePackage: appRegistration.sourcePackage, + manifest, + version: installedVersion, + }), + latestAvailableVersion: installedVersion, + preventVersionDowngrade: true, + }); + } + private async runPreInstallHook(params: { manifest: Manifest; workspaceId: string; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/should-refresh-application-registration-on-install.util.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/should-refresh-application-registration-on-install.util.spec.ts new file mode 100644 index 0000000000..93162ed429 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/__tests__/should-refresh-application-registration-on-install.util.spec.ts @@ -0,0 +1,57 @@ +import { shouldRefreshApplicationRegistrationOnInstall } from 'src/engine/core-modules/application/application-install/utils/should-refresh-application-registration-on-install.util'; + +describe('shouldRefreshApplicationRegistrationOnInstall', () => { + it('should refresh when latestAvailableVersion is null', () => { + expect( + shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: '1.0.0', + latestAvailableVersion: null, + }), + ).toBe(true); + }); + + it('should refresh when latestAvailableVersion is not a valid semver', () => { + expect( + shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: '1.0.0', + latestAvailableVersion: 'not-a-version', + }), + ).toBe(true); + }); + + it('should refresh when installed version is newer than latestAvailableVersion', () => { + expect( + shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: '2.0.0', + latestAvailableVersion: '1.0.0', + }), + ).toBe(true); + }); + + it('should refresh when installed version equals latestAvailableVersion', () => { + expect( + shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: '1.2.3', + latestAvailableVersion: '1.2.3', + }), + ).toBe(true); + }); + + it('should not refresh when installed version is older than latestAvailableVersion', () => { + expect( + shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: '1.0.0', + latestAvailableVersion: '2.0.0', + }), + ).toBe(false); + }); + + it('should not refresh when installed version is not a valid semver and latestAvailableVersion is valid', () => { + expect( + shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: 'not-a-version', + latestAvailableVersion: '1.0.0', + }), + ).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/utils/should-refresh-application-registration-on-install.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/should-refresh-application-registration-on-install.util.ts new file mode 100644 index 0000000000..352c851f5b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/utils/should-refresh-application-registration-on-install.util.ts @@ -0,0 +1,23 @@ +import semver from 'semver'; +import { isDefined } from 'twenty-shared/utils'; + +export const shouldRefreshApplicationRegistrationOnInstall = ({ + installedVersion, + latestAvailableVersion, +}: { + installedVersion: string; + latestAvailableVersion: string | null; +}): boolean => { + if ( + !isDefined(latestAvailableVersion) || + !isDefined(semver.valid(latestAvailableVersion)) + ) { + return true; + } + + if (!isDefined(semver.valid(installedVersion))) { + return false; + } + + return semver.gte(installedVersion, latestAvailableVersion); +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service.ts index fbbd5e2819..c1e8fd55ec 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service.ts @@ -3,9 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum'; import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service'; -import { buildRegistryCdnUrl } from 'src/engine/core-modules/application/application-marketplace/utils/build-registry-cdn-url.util'; -import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { ManifestAssetUrlResolverService } from 'src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service'; @Injectable() export class MarketplaceCatalogSyncService { @@ -14,7 +12,7 @@ export class MarketplaceCatalogSyncService { constructor( private readonly applicationRegistrationService: ApplicationRegistrationService, private readonly marketplaceService: MarketplaceService, - private readonly twentyConfigService: TwentyConfigService, + private readonly manifestAssetUrlResolverService: ManifestAssetUrlResolverService, ) {} async syncCatalog(): Promise { @@ -45,18 +43,12 @@ export class MarketplaceCatalogSyncService { const universalIdentifier = fetchedManifest.application.universalIdentifier; - const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL'); - - const manifestWithResolvedUrls = resolveManifestAssetUrls( - fetchedManifest, - (filePath) => - buildRegistryCdnUrl({ - cdnBaseUrl, - packageName: pkg.name, - version: pkg.version, - filePath, - }), - ); + const manifestWithResolvedUrls = + this.manifestAssetUrlResolverService.resolveFromRegistrySource({ + manifest: fetchedManifest, + packageName: pkg.name, + version: pkg.version, + }); await this.applicationRegistrationService.upsertFromCatalog({ universalIdentifier, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts index 2d1837b753..201e4ba0f6 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts @@ -6,9 +6,11 @@ import { ApplicationRegistrationResolver } from 'src/engine/core-modules/applica import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; import { ApplicationRegistrationVariableModule } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.module'; import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service'; +import { ManifestAssetUrlResolverService } from 'src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service'; import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module'; import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; @@ -27,6 +29,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ ApplicationRegistrationVariableModule, ApplicationModule, ApplicationPackageModule, + CacheLockModule, DomainServerConfigModule, FeatureFlagModule, PermissionsModule, @@ -38,10 +41,12 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ ApplicationRegistrationService, ApplicationRegistrationResolver, ApplicationTarballService, + ManifestAssetUrlResolverService, ], exports: [ ApplicationRegistrationService, ApplicationRegistrationVariableModule, + ManifestAssetUrlResolverService, ], }) export class ApplicationRegistrationModule {} diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts index b172acc23f..7e747a5668 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import crypto from 'crypto'; @@ -10,6 +10,8 @@ import { ILike, type FindOptionsWhere, type Repository } from 'typeorm'; import { v4 } from 'uuid'; import { ALL_OAUTH_SCOPES } from 'src/engine/core-modules/application/application-oauth/constants/oauth-scopes'; +import { shouldRefreshApplicationRegistrationOnInstall } from 'src/engine/core-modules/application/application-install/utils/should-refresh-application-registration-on-install.util'; +import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service'; import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; import { TWENTY_CLI_APPLICATION_REGISTRATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-cli-application-registration.constant'; import { @@ -79,6 +81,8 @@ export type ApplicationRegistrationCatalogCard = { @Injectable() export class ApplicationRegistrationService { + private readonly logger = new Logger(ApplicationRegistrationService.name); + constructor( @InjectRepository(ApplicationRegistrationEntity) private readonly applicationRegistrationRepository: Repository, @@ -87,6 +91,7 @@ export class ApplicationRegistrationService { @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService, + private readonly cacheLockService: CacheLockService, ) {} async findMany( @@ -286,22 +291,47 @@ export class ApplicationRegistrationService { applicationRegistrationId, manifest, sourceType, + latestAvailableVersion, + preventVersionDowngrade = false, }: { applicationRegistrationId: string; manifest: Manifest; sourceType?: ApplicationRegistrationSourceType; + latestAvailableVersion?: string; + preventVersionDowngrade?: boolean; }): Promise { - const existing = await this.applicationRegistrationRepository.findOneOrFail( - { where: { id: applicationRegistrationId } }, - ); + await this.cacheLockService.withLock(async () => { + const existing = + await this.applicationRegistrationRepository.findOneOrFail({ + where: { id: applicationRegistrationId }, + }); - await this.applicationRegistrationRepository.save({ - ...existing, - name: manifest.application.displayName, - manifest, - ...fromManifestApplicationToDisplayFields(manifest.application), - ...(sourceType !== undefined && { sourceType }), - }); + if ( + preventVersionDowngrade && + isDefined(latestAvailableVersion) && + !shouldRefreshApplicationRegistrationOnInstall({ + installedVersion: latestAvailableVersion, + latestAvailableVersion: existing.latestAvailableVersion, + }) + ) { + this.logger.log( + `Skipping registration update for ${existing.universalIdentifier}: version ${latestAvailableVersion} is older than latest available version ${existing.latestAvailableVersion}`, + ); + + return; + } + + await this.applicationRegistrationRepository.save({ + ...existing, + name: manifest.application.displayName, + manifest, + ...fromManifestApplicationToDisplayFields(manifest.application), + ...(sourceType !== undefined && { sourceType }), + ...(latestAvailableVersion !== undefined && { + latestAvailableVersion, + }), + }); + }, `application-registration-update:${applicationRegistrationId}`); } async delete(id: string, ownerWorkspaceId: string): Promise { diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service.ts new file mode 100644 index 0000000000..d4d2e77f21 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/manifest-asset-url-resolver.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; + +import { type Manifest } from 'twenty-shared/application'; +import { isDefined } from 'twenty-shared/utils'; + +import { buildRegistryCdnUrl } from 'src/engine/core-modules/application/application-marketplace/utils/build-registry-cdn-url.util'; +import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util'; +import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +@Injectable() +export class ManifestAssetUrlResolverService { + constructor(private readonly twentyConfigService: TwentyConfigService) {} + + resolveFromRegistrySource({ + manifest, + packageName, + version, + }: { + manifest: Manifest; + packageName: string; + version: string; + }): Manifest { + const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL'); + + return resolveManifestAssetUrls(manifest, (filePath) => + buildRegistryCdnUrl({ cdnBaseUrl, packageName, version, filePath }), + ); + } + + resolveFromRegistration({ + sourceType, + sourcePackage, + manifest, + version, + }: { + sourceType: ApplicationRegistrationSourceType; + sourcePackage: string | null; + manifest: Manifest; + version: string; + }): Manifest { + if ( + sourceType !== ApplicationRegistrationSourceType.NPM || + !isDefined(sourcePackage) + ) { + return manifest; + } + + return this.resolveFromRegistrySource({ + manifest, + packageName: sourcePackage, + version, + }); + } +}