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
@@ -1845,6 +1845,80 @@ export interface DeletedWorkspaceMember {
__typename: 'DeletedWorkspaceMember'
}
export interface MarketplaceApp {
id: Scalars['String']
name: Scalars['String']
description: Scalars['String']
author: Scalars['String']
category: Scalars['String']
logo?: Scalars['String']
sourcePackage?: Scalars['String']
isFeatured: Scalars['Boolean']
__typename: 'MarketplaceApp'
}
export interface MarketplaceAppRoleObjectPermission {
universalIdentifier: Scalars['String']
objectUniversalIdentifier: Scalars['String']
canReadObjectRecords?: Scalars['Boolean']
canUpdateObjectRecords?: Scalars['Boolean']
canSoftDeleteObjectRecords?: Scalars['Boolean']
canDestroyObjectRecords?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleObjectPermission'
}
export interface MarketplaceAppRoleFieldPermission {
universalIdentifier: Scalars['String']
objectUniversalIdentifier: Scalars['String']
fieldUniversalIdentifier: Scalars['String']
canReadFieldValue?: Scalars['Boolean']
canUpdateFieldValue?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleFieldPermission'
}
export interface MarketplaceAppRole {
universalIdentifier: Scalars['String']
label: Scalars['String']
description?: Scalars['String']
icon?: Scalars['String']
canUpdateAllSettings?: Scalars['Boolean']
canAccessAllTools?: Scalars['Boolean']
canReadAllObjectRecords?: Scalars['Boolean']
canUpdateAllObjectRecords?: Scalars['Boolean']
canSoftDeleteAllObjectRecords?: Scalars['Boolean']
canDestroyAllObjectRecords?: Scalars['Boolean']
permissionFlagUniversalIdentifiers?: Scalars['String'][]
objectPermissions?: MarketplaceAppRoleObjectPermission[]
fieldPermissions?: MarketplaceAppRoleFieldPermission[]
__typename: 'MarketplaceAppRole'
}
export interface MarketplaceAppDetail {
universalIdentifier: Scalars['String']
id: Scalars['String']
name: Scalars['String']
sourceType: ApplicationRegistrationSourceType
sourcePackage?: Scalars['String']
latestAvailableVersion?: Scalars['String']
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
description?: Scalars['String']
author?: Scalars['String']
category?: Scalars['String']
logo?: Scalars['String']
websiteUrl?: Scalars['String']
aboutDescription?: Scalars['String']
termsUrl?: Scalars['String']
emailSupport?: Scalars['String']
issueReportUrl?: Scalars['String']
screenshots: Scalars['String'][]
defaultRoleUniversalIdentifier?: Scalars['String']
roles?: MarketplaceAppRole[]
/** @deprecated Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead */
manifest?: Scalars['JSON']
__typename: 'MarketplaceAppDetail'
}
export interface BillingEntitlement {
key: BillingEntitlementKey
value: Scalars['Boolean']
@@ -2092,80 +2166,6 @@ export interface File {
__typename: 'File'
}
export interface MarketplaceApp {
id: Scalars['String']
name: Scalars['String']
description: Scalars['String']
author: Scalars['String']
category: Scalars['String']
logo?: Scalars['String']
sourcePackage?: Scalars['String']
isFeatured: Scalars['Boolean']
__typename: 'MarketplaceApp'
}
export interface MarketplaceAppRoleObjectPermission {
universalIdentifier: Scalars['String']
objectUniversalIdentifier: Scalars['String']
canReadObjectRecords?: Scalars['Boolean']
canUpdateObjectRecords?: Scalars['Boolean']
canSoftDeleteObjectRecords?: Scalars['Boolean']
canDestroyObjectRecords?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleObjectPermission'
}
export interface MarketplaceAppRoleFieldPermission {
universalIdentifier: Scalars['String']
objectUniversalIdentifier: Scalars['String']
fieldUniversalIdentifier: Scalars['String']
canReadFieldValue?: Scalars['Boolean']
canUpdateFieldValue?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleFieldPermission'
}
export interface MarketplaceAppRole {
universalIdentifier: Scalars['String']
label: Scalars['String']
description?: Scalars['String']
icon?: Scalars['String']
canUpdateAllSettings?: Scalars['Boolean']
canAccessAllTools?: Scalars['Boolean']
canReadAllObjectRecords?: Scalars['Boolean']
canUpdateAllObjectRecords?: Scalars['Boolean']
canSoftDeleteAllObjectRecords?: Scalars['Boolean']
canDestroyAllObjectRecords?: Scalars['Boolean']
permissionFlagUniversalIdentifiers?: Scalars['String'][]
objectPermissions?: MarketplaceAppRoleObjectPermission[]
fieldPermissions?: MarketplaceAppRoleFieldPermission[]
__typename: 'MarketplaceAppRole'
}
export interface MarketplaceAppDetail {
universalIdentifier: Scalars['String']
id: Scalars['String']
name: Scalars['String']
sourceType: ApplicationRegistrationSourceType
sourcePackage?: Scalars['String']
latestAvailableVersion?: Scalars['String']
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
description?: Scalars['String']
author?: Scalars['String']
category?: Scalars['String']
logo?: Scalars['String']
websiteUrl?: Scalars['String']
aboutDescription?: Scalars['String']
termsUrl?: Scalars['String']
emailSupport?: Scalars['String']
issueReportUrl?: Scalars['String']
screenshots: Scalars['String'][]
defaultRoleUniversalIdentifier?: Scalars['String']
roles?: MarketplaceAppRole[]
/** @deprecated Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead */
manifest?: Scalars['JSON']
__typename: 'MarketplaceAppDetail'
}
export interface PublicDomain {
id: Scalars['UUID']
domain: Scalars['String']
@@ -2763,6 +2763,8 @@ export interface Query {
getViewGroup?: ViewGroup
findManyApplications: Application[]
findOneApplication: Application
findManyMarketplaceApps: MarketplaceApp[]
findMarketplaceAppDetail: MarketplaceAppDetail
findApplicationRegistrationByClientId?: PublicApplicationRegistration
findApplicationRegistrationByUniversalIdentifier?: ApplicationRegistration
findManyApplicationRegistrations: ApplicationRegistration[]
@@ -2812,8 +2814,6 @@ export interface Query {
getAddressDetails: PlaceDetailsResult
getUsageAnalytics: UsageAnalytics
findManyPublicDomains: PublicDomain[]
findManyMarketplaceApps: MarketplaceApp[]
findMarketplaceAppDetail: MarketplaceAppDetail
__typename: 'Query'
}
@@ -2938,8 +2938,11 @@ export interface Mutation {
updateManyViewGroups: ViewGroup[]
deleteViewGroup: ViewGroup
destroyViewGroup: ViewGroup
runWorkspaceMigration: Scalars['Boolean']
/** @deprecated Use installApplication instead */
installMarketplaceApp: Scalars['Boolean']
installApplication: Application
uninstallApplication: Scalars['Boolean']
syncMarketplaceCatalog: Scalars['Boolean']
createApplicationRegistration: CreateApplicationRegistration
updateApplicationRegistration: ApplicationRegistration
deleteApplicationRegistration: Scalars['Boolean']
@@ -3039,23 +3042,17 @@ export interface Mutation {
deletePublicDomain: Scalars['Boolean']
checkPublicDomainValidRecords?: DomainValidRecords
createOneAppToken: AppToken
/** @deprecated Use installApplication instead */
installMarketplaceApp: Scalars['Boolean']
installApplication: Application
syncMarketplaceCatalog: Scalars['Boolean']
createDevelopmentApplication: DevelopmentApplication
generateApplicationToken: ApplicationTokenPair
syncApplication: WorkspaceMigration
uploadApplicationFile: File
upgradeApplication: Scalars['Boolean']
generateApplicationToken: ApplicationTokenPair
renewApplicationToken: ApplicationTokenPair
__typename: 'Mutation'
}
export type FileFolder = 'CorePicture' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient' | 'Dpa'
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
export interface Subscription {
@@ -4989,6 +4986,85 @@ export interface DeletedWorkspaceMemberGenqlSelection{
__scalar?: boolean | number
}
export interface MarketplaceAppGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
sourcePackage?: boolean | number
isFeatured?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleObjectPermissionGenqlSelection{
universalIdentifier?: boolean | number
objectUniversalIdentifier?: boolean | number
canReadObjectRecords?: boolean | number
canUpdateObjectRecords?: boolean | number
canSoftDeleteObjectRecords?: boolean | number
canDestroyObjectRecords?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleFieldPermissionGenqlSelection{
universalIdentifier?: boolean | number
objectUniversalIdentifier?: boolean | number
fieldUniversalIdentifier?: boolean | number
canReadFieldValue?: boolean | number
canUpdateFieldValue?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleGenqlSelection{
universalIdentifier?: boolean | number
label?: boolean | number
description?: boolean | number
icon?: boolean | number
canUpdateAllSettings?: boolean | number
canAccessAllTools?: boolean | number
canReadAllObjectRecords?: boolean | number
canUpdateAllObjectRecords?: boolean | number
canSoftDeleteAllObjectRecords?: boolean | number
canDestroyAllObjectRecords?: boolean | number
permissionFlagUniversalIdentifiers?: boolean | number
objectPermissions?: MarketplaceAppRoleObjectPermissionGenqlSelection
fieldPermissions?: MarketplaceAppRoleFieldPermissionGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppDetailGenqlSelection{
universalIdentifier?: boolean | number
id?: boolean | number
name?: boolean | number
sourceType?: boolean | number
sourcePackage?: boolean | number
latestAvailableVersion?: boolean | number
isListed?: boolean | number
isFeatured?: boolean | number
description?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
websiteUrl?: boolean | number
aboutDescription?: boolean | number
termsUrl?: boolean | number
emailSupport?: boolean | number
issueReportUrl?: boolean | number
screenshots?: boolean | number
defaultRoleUniversalIdentifier?: boolean | number
roles?: MarketplaceAppRoleGenqlSelection
/** @deprecated Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead */
manifest?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface BillingEntitlementGenqlSelection{
key?: boolean | number
value?: boolean | number
@@ -5268,85 +5344,6 @@ export interface FileGenqlSelection{
__scalar?: boolean | number
}
export interface MarketplaceAppGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
sourcePackage?: boolean | number
isFeatured?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleObjectPermissionGenqlSelection{
universalIdentifier?: boolean | number
objectUniversalIdentifier?: boolean | number
canReadObjectRecords?: boolean | number
canUpdateObjectRecords?: boolean | number
canSoftDeleteObjectRecords?: boolean | number
canDestroyObjectRecords?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleFieldPermissionGenqlSelection{
universalIdentifier?: boolean | number
objectUniversalIdentifier?: boolean | number
fieldUniversalIdentifier?: boolean | number
canReadFieldValue?: boolean | number
canUpdateFieldValue?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleGenqlSelection{
universalIdentifier?: boolean | number
label?: boolean | number
description?: boolean | number
icon?: boolean | number
canUpdateAllSettings?: boolean | number
canAccessAllTools?: boolean | number
canReadAllObjectRecords?: boolean | number
canUpdateAllObjectRecords?: boolean | number
canSoftDeleteAllObjectRecords?: boolean | number
canDestroyAllObjectRecords?: boolean | number
permissionFlagUniversalIdentifiers?: boolean | number
objectPermissions?: MarketplaceAppRoleObjectPermissionGenqlSelection
fieldPermissions?: MarketplaceAppRoleFieldPermissionGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppDetailGenqlSelection{
universalIdentifier?: boolean | number
id?: boolean | number
name?: boolean | number
sourceType?: boolean | number
sourcePackage?: boolean | number
latestAvailableVersion?: boolean | number
isListed?: boolean | number
isFeatured?: boolean | number
description?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
websiteUrl?: boolean | number
aboutDescription?: boolean | number
termsUrl?: boolean | number
emailSupport?: boolean | number
issueReportUrl?: boolean | number
screenshots?: boolean | number
defaultRoleUniversalIdentifier?: boolean | number
roles?: MarketplaceAppRoleGenqlSelection
/** @deprecated Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead */
manifest?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface PublicDomainGenqlSelection{
id?: boolean | number
domain?: boolean | number
@@ -5986,6 +5983,8 @@ export interface QueryGenqlSelection{
getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} })
findManyApplications?: ApplicationGenqlSelection
findOneApplication?: (ApplicationGenqlSelection & { __args?: {id?: (Scalars['UUID'] | null), universalIdentifier?: (Scalars['UUID'] | null)} })
findManyMarketplaceApps?: MarketplaceAppGenqlSelection
findMarketplaceAppDetail?: (MarketplaceAppDetailGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
findApplicationRegistrationByClientId?: (PublicApplicationRegistrationGenqlSelection & { __args: {clientId: Scalars['String']} })
findApplicationRegistrationByUniversalIdentifier?: (ApplicationRegistrationGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
findManyApplicationRegistrations?: ApplicationRegistrationGenqlSelection
@@ -6035,8 +6034,6 @@ export interface QueryGenqlSelection{
getAddressDetails?: (PlaceDetailsResultGenqlSelection & { __args: {placeId: Scalars['String'], token: Scalars['String']} })
getUsageAnalytics?: (UsageAnalyticsGenqlSelection & { __args?: {input?: (UsageAnalyticsInput | null)} })
findManyPublicDomains?: PublicDomainGenqlSelection
findManyMarketplaceApps?: MarketplaceAppGenqlSelection
findMarketplaceAppDetail?: (MarketplaceAppDetailGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -6186,8 +6183,11 @@ export interface MutationGenqlSelection{
updateManyViewGroups?: (ViewGroupGenqlSelection & { __args: {inputs: UpdateViewGroupInput[]} })
deleteViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DeleteViewGroupInput} })
destroyViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DestroyViewGroupInput} })
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
/** @deprecated Use installApplication instead */
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
installApplication?: (ApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} })
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
syncMarketplaceCatalog?: boolean | number
createApplicationRegistration?: (CreateApplicationRegistrationGenqlSelection & { __args: {input: CreateApplicationRegistrationInput} })
updateApplicationRegistration?: (ApplicationRegistrationGenqlSelection & { __args: {input: UpdateApplicationRegistrationInput} })
deleteApplicationRegistration?: { __args: {id: Scalars['String']} }
@@ -6287,15 +6287,11 @@ export interface MutationGenqlSelection{
deletePublicDomain?: { __args: {domain: Scalars['String']} }
checkPublicDomainValidRecords?: (DomainValidRecordsGenqlSelection & { __args: {domain: Scalars['String']} })
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
/** @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']} })
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON'], dryRun?: (Scalars['Boolean'] | null)} })
uploadApplicationFile?: (FileGenqlSelection & { __args: {file: Scalars['Upload'], applicationUniversalIdentifier: Scalars['String'], fileFolder: FileFolder, filePath: Scalars['String']} })
upgradeApplication?: { __args: {appRegistrationId: Scalars['String'], targetVersion: Scalars['String']} }
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
renewApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationRefreshToken: Scalars['String']} })
__typename?: boolean | number
__scalar?: boolean | number
@@ -6571,10 +6567,6 @@ export interface DestroyViewGroupInput {
/** The id of the view group to destroy. */
id: Scalars['UUID']}
export interface WorkspaceMigrationInput {actions: WorkspaceMigrationDeleteActionInput[]}
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}
@@ -7989,6 +7981,46 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const MarketplaceApp_possibleTypes: string[] = ['MarketplaceApp']
export const isMarketplaceApp = (obj?: { __typename?: any } | null): obj is MarketplaceApp => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceApp"')
return MarketplaceApp_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleObjectPermission_possibleTypes: string[] = ['MarketplaceAppRoleObjectPermission']
export const isMarketplaceAppRoleObjectPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleObjectPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleObjectPermission"')
return MarketplaceAppRoleObjectPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleFieldPermission_possibleTypes: string[] = ['MarketplaceAppRoleFieldPermission']
export const isMarketplaceAppRoleFieldPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleFieldPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleFieldPermission"')
return MarketplaceAppRoleFieldPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRole_possibleTypes: string[] = ['MarketplaceAppRole']
export const isMarketplaceAppRole = (obj?: { __typename?: any } | null): obj is MarketplaceAppRole => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRole"')
return MarketplaceAppRole_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppDetail_possibleTypes: string[] = ['MarketplaceAppDetail']
export const isMarketplaceAppDetail = (obj?: { __typename?: any } | null): obj is MarketplaceAppDetail => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppDetail"')
return MarketplaceAppDetail_possibleTypes.includes(obj.__typename)
}
const BillingEntitlement_possibleTypes: string[] = ['BillingEntitlement']
export const isBillingEntitlement = (obj?: { __typename?: any } | null): obj is BillingEntitlement => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingEntitlement"')
@@ -8277,46 +8309,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const MarketplaceApp_possibleTypes: string[] = ['MarketplaceApp']
export const isMarketplaceApp = (obj?: { __typename?: any } | null): obj is MarketplaceApp => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceApp"')
return MarketplaceApp_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleObjectPermission_possibleTypes: string[] = ['MarketplaceAppRoleObjectPermission']
export const isMarketplaceAppRoleObjectPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleObjectPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleObjectPermission"')
return MarketplaceAppRoleObjectPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleFieldPermission_possibleTypes: string[] = ['MarketplaceAppRoleFieldPermission']
export const isMarketplaceAppRoleFieldPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleFieldPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleFieldPermission"')
return MarketplaceAppRoleFieldPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRole_possibleTypes: string[] = ['MarketplaceAppRole']
export const isMarketplaceAppRole = (obj?: { __typename?: any } | null): obj is MarketplaceAppRole => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRole"')
return MarketplaceAppRole_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppDetail_possibleTypes: string[] = ['MarketplaceAppDetail']
export const isMarketplaceAppDetail = (obj?: { __typename?: any } | null): obj is MarketplaceAppDetail => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppDetail"')
return MarketplaceAppDetail_possibleTypes.includes(obj.__typename)
}
const PublicDomain_possibleTypes: string[] = ['PublicDomain']
export const isPublicDomain = (obj?: { __typename?: any } | null): obj is PublicDomain => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicDomain"')
@@ -9464,12 +9456,6 @@ export const enumFileFolder = {
Dpa: 'Dpa' as const
}
export const enumWorkspaceMigrationActionType = {
delete: 'delete' as const,
create: 'create' as const,
update: 'update' as const
}
export const enumAnalyticsType = {
PAGEVIEW: 'PAGEVIEW' as const,
TRACK: 'TRACK' as const
File diff suppressed because it is too large Load Diff
@@ -2657,7 +2657,6 @@ export type Mutation = {
rotateApplicationRegistrationClientSecret: RotateClientSecret;
runAgent: RunAgentResult;
runEvaluationInput: AgentTurn;
runWorkspaceMigration: Scalars['Boolean']['output'];
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
sendChatMessage: SendChatMessageResult;
sendEmail: SendEmailOutput;
@@ -3425,11 +3424,6 @@ export type MutationRunEvaluationInputArgs = {
};
export type MutationRunWorkspaceMigrationArgs = {
workspaceMigration: WorkspaceMigrationInput;
};
export type MutationSaveImapSmtpCaldavAccountArgs = {
connectionParameters: EmailAccountConnectionParameters;
handle: Scalars['String']['input'];
@@ -6401,22 +6395,6 @@ export type WorkspaceMigration = {
applicationUniversalIdentifier: Scalars['String']['output'];
};
export enum WorkspaceMigrationActionType {
create = 'create',
delete = 'delete',
update = 'update'
}
export type WorkspaceMigrationDeleteActionInput = {
metadataName: AllMetadataName;
type: WorkspaceMigrationActionType;
universalIdentifier: Scalars['String']['input'];
};
export type WorkspaceMigrationInput = {
actions: Array<WorkspaceMigrationDeleteActionInput>;
};
export type WorkspaceNameAndId = {
__typename?: 'WorkspaceNameAndId';
displayName?: Maybe<Scalars['String']['output']>;
@@ -111,7 +111,7 @@ export const SettingsApplicationDetails = () => {
const { upgrade, isUpgrading } = useUpgradeApplication();
const canInstallMarketplaceApps = useHasPermissionFlag(
PermissionFlagType.MARKETPLACE_APPS,
PermissionFlagType.APPLICATIONS,
);
const sourceType = application?.applicationRegistration?.sourceType;
@@ -55,7 +55,7 @@ export const SettingsAvailableApplicationDetails = () => {
const { upgrade, isUpgrading } = useUpgradeApplication();
const canInstallMarketplaceApps = useHasPermissionFlag(
PermissionFlagType.MARKETPLACE_APPS,
PermissionFlagType.APPLICATIONS,
);
const { data: applicationData } = useQuery(
@@ -4,7 +4,6 @@ import { ApplicationRegistrationModule } from 'src/engine/core-modules/applicati
import { ApplicationManifestModule } from 'src/engine/core-modules/application/application-manifest/application-manifest.module';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ApplicationDevelopmentResolver } from 'src/engine/core-modules/application/application-development/application-development.resolver';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
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 { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
@@ -21,7 +20,6 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
CacheLockModule,
FeatureFlagModule,
SdkClientModule,
TokenModule,
FileStorageModule,
PermissionsModule,
ThrottlerModule,
@@ -17,13 +17,11 @@ import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorato
import { ApplicationInput } from 'src/engine/core-modules/application/application-development/dtos/application.input';
import { CreateDevelopmentApplicationInput } from 'src/engine/core-modules/application/application-development/dtos/create-development-application.input';
import { DevelopmentApplicationDTO } from 'src/engine/core-modules/application/application-development/dtos/development-application.dto';
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/application-development/dtos/generate-application-token.input';
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/application-development/dtos/upload-application-file.input';
import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/application-development/dtos/workspace-migration.dto';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
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';
@@ -32,7 +30,6 @@ import {
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util';
@@ -42,8 +39,6 @@ import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/s
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -65,7 +60,6 @@ const APP_SYNC_LOCK_OPTIONS = { ttl: 60_000, ms: 500, maxRetries: 120 };
)
export class ApplicationDevelopmentResolver {
constructor(
private readonly applicationTokenService: ApplicationTokenService,
private readonly applicationService: ApplicationService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationRegistrationService: ApplicationRegistrationService,
@@ -114,23 +108,6 @@ export class ApplicationDevelopmentResolver {
};
}
@Mutation(() => ApplicationTokenPairDTO)
async generateApplicationToken(
@Args() { applicationId }: GenerateApplicationTokenInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@AuthUser({ allowUndefined: true }) user?: { id: string },
@AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string,
): Promise<ApplicationTokenPairDTO> {
await this.throttlePerApplication(applicationId, workspaceId);
return this.applicationTokenService.generateApplicationTokenPair({
workspaceId,
applicationId,
userId: user?.id,
userWorkspaceId,
});
}
@Mutation(() => WorkspaceMigrationDTO)
async syncApplication(
@Args() { manifest, dryRun }: ApplicationInput,
@@ -8,6 +8,7 @@ import { ApplicationRegistrationModule } from 'src/engine/core-modules/applicati
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';
import { MarketplaceModule } from 'src/engine/core-modules/application/application-marketplace/marketplace.module';
import { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
@@ -23,6 +24,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
ApplicationRegistrationModule,
ApplicationManifestModule,
ApplicationPackageModule,
MarketplaceModule,
CacheLockModule,
FeatureFlagModule,
LogicFunctionModule,
@@ -4,12 +4,17 @@ import {
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { Args, Query } from '@nestjs/graphql';
import { Args, Mutation, 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 { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { UninstallApplicationInput } from 'src/engine/core-modules/application/application-manifest/dtos/uninstall-application.input';
import { MarketplaceQueryService } from 'src/engine/core-modules/application/application-marketplace/marketplace-query.service';
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application/application-registration/application-registration-exception-filter';
import { ApplicationService } from 'src/engine/core-modules/application/application.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';
@@ -22,11 +27,20 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@UseFilters(ApplicationExceptionFilter, AuthGraphqlApiExceptionFilter)
@UseFilters(
ApplicationExceptionFilter,
ApplicationRegistrationExceptionFilter,
AuthGraphqlApiExceptionFilter,
)
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
@UseGuards(WorkspaceAuthGuard)
export class ApplicationInstallResolver {
constructor(private readonly applicationService: ApplicationService) {}
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationInstallService: ApplicationInstallService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly marketplaceQueryService: MarketplaceQueryService,
) {}
@Query(() => [ApplicationDTO])
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
@@ -53,4 +67,74 @@ export class ApplicationInstallResolver {
workspaceId,
});
}
@Mutation(() => Boolean, {
deprecationReason: 'Use installApplication instead',
})
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async installMarketplaceApp(
@Args('universalIdentifier') universalIdentifier: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
await this.installRegisteredApplication({
universalIdentifier,
version,
workspaceId: workspace.id,
});
return true;
}
@Mutation(() => ApplicationDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async installApplication(
@Args('universalIdentifier') universalIdentifier: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
await this.installRegisteredApplication({
universalIdentifier,
version,
workspaceId: workspace.id,
});
return this.applicationService.findOneApplicationOrThrow({
universalIdentifier,
workspaceId: workspace.id,
});
}
private async installRegisteredApplication(params: {
universalIdentifier: string;
version: string | undefined;
workspaceId: string;
}): Promise<void> {
const registration =
await this.marketplaceQueryService.findRegistrationByUniversalIdentifier(
params.universalIdentifier,
);
await this.applicationInstallService.installApplication({
appRegistrationId: registration.id,
version: params.version,
workspaceId: params.workspaceId,
});
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async uninstallApplication(
@Args() { universalIdentifier }: UninstallApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.uninstallApplication({
applicationUniversalIdentifier: universalIdentifier,
workspaceId,
});
return true;
}
}
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
import { ApplicationManifestResolver } from 'src/engine/core-modules/application/application-manifest/application-manifest.resolver';
import { ComputeApplicationManifestAllUniversalFlatEntityMapsService } from 'src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { ApplicationTranslationModule } from 'src/engine/core-modules/application/application-translation/application-translation.module';
@@ -12,8 +11,6 @@ import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-sto
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
@@ -27,14 +24,11 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
SecretEncryptionModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
],
providers: [
ApplicationManifestMigrationService,
ApplicationManifestResolver,
ApplicationSyncService,
ComputeApplicationManifestAllUniversalFlatEntityMapsService,
WorkspaceMigrationGraphqlApiExceptionInterceptor,
],
exports: [ApplicationManifestMigrationService, ApplicationSyncService],
})
@@ -1,82 +0,0 @@
import {
UseFilters,
UseGuards,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { RunWorkspaceMigrationInput } from 'src/engine/core-modules/application/application-manifest/dtos/run-workspace-migration.input';
import { UninstallApplicationInput } from 'src/engine/core-modules/application/application-manifest/dtos/uninstall-application.input';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { enrichCreateWorkspaceMigrationActionsWithIds } from 'src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util';
import { AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
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()
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
@UseFilters(ApplicationExceptionFilter, AuthGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ApplicationManifestResolver {
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
) {}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async runWorkspaceMigration(
@Args() { workspaceMigration: { actions } }: RunWorkspaceMigrationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const workspaceMigration = enrichCreateWorkspaceMigrationActionsWithIds({
workspaceMigration: {
actions: actions as AllUniversalWorkspaceMigrationAction[],
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
idByUniversalIdentifierByMetadataName: {},
});
await this.workspaceMigrationRunnerService.run({
workspaceMigration,
workspaceId,
});
return true;
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async uninstallApplication(
@Args() { universalIdentifier }: UninstallApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.uninstallApplication({
applicationUniversalIdentifier: universalIdentifier,
workspaceId,
});
return true;
}
}
@@ -1,54 +0,0 @@
import { ArgsType, Field, InputType, registerEnumType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
ArrayNotEmpty,
IsArray,
IsEnum,
IsUUID,
ValidateNested,
} from 'class-validator';
import { ALL_METADATA_NAME, AllMetadataName } from 'twenty-shared/metadata';
import { WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant';
registerEnumType(ALL_METADATA_NAME, {
name: 'AllMetadataName',
});
registerEnumType(WORKSPACE_MIGRATION_ACTION_TYPE, {
name: 'WorkspaceMigrationActionType',
});
@InputType()
export class WorkspaceMigrationDeleteActionInput {
@Field(() => WORKSPACE_MIGRATION_ACTION_TYPE)
@IsEnum(WORKSPACE_MIGRATION_ACTION_TYPE)
type: 'delete';
@Field(() => ALL_METADATA_NAME)
@IsEnum(ALL_METADATA_NAME)
metadataName: AllMetadataName;
@Field(() => String)
@IsUUID()
universalIdentifier: string;
}
@InputType()
export class WorkspaceMigrationInput {
@Field(() => [WorkspaceMigrationDeleteActionInput])
@IsArray()
@ArrayNotEmpty()
@ValidateNested({ each: true })
@Type(() => WorkspaceMigrationDeleteActionInput)
actions: WorkspaceMigrationDeleteActionInput[];
}
@ArgsType()
export class RunWorkspaceMigrationInput {
@Field(() => WorkspaceMigrationInput)
@ValidateNested()
@Type(() => WorkspaceMigrationInput)
workspaceMigration: WorkspaceMigrationInput;
}
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command';
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job';
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service';
@@ -18,7 +17,6 @@ import { MarketplaceCatalogSyncCommand } from 'src/engine/core-modules/applicati
imports: [
ApplicationModule,
ApplicationRegistrationModule,
ApplicationInstallModule,
FeatureFlagModule,
PermissionsModule,
TwentyConfigModule,
@@ -4,14 +4,9 @@ import { Args, Mutation, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application/application-registration/application-registration-exception-filter';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
import { MarketplaceAppDetailDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-detail.dto';
import { MarketplaceQueryService } from 'src/engine/core-modules/application/application-marketplace/marketplace-query.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -28,8 +23,6 @@ import { MessageQueueService } from 'src/engine/core-modules/message-queue/servi
export class MarketplaceResolver {
constructor(
private readonly marketplaceQueryService: MarketplaceQueryService,
private readonly applicationInstallService: ApplicationInstallService,
private readonly applicationService: ApplicationService,
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@@ -48,55 +41,6 @@ export class MarketplaceResolver {
);
}
@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(
universalIdentifier,
);
await this.applicationInstallService.installApplication({
appRegistrationId: registration.id,
version,
workspaceId: workspace.id,
});
return this.applicationService.findOneApplicationOrThrow({
universalIdentifier,
workspaceId: workspace.id,
});
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
async syncMarketplaceCatalog(): Promise<boolean> {
@@ -1,29 +1,63 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/application-development/dtos/generate-application-token.input';
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
const APP_TOKEN_RATE_LIMIT_MAX = 30;
const APP_TOKEN_RATE_LIMIT_WINDOW_MS = 30_000;
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@UseFilters(AuthGraphqlApiExceptionFilter)
@UseFilters(ApplicationExceptionFilter, AuthGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ApplicationOAuthResolver {
constructor(
private readonly applicationTokenService: ApplicationTokenService,
private readonly throttlerService: ThrottlerService,
) {}
@Mutation(() => ApplicationTokenPairDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async generateApplicationToken(
@Args() { applicationId }: GenerateApplicationTokenInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@AuthUser({ allowUndefined: true }) user?: { id: string },
@AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string,
): Promise<ApplicationTokenPairDTO> {
await this.throttlerService.tokenBucketThrottleOrThrow(
`app-dev:${workspaceId}:${applicationId}`,
1,
APP_TOKEN_RATE_LIMIT_MAX,
APP_TOKEN_RATE_LIMIT_WINDOW_MS,
);
return this.applicationTokenService.generateApplicationTokenPair({
workspaceId,
applicationId,
userId: user?.id,
userWorkspaceId,
});
}
@Mutation(() => ApplicationTokenPairDTO)
@UseGuards(NoPermissionGuard)
async renewApplicationToken(
@@ -21,7 +21,7 @@ export class ApplicationUpgradeResolver {
) {}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async upgradeApplication(
@Args('appRegistrationId') appRegistrationId: string,
@Args('targetVersion') targetVersion: string,
@@ -1,10 +1,14 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
ALL_METADATA_NAME,
type AllMetadataName,
} from 'twenty-shared/metadata';
registerEnumType(ALL_METADATA_NAME, {
name: 'AllMetadataName',
});
@ObjectType('CollectionHash')
export class CollectionHashDTO {
@Field(() => ALL_METADATA_NAME)