refactor(server): regroup application resolvers by resource and unify install permission flag (#22532)

Part of the app settings architecture cleanup
(twentyhq/core-team-issues#2456) — implements the API-surface regroup
Charles asked for in #20825 ("in application resolvers we have
uninstall, upgrade, findMany, etc. and for some reason install is part
of the marketplace, and they are not protected by same guards").

## Changes

**Resolver regroup by resource** (GraphQL operation names and signatures
unchanged):
- `installApplication` + `installMarketplaceApp` (deprecation preserved)
move from the marketplace resolver into
`application-install.resolver.ts`, next to
`findManyApplications`/`findOneApplication`/`uninstallApplication`.
- `uninstallApplication` moves from the manifest resolver into
`application-install.resolver.ts`.
- `runWorkspaceMigration` is deleted outright (unused — no consumer
anywhere in the repo, front/SDK/e2e/docs); the now-empty manifest
resolver is deleted. Its `AllMetadataName` GraphQL enum registration
moves to `collection-hash.dto.ts` (its remaining consumer).
- `generateApplicationToken` moves from the development resolver into
`application-oauth.resolver.ts` next to `renewApplicationToken`, keeping
its effective guards (`WorkspaceAuthGuard` +
`SettingsPermissionGuard(APPLICATIONS)`) and its token-bucket throttle
verbatim.
- `upgradeApplication` stays in the upgrade resolver (moving it into the
install resolver would create a module cycle — the upgrade module
imports the install module).
- Marketplace resolver now only holds catalog concerns:
`findManyMarketplaceApps`, `findMarketplaceAppDetail`,
`syncMarketplaceCatalog`.

**Permission unification** (the only behavior change):
`installApplication`, `installMarketplaceApp` and `upgradeApplication`
move from `MARKETPLACE_APPS` to `APPLICATIONS`, matching uninstall and
the find queries. Front-end install/upgrade button gating updated
accordingly (`SettingsApplicationDetails` /
`SettingsAvailableApplicationDetails`).

**Module wiring**: `MarketplaceModule` no longer imports
`ApplicationInstallModule` (only the moved resolver needed it);
`ApplicationInstallModule` now imports `MarketplaceModule` — no cycle.
Exception filters follow the moved operations
(`ApplicationRegistrationExceptionFilter` on the install resolver;
`ApplicationExceptionFilter` on the oauth resolver, which also fixes
`renewApplicationToken`'s previously unmapped FORBIDDEN).

**Codegen**: `twenty-client-sdk` metadata client regenerated for the new
schema ordering (pure reordering — no field changes); all front
`graphql:generate` configurations produced zero diffs.

## Explicitly kept (per review discussion)

`installMarketplaceApp` (deprecated) and `generateApplicationToken` are
kept for SDK back-compat despite having no current consumers.
`runWorkspaceMigration` was also consumer-less but, unlike those two,
had no back-compat rationale (not a deprecated alias, not a token
primitive), so it is removed rather than relocated.

## Deferred follow-ups (guard inconsistencies found in the audit,
intentionally NOT changed here)

- `findApplicationRegistrationByUniversalIdentifier` uses
`NoPermissionGuard` and returns the full registration entity, bypassing
the `API_KEYS_AND_WEBHOOKS` gate that `findOneApplicationRegistration`
enforces on the same data (SDK CLI `ensure-app-registration` depends on
it today).
- `upgradeApplication` alone requires `UserAuthGuard` — an API key can
install but not upgrade.
- `uploadAppTarball` (`MARKETPLACE_APPS`) and
`transferApplicationRegistrationOwnership` (`APPLICATIONS`) are
flag-inconsistent with the rest of registration CRUD
(`API_KEYS_AND_WEBHOOKS`).
- `syncMarketplaceCatalog` triggers an instance-wide job but is gated
only by a per-workspace settings flag.

## Verification

- `npx nx typecheck twenty-server` / `twenty-front` ✓;
`lint:diff-with-main` clean for both
- `npx jest "application"` in twenty-server: 30 suites / 154 tests
passed
- Server boots with the new module graph (DI verified at runtime);
codegen run against the live server
- Repo-wide grep: no remaining imports of the deleted manifest resolver

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22532?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2026-07-05 00:24:12 +02:00
committed by GitHub
parent 59c16ef46f
commit 4e43a0fb4e
18 changed files with 1027 additions and 1214 deletions
@@ -2192,6 +2192,74 @@ type DeletedWorkspaceMember {
userWorkspaceId: UUID
}
type MarketplaceApp {
id: String!
name: String!
description: String!
author: String!
category: String!
logo: String
sourcePackage: String
isFeatured: Boolean!
}
type MarketplaceAppRoleObjectPermission {
universalIdentifier: String!
objectUniversalIdentifier: String!
canReadObjectRecords: Boolean
canUpdateObjectRecords: Boolean
canSoftDeleteObjectRecords: Boolean
canDestroyObjectRecords: Boolean
}
type MarketplaceAppRoleFieldPermission {
universalIdentifier: String!
objectUniversalIdentifier: String!
fieldUniversalIdentifier: String!
canReadFieldValue: Boolean
canUpdateFieldValue: Boolean
}
type MarketplaceAppRole {
universalIdentifier: String!
label: String!
description: String
icon: String
canUpdateAllSettings: Boolean
canAccessAllTools: Boolean
canReadAllObjectRecords: Boolean
canUpdateAllObjectRecords: Boolean
canSoftDeleteAllObjectRecords: Boolean
canDestroyAllObjectRecords: Boolean
permissionFlagUniversalIdentifiers: [String!]
objectPermissions: [MarketplaceAppRoleObjectPermission!]
fieldPermissions: [MarketplaceAppRoleFieldPermission!]
}
type MarketplaceAppDetail {
universalIdentifier: String!
id: String!
name: String!
sourceType: ApplicationRegistrationSourceType!
sourcePackage: String
latestAvailableVersion: String
isListed: Boolean!
isFeatured: Boolean!
description: String
author: String
category: String
logo: String
websiteUrl: String
aboutDescription: String
termsUrl: String
emailSupport: String
issueReportUrl: String
screenshots: [String!]!
defaultRoleUniversalIdentifier: String
roles: [MarketplaceAppRole!]
manifest: JSON @deprecated(reason: "Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead")
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
@@ -2412,74 +2480,6 @@ type File {
createdAt: DateTime!
}
type MarketplaceApp {
id: String!
name: String!
description: String!
author: String!
category: String!
logo: String
sourcePackage: String
isFeatured: Boolean!
}
type MarketplaceAppRoleObjectPermission {
universalIdentifier: String!
objectUniversalIdentifier: String!
canReadObjectRecords: Boolean
canUpdateObjectRecords: Boolean
canSoftDeleteObjectRecords: Boolean
canDestroyObjectRecords: Boolean
}
type MarketplaceAppRoleFieldPermission {
universalIdentifier: String!
objectUniversalIdentifier: String!
fieldUniversalIdentifier: String!
canReadFieldValue: Boolean
canUpdateFieldValue: Boolean
}
type MarketplaceAppRole {
universalIdentifier: String!
label: String!
description: String
icon: String
canUpdateAllSettings: Boolean
canAccessAllTools: Boolean
canReadAllObjectRecords: Boolean
canUpdateAllObjectRecords: Boolean
canSoftDeleteAllObjectRecords: Boolean
canDestroyAllObjectRecords: Boolean
permissionFlagUniversalIdentifiers: [String!]
objectPermissions: [MarketplaceAppRoleObjectPermission!]
fieldPermissions: [MarketplaceAppRoleFieldPermission!]
}
type MarketplaceAppDetail {
universalIdentifier: String!
id: String!
name: String!
sourceType: ApplicationRegistrationSourceType!
sourcePackage: String
latestAvailableVersion: String
isListed: Boolean!
isFeatured: Boolean!
description: String
author: String
category: String
logo: String
websiteUrl: String
aboutDescription: String
termsUrl: String
emailSupport: String
issueReportUrl: String
screenshots: [String!]!
defaultRoleUniversalIdentifier: String
roles: [MarketplaceAppRole!]
manifest: JSON @deprecated(reason: "Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead")
}
type PublicDomain {
id: UUID!
domain: String!
@@ -3151,6 +3151,8 @@ type Query {
getViewGroup(id: String!): ViewGroup
findManyApplications: [Application!]!
findOneApplication(id: UUID, universalIdentifier: UUID): Application!
findManyMarketplaceApps: [MarketplaceApp!]!
findMarketplaceAppDetail(universalIdentifier: String!): MarketplaceAppDetail!
findApplicationRegistrationByClientId(clientId: String!): PublicApplicationRegistration
findApplicationRegistrationByUniversalIdentifier(universalIdentifier: String!): ApplicationRegistration
findManyApplicationRegistrations: [ApplicationRegistration!]!
@@ -3200,8 +3202,6 @@ type Query {
getAddressDetails(placeId: String!, token: String!): PlaceDetailsResult!
getUsageAnalytics(input: UsageAnalyticsInput): UsageAnalytics!
findManyPublicDomains: [PublicDomain!]!
findManyMarketplaceApps: [MarketplaceApp!]!
findMarketplaceAppDetail(universalIdentifier: String!): MarketplaceAppDetail!
}
input GetApiKeyInput {
@@ -3406,8 +3406,10 @@ type Mutation {
updateManyViewGroups(inputs: [UpdateViewGroupInput!]!): [ViewGroup!]!
deleteViewGroup(input: DeleteViewGroupInput!): ViewGroup!
destroyViewGroup(input: DestroyViewGroupInput!): ViewGroup!
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean! @deprecated(reason: "Use installApplication instead")
installApplication(universalIdentifier: String!, version: String): Application!
uninstallApplication(universalIdentifier: String!): Boolean!
syncMarketplaceCatalog: Boolean!
createApplicationRegistration(input: CreateApplicationRegistrationInput!): CreateApplicationRegistration!
updateApplicationRegistration(input: UpdateApplicationRegistrationInput!): ApplicationRegistration!
deleteApplicationRegistration(id: String!): Boolean!
@@ -3507,14 +3509,11 @@ type Mutation {
deletePublicDomain(domain: String!): Boolean!
checkPublicDomainValidRecords(domain: String!): DomainValidRecords
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
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!
syncApplication(manifest: JSON!, dryRun: Boolean): WorkspaceMigration!
uploadApplicationFile(file: Upload!, applicationUniversalIdentifier: String!, fileFolder: FileFolder!, filePath: String!): File!
upgradeApplication(appRegistrationId: String!, targetVersion: String!): Boolean!
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
renewApplicationToken(applicationRefreshToken: String!): ApplicationTokenPair!
}
@@ -4308,22 +4307,6 @@ input DestroyViewGroupInput {
id: UUID!
}
input WorkspaceMigrationInput {
actions: [WorkspaceMigrationDeleteActionInput!]!
}
input WorkspaceMigrationDeleteActionInput {
type: WorkspaceMigrationActionType!
metadataName: AllMetadataName!
universalIdentifier: String!
}
enum WorkspaceMigrationActionType {
delete
create
update
}
input CreateApplicationRegistrationInput {
name: String!
universalIdentifier: String