Refactor application module architecture for clarity and explicitness (#18432)

## Summary

- **Module reorganization**: Moved `ApplicationUpgradeService` and cron
jobs to `application-upgrade/`, `ApplicationSyncService` to
`application-manifest/`, and
`runWorkspaceMigration`/`uninstallApplication` mutations to the manifest
resolver — each module now has a single clear responsibility.
- **Explicit install flow**: Removed implicit `ApplicationEntity`
creation from `ApplicationSyncService`. The install service and dev
resolver now explicitly create the `ApplicationEntity` before syncing.
npm packages are resolved at registration time to extract manifest
metadata (universalIdentifier, name, description, etc.), eliminating the
`reconcileUniversalIdentifier` hack.
- **Better error handling**: Frontend hooks now surface actual server
error messages in snackbars instead of swallowing them. Replaced the
ugly `ConfirmationModal` for transfer ownership with a proper form
modal. Fixed `SettingsAdminTableCard` row height overflow and corrected
the `yarn-engine` asset path.

## Test plan
- [ ] Register an npm package — verify manifest metadata (name,
description, universalIdentifier) is extracted correctly
- [ ] Install a registered npm app on a workspace — verify
ApplicationEntity is created and sync succeeds
- [ ] Test `app:dev` CLI flow — verify local app registration and sync
work
- [ ] Upload a tarball — verify registration and install flow
- [ ] Transfer ownership — verify the new modal UX works
- [ ] Verify error messages appear correctly in snackbars when
operations fail


Made with [Cursor](https://cursor.com)
This commit is contained in:
Félix Malfait
2026-03-06 08:45:08 +01:00
committed by GitHub
parent 90cced0e74
commit 514d0017ea
184 changed files with 3179 additions and 1382 deletions

View File

@@ -316,12 +316,6 @@ export type ApiKeyToken = {
token: Scalars['String'];
};
export enum AppRegistrationSourceType {
LOCAL = 'LOCAL',
NPM = 'NPM',
TARBALL = 'TARBALL'
}
export type AppToken = {
__typename?: 'AppToken';
createdAt: Scalars['DateTime'];
@@ -362,20 +356,28 @@ export type ApplicationRegistration = {
description?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isFeatured: Scalars['Boolean'];
isListed: Scalars['Boolean'];
latestAvailableVersion?: Maybe<Scalars['String']>;
logoUrl?: Maybe<Scalars['String']>;
name: Scalars['String'];
oAuthClientId: Scalars['String'];
oAuthRedirectUris: Array<Scalars['String']>;
oAuthScopes: Array<Scalars['String']>;
ownerWorkspaceId?: Maybe<Scalars['UUID']>;
sourcePackage?: Maybe<Scalars['String']>;
sourceType: AppRegistrationSourceType;
sourceType: ApplicationRegistrationSourceType;
termsUrl?: Maybe<Scalars['String']>;
universalIdentifier: Scalars['String'];
updatedAt: Scalars['DateTime'];
websiteUrl?: Maybe<Scalars['String']>;
};
export enum ApplicationRegistrationSourceType {
LOCAL = 'LOCAL',
NPM = 'NPM',
TARBALL = 'TARBALL'
}
export type ApplicationRegistrationStats = {
__typename?: 'ApplicationRegistrationStats';
activeInstalls: Scalars['Int'];
@@ -387,7 +389,7 @@ export type ApplicationRegistrationSummary = {
__typename?: 'ApplicationRegistrationSummary';
id: Scalars['UUID'];
latestAvailableVersion?: Maybe<Scalars['String']>;
sourceType: AppRegistrationSourceType;
sourceType: ApplicationRegistrationSourceType;
};
export type ApplicationRegistrationVariable = {
@@ -1086,15 +1088,6 @@ export type CreateAppTokenInput = {
expiresAt: Scalars['DateTime'];
};
export type CreateApplicationInput = {
applicationRegistrationId?: InputMaybe<Scalars['String']>;
description?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
sourcePath: Scalars['String'];
universalIdentifier: Scalars['String'];
version: Scalars['String'];
};
export type CreateApplicationRegistration = {
__typename?: 'CreateApplicationRegistration';
applicationRegistration: ApplicationRegistration;
@@ -2235,6 +2228,7 @@ export type MarketplaceApp = {
frontComponents: Array<MarketplaceAppFrontComponent>;
icon: Scalars['String'];
id: Scalars['String'];
isFeatured: Scalars['Boolean'];
logicFunctions: Array<MarketplaceAppLogicFunction>;
logo?: Maybe<Scalars['String']>;
name: Scalars['String'];
@@ -2383,7 +2377,6 @@ export type Mutation = {
createObjectEvent: Analytics;
createOneAgent: Agent;
createOneAppToken: AppToken;
createOneApplication: Application;
createOneField: Field;
createOneLogicFunction: LogicFunction;
createOneObject: Object;
@@ -2456,7 +2449,7 @@ export type Mutation = {
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioning;
installApplication: Scalars['Boolean'];
installMarketplaceApp: Scalars['Boolean'];
installNpmApp: Scalars['Boolean'];
registerNpmPackage: ApplicationRegistration;
removeQueryFromEventStream: Scalars['Boolean'];
removeRoleFromAgent: Scalars['Boolean'];
renewApplicationToken: ApplicationTokenPair;
@@ -2467,6 +2460,7 @@ export type Mutation = {
revokeApiKey?: Maybe<ApiKey>;
rotateApplicationRegistrationClientSecret: RotateClientSecret;
runEvaluationInput: AgentTurn;
runWorkspaceMigration: Scalars['Boolean'];
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
sendInvitations: SendInvitations;
setAdminAiModelEnabled: Scalars['Boolean'];
@@ -2482,6 +2476,7 @@ export type Mutation = {
switchSubscriptionInterval: BillingUpdate;
syncApplication: WorkspaceMigration;
trackAnalytics: Analytics;
transferApplicationRegistrationOwnership: ApplicationRegistration;
uninstallApplication: Scalars['Boolean'];
updateApiKey?: Maybe<ApiKey>;
updateApplicationRegistration: ApplicationRegistration;
@@ -2707,11 +2702,6 @@ export type MutationCreateOneAppTokenArgs = {
};
export type MutationCreateOneApplicationArgs = {
input: CreateApplicationInput;
};
export type MutationCreateOneFieldArgs = {
input: CreateOneFieldMetadataInput;
};
@@ -3042,7 +3032,8 @@ export type MutationInitiateOtpProvisioningArgs = {
export type MutationInstallApplicationArgs = {
workspaceMigration: WorkspaceMigrationInput;
appRegistrationId: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
};
@@ -3052,9 +3043,8 @@ export type MutationInstallMarketplaceAppArgs = {
};
export type MutationInstallNpmAppArgs = {
export type MutationRegisterNpmPackageArgs = {
packageName: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
};
@@ -3111,6 +3101,11 @@ export type MutationRunEvaluationInputArgs = {
};
export type MutationRunWorkspaceMigrationArgs = {
workspaceMigration: WorkspaceMigrationInput;
};
export type MutationSaveImapSmtpCaldavAccountArgs = {
accountOwnerId: Scalars['UUID'];
connectionParameters: EmailAccountConnectionParameters;
@@ -3184,6 +3179,12 @@ export type MutationTrackAnalyticsArgs = {
};
export type MutationTransferApplicationRegistrationOwnershipArgs = {
applicationRegistrationId: Scalars['String'];
targetWorkspaceSubdomain: Scalars['String'];
};
export type MutationUninstallApplicationArgs = {
universalIdentifier: Scalars['String'];
};
@@ -3932,6 +3933,7 @@ export type Query = {
agentTurns: Array<AgentTurn>;
apiKey?: Maybe<ApiKey>;
apiKeys: Array<ApiKey>;
applicationRegistrationTarballUrl?: Maybe<Scalars['String']>;
barChartData: BarChartData;
billingPortalSession: BillingSession;
chatMessages: Array<AgentMessage>;
@@ -3946,6 +3948,7 @@ export type Query = {
eventLogs: EventLogQueryResult;
field: Field;
fields: FieldConnection;
findAllApplicationRegistrations: Array<ApplicationRegistration>;
findApplicationRegistrationByClientId?: Maybe<PublicApplicationRegistration>;
findApplicationRegistrationByUniversalIdentifier?: Maybe<ApplicationRegistration>;
findApplicationRegistrationStats: ApplicationRegistrationStats;
@@ -3960,6 +3963,7 @@ export type Query = {
findOneApplication: Application;
findOneApplicationRegistration: ApplicationRegistration;
findOneLogicFunction: LogicFunction;
findOneMarketplaceApp: MarketplaceApp;
findWorkspaceFromInviteHash: Workspace;
findWorkspaceInvitations: Array<WorkspaceInvitation>;
frontComponent?: Maybe<FrontComponent>;
@@ -4035,6 +4039,11 @@ export type QueryApiKeyArgs = {
};
export type QueryApplicationRegistrationTarballUrlArgs = {
id: Scalars['String'];
};
export type QueryBarChartDataArgs = {
input: BarChartDataInput;
};
@@ -4135,6 +4144,11 @@ export type QueryFindOneLogicFunctionArgs = {
};
export type QueryFindOneMarketplaceAppArgs = {
universalIdentifier: Scalars['String'];
};
export type QueryFindWorkspaceFromInviteHashArgs = {
inviteHash: Scalars['String'];
};
@@ -4827,6 +4841,7 @@ export type UpdateApplicationRegistrationInput = {
export type UpdateApplicationRegistrationPayload = {
author?: InputMaybe<Scalars['String']>;
description?: InputMaybe<Scalars['String']>;
isListed?: InputMaybe<Scalars['Boolean']>;
logoUrl?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
oAuthRedirectUris?: InputMaybe<Array<Scalars['String']>>;
@@ -5818,19 +5833,26 @@ export type UpdateOneApplicationVariableMutationVariables = Exact<{
export type UpdateOneApplicationVariableMutation = { __typename?: 'Mutation', updateOneApplicationVariable: boolean };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description?: string | null, version?: string | null, universalIdentifier: string, applicationRegistrationId?: string | null, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null, sourceType: AppRegistrationSourceType } | null, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description?: string | null, version?: string | null, universalIdentifier: string, applicationRegistrationId?: string | null, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null, sourceType: ApplicationRegistrationSourceType } | null, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type FindManyApplicationsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyApplicationsQuery = { __typename?: 'Query', findManyApplications: Array<{ __typename?: 'Application', id: string, name: string, description?: string | null, version?: string | null, applicationRegistrationId?: string | null, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null, sourceType: AppRegistrationSourceType } | null }> };
export type FindManyApplicationsQuery = { __typename?: 'Query', findManyApplications: Array<{ __typename?: 'Application', id: string, name: string, description?: string | null, version?: string | null, universalIdentifier: string, applicationRegistrationId?: string | null, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null, sourceType: ApplicationRegistrationSourceType } | null }> };
export type FindOneApplicationQueryVariables = Exact<{
id: Scalars['UUID'];
}>;
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description?: string | null, version?: string | null, universalIdentifier: string, applicationRegistrationId?: string | null, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null, sourceType: AppRegistrationSourceType } | null, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, applicationId?: string | null, createdAt: string, updatedAt: string }> } };
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description?: string | null, version?: string | null, universalIdentifier: string, applicationRegistrationId?: string | null, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null, sourceType: ApplicationRegistrationSourceType } | null, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, applicationId?: string | null, createdAt: string, updatedAt: string }> } };
export type FindOneApplicationByUniversalIdentifierQueryVariables = Exact<{
universalIdentifier: Scalars['UUID'];
}>;
export type FindOneApplicationByUniversalIdentifierQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string } };
export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string };
@@ -6225,7 +6247,15 @@ export type GetLogicFunctionSourceCodeQueryVariables = Exact<{
export type GetLogicFunctionSourceCodeQuery = { __typename?: 'Query', getLogicFunctionSourceCode?: string | null };
export type MarketplaceAppFieldsFragment = { __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, sourcePackage?: string | null, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null };
export type MarketplaceAppFieldsFragment = { __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, sourcePackage?: string | null, isFeatured: boolean, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null };
export type InstallApplicationMutationVariables = Exact<{
appRegistrationId: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
}>;
export type InstallApplicationMutation = { __typename?: 'Mutation', installApplication: boolean };
export type InstallMarketplaceAppMutationVariables = Exact<{
universalIdentifier: Scalars['String'];
@@ -6235,13 +6265,12 @@ export type InstallMarketplaceAppMutationVariables = Exact<{
export type InstallMarketplaceAppMutation = { __typename?: 'Mutation', installMarketplaceApp: boolean };
export type InstallNpmAppMutationVariables = Exact<{
export type RegisterNpmPackageMutationVariables = Exact<{
packageName: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
}>;
export type InstallNpmAppMutation = { __typename?: 'Mutation', installNpmApp: boolean };
export type RegisterNpmPackageMutation = { __typename?: 'Mutation', registerNpmPackage: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string } };
export type UpgradeApplicationMutationVariables = Exact<{
appRegistrationId: Scalars['String'];
@@ -6262,7 +6291,14 @@ export type UploadAppTarballMutation = { __typename?: 'Mutation', uploadAppTarba
export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, sourcePackage?: string | null, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null }> };
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, sourcePackage?: string | null, isFeatured: boolean, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null }> };
export type FindOneMarketplaceAppQueryVariables = Exact<{
universalIdentifier: Scalars['String'];
}>;
export type FindOneMarketplaceAppQuery = { __typename?: 'Query', findOneMarketplaceApp: { __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, sourcePackage?: string | null, isFeatured: boolean, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null } };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
@@ -6452,6 +6488,11 @@ export type GetAdminAiModelsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetAdminAiModelsQuery = { __typename?: 'Query', getAdminAiModels: { __typename?: 'AdminAIModels', autoEnableNewModels: boolean, models: Array<{ __typename?: 'AdminAIModelConfig', modelId: string, label: string, modelFamily?: ModelFamily | null, inferenceProvider: InferenceProvider, isAvailable: boolean, isAdminEnabled: boolean, deprecated?: boolean | null, isRecommended?: boolean | null }> } };
export type FindAllApplicationRegistrationsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindAllApplicationRegistrationsQuery = { __typename?: 'Query', findAllApplicationRegistrations: Array<{ __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, isListed: boolean, isFeatured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string }> };
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
key: Scalars['String'];
value: Scalars['JSON'];
@@ -6554,7 +6595,7 @@ export type GetSystemHealthStatusQueryVariables = Exact<{ [key: string]: never;
export type GetSystemHealthStatusQuery = { __typename?: 'Query', getSystemHealthStatus: { __typename?: 'SystemHealth', services: Array<{ __typename?: 'SystemHealthService', id: HealthIndicatorId, label: string, status: AdminPanelHealthServiceStatus }> } };
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string };
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, isListed: boolean, isFeatured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string };
export type DeleteApplicationRegistrationMutationVariables = Exact<{
id: Scalars['String'];
@@ -6570,12 +6611,20 @@ export type RotateApplicationRegistrationClientSecretMutationVariables = Exact<{
export type RotateApplicationRegistrationClientSecretMutation = { __typename?: 'Mutation', rotateApplicationRegistrationClientSecret: { __typename?: 'RotateClientSecret', clientSecret: string } };
export type TransferApplicationRegistrationOwnershipMutationVariables = Exact<{
applicationRegistrationId: Scalars['String'];
targetWorkspaceSubdomain: Scalars['String'];
}>;
export type TransferApplicationRegistrationOwnershipMutation = { __typename?: 'Mutation', transferApplicationRegistrationOwnership: { __typename?: 'ApplicationRegistration', id: string, name: string } };
export type UpdateApplicationRegistrationMutationVariables = Exact<{
input: UpdateApplicationRegistrationInput;
}>;
export type UpdateApplicationRegistrationMutation = { __typename?: 'Mutation', updateApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string } };
export type UpdateApplicationRegistrationMutation = { __typename?: 'Mutation', updateApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, isListed: boolean, isFeatured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string } };
export type UpdateApplicationRegistrationVariableMutationVariables = Exact<{
input: UpdateApplicationRegistrationVariableInput;
@@ -6584,6 +6633,13 @@ export type UpdateApplicationRegistrationVariableMutationVariables = Exact<{
export type UpdateApplicationRegistrationVariableMutation = { __typename?: 'Mutation', updateApplicationRegistrationVariable: { __typename?: 'ApplicationRegistrationVariable', id: string, key: string, description: string, isSecret: boolean, isRequired: boolean, isFilled: boolean, createdAt: string, updatedAt: string } };
export type ApplicationRegistrationTarballUrlQueryVariables = Exact<{
id: Scalars['String'];
}>;
export type ApplicationRegistrationTarballUrlQuery = { __typename?: 'Query', applicationRegistrationTarballUrl?: string | null };
export type FindApplicationRegistrationByClientIdQueryVariables = Exact<{
clientId: Scalars['String'];
}>;
@@ -6608,14 +6664,14 @@ export type FindApplicationRegistrationVariablesQuery = { __typename?: 'Query',
export type FindManyApplicationRegistrationsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyApplicationRegistrationsQuery = { __typename?: 'Query', findManyApplicationRegistrations: Array<{ __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string }> };
export type FindManyApplicationRegistrationsQuery = { __typename?: 'Query', findManyApplicationRegistrations: Array<{ __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, isListed: boolean, isFeatured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string }> };
export type FindOneApplicationRegistrationQueryVariables = Exact<{
id: Scalars['String'];
}>;
export type FindOneApplicationRegistrationQuery = { __typename?: 'Query', findOneApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string } };
export type FindOneApplicationRegistrationQuery = { __typename?: 'Query', findOneApplicationRegistration: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, description?: string | null, logoUrl?: string | null, author?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, isListed: boolean, isFeatured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string } };
export type UninstallApplicationMutationVariables = Exact<{
universalIdentifier: Scalars['String'];
@@ -7889,6 +7945,7 @@ export const MarketplaceAppFieldsFragmentDoc = gql`
description
}
sourcePackage
isFeatured
defaultRole {
id
label
@@ -7960,6 +8017,9 @@ export const ApplicationRegistrationFragmentFragmentDoc = gql`
latestAvailableVersion
websiteUrl
termsUrl
isListed
isFeatured
ownerWorkspaceId
createdAt
updatedAt
}
@@ -9385,6 +9445,7 @@ export const FindManyApplicationsDocument = gql`
name
description
version
universalIdentifier
applicationRegistrationId
applicationRegistration {
id
@@ -9456,6 +9517,41 @@ export function useFindOneApplicationLazyQuery(baseOptions?: Apollo.LazyQueryHoo
export type FindOneApplicationQueryHookResult = ReturnType<typeof useFindOneApplicationQuery>;
export type FindOneApplicationLazyQueryHookResult = ReturnType<typeof useFindOneApplicationLazyQuery>;
export type FindOneApplicationQueryResult = Apollo.QueryResult<FindOneApplicationQuery, FindOneApplicationQueryVariables>;
export const FindOneApplicationByUniversalIdentifierDocument = gql`
query FindOneApplicationByUniversalIdentifier($universalIdentifier: UUID!) {
findOneApplication(universalIdentifier: $universalIdentifier) {
id
}
}
`;
/**
* __useFindOneApplicationByUniversalIdentifierQuery__
*
* To run a query within a React component, call `useFindOneApplicationByUniversalIdentifierQuery` and pass it any options that fit your needs.
* When your component renders, `useFindOneApplicationByUniversalIdentifierQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindOneApplicationByUniversalIdentifierQuery({
* variables: {
* universalIdentifier: // value for 'universalIdentifier'
* },
* });
*/
export function useFindOneApplicationByUniversalIdentifierQuery(baseOptions: Apollo.QueryHookOptions<FindOneApplicationByUniversalIdentifierQuery, FindOneApplicationByUniversalIdentifierQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindOneApplicationByUniversalIdentifierQuery, FindOneApplicationByUniversalIdentifierQueryVariables>(FindOneApplicationByUniversalIdentifierDocument, options);
}
export function useFindOneApplicationByUniversalIdentifierLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindOneApplicationByUniversalIdentifierQuery, FindOneApplicationByUniversalIdentifierQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindOneApplicationByUniversalIdentifierQuery, FindOneApplicationByUniversalIdentifierQueryVariables>(FindOneApplicationByUniversalIdentifierDocument, options);
}
export type FindOneApplicationByUniversalIdentifierQueryHookResult = ReturnType<typeof useFindOneApplicationByUniversalIdentifierQuery>;
export type FindOneApplicationByUniversalIdentifierLazyQueryHookResult = ReturnType<typeof useFindOneApplicationByUniversalIdentifierLazyQuery>;
export type FindOneApplicationByUniversalIdentifierQueryResult = Apollo.QueryResult<FindOneApplicationByUniversalIdentifierQuery, FindOneApplicationByUniversalIdentifierQueryVariables>;
export const AuthorizeAppDocument = gql`
mutation authorizeApp($clientId: String!, $codeChallenge: String, $redirectUrl: String!) {
authorizeApp(
@@ -11437,6 +11533,38 @@ export function useGetLogicFunctionSourceCodeLazyQuery(baseOptions?: Apollo.Lazy
export type GetLogicFunctionSourceCodeQueryHookResult = ReturnType<typeof useGetLogicFunctionSourceCodeQuery>;
export type GetLogicFunctionSourceCodeLazyQueryHookResult = ReturnType<typeof useGetLogicFunctionSourceCodeLazyQuery>;
export type GetLogicFunctionSourceCodeQueryResult = Apollo.QueryResult<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
export const InstallApplicationDocument = gql`
mutation InstallApplication($appRegistrationId: String!, $version: String) {
installApplication(appRegistrationId: $appRegistrationId, version: $version)
}
`;
export type InstallApplicationMutationFn = Apollo.MutationFunction<InstallApplicationMutation, InstallApplicationMutationVariables>;
/**
* __useInstallApplicationMutation__
*
* To run a mutation, you first call `useInstallApplicationMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInstallApplicationMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [installApplicationMutation, { data, loading, error }] = useInstallApplicationMutation({
* variables: {
* appRegistrationId: // value for 'appRegistrationId'
* version: // value for 'version'
* },
* });
*/
export function useInstallApplicationMutation(baseOptions?: Apollo.MutationHookOptions<InstallApplicationMutation, InstallApplicationMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InstallApplicationMutation, InstallApplicationMutationVariables>(InstallApplicationDocument, options);
}
export type InstallApplicationMutationHookResult = ReturnType<typeof useInstallApplicationMutation>;
export type InstallApplicationMutationResult = Apollo.MutationResult<InstallApplicationMutation>;
export type InstallApplicationMutationOptions = Apollo.BaseMutationOptions<InstallApplicationMutation, InstallApplicationMutationVariables>;
export const InstallMarketplaceAppDocument = gql`
mutation InstallMarketplaceApp($universalIdentifier: String!, $version: String) {
installMarketplaceApp(
@@ -11472,38 +11600,41 @@ export function useInstallMarketplaceAppMutation(baseOptions?: Apollo.MutationHo
export type InstallMarketplaceAppMutationHookResult = ReturnType<typeof useInstallMarketplaceAppMutation>;
export type InstallMarketplaceAppMutationResult = Apollo.MutationResult<InstallMarketplaceAppMutation>;
export type InstallMarketplaceAppMutationOptions = Apollo.BaseMutationOptions<InstallMarketplaceAppMutation, InstallMarketplaceAppMutationVariables>;
export const InstallNpmAppDocument = gql`
mutation InstallNpmApp($packageName: String!, $version: String) {
installNpmApp(packageName: $packageName, version: $version)
export const RegisterNpmPackageDocument = gql`
mutation RegisterNpmPackage($packageName: String!) {
registerNpmPackage(packageName: $packageName) {
id
universalIdentifier
name
}
}
`;
export type InstallNpmAppMutationFn = Apollo.MutationFunction<InstallNpmAppMutation, InstallNpmAppMutationVariables>;
export type RegisterNpmPackageMutationFn = Apollo.MutationFunction<RegisterNpmPackageMutation, RegisterNpmPackageMutationVariables>;
/**
* __useInstallNpmAppMutation__
* __useRegisterNpmPackageMutation__
*
* To run a mutation, you first call `useInstallNpmAppMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInstallNpmAppMutation` returns a tuple that includes:
* To run a mutation, you first call `useRegisterNpmPackageMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRegisterNpmPackageMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [installNpmAppMutation, { data, loading, error }] = useInstallNpmAppMutation({
* const [registerNpmPackageMutation, { data, loading, error }] = useRegisterNpmPackageMutation({
* variables: {
* packageName: // value for 'packageName'
* version: // value for 'version'
* },
* });
*/
export function useInstallNpmAppMutation(baseOptions?: Apollo.MutationHookOptions<InstallNpmAppMutation, InstallNpmAppMutationVariables>) {
export function useRegisterNpmPackageMutation(baseOptions?: Apollo.MutationHookOptions<RegisterNpmPackageMutation, RegisterNpmPackageMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InstallNpmAppMutation, InstallNpmAppMutationVariables>(InstallNpmAppDocument, options);
return Apollo.useMutation<RegisterNpmPackageMutation, RegisterNpmPackageMutationVariables>(RegisterNpmPackageDocument, options);
}
export type InstallNpmAppMutationHookResult = ReturnType<typeof useInstallNpmAppMutation>;
export type InstallNpmAppMutationResult = Apollo.MutationResult<InstallNpmAppMutation>;
export type InstallNpmAppMutationOptions = Apollo.BaseMutationOptions<InstallNpmAppMutation, InstallNpmAppMutationVariables>;
export type RegisterNpmPackageMutationHookResult = ReturnType<typeof useRegisterNpmPackageMutation>;
export type RegisterNpmPackageMutationResult = Apollo.MutationResult<RegisterNpmPackageMutation>;
export type RegisterNpmPackageMutationOptions = Apollo.BaseMutationOptions<RegisterNpmPackageMutation, RegisterNpmPackageMutationVariables>;
export const UpgradeApplicationDocument = gql`
mutation UpgradeApplication($appRegistrationId: String!, $targetVersion: String!) {
upgradeApplication(
@@ -11609,6 +11740,41 @@ export function useFindManyMarketplaceAppsLazyQuery(baseOptions?: Apollo.LazyQue
export type FindManyMarketplaceAppsQueryHookResult = ReturnType<typeof useFindManyMarketplaceAppsQuery>;
export type FindManyMarketplaceAppsLazyQueryHookResult = ReturnType<typeof useFindManyMarketplaceAppsLazyQuery>;
export type FindManyMarketplaceAppsQueryResult = Apollo.QueryResult<FindManyMarketplaceAppsQuery, FindManyMarketplaceAppsQueryVariables>;
export const FindOneMarketplaceAppDocument = gql`
query FindOneMarketplaceApp($universalIdentifier: String!) {
findOneMarketplaceApp(universalIdentifier: $universalIdentifier) {
...MarketplaceAppFields
}
}
${MarketplaceAppFieldsFragmentDoc}`;
/**
* __useFindOneMarketplaceAppQuery__
*
* To run a query within a React component, call `useFindOneMarketplaceAppQuery` and pass it any options that fit your needs.
* When your component renders, `useFindOneMarketplaceAppQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindOneMarketplaceAppQuery({
* variables: {
* universalIdentifier: // value for 'universalIdentifier'
* },
* });
*/
export function useFindOneMarketplaceAppQuery(baseOptions: Apollo.QueryHookOptions<FindOneMarketplaceAppQuery, FindOneMarketplaceAppQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindOneMarketplaceAppQuery, FindOneMarketplaceAppQueryVariables>(FindOneMarketplaceAppDocument, options);
}
export function useFindOneMarketplaceAppLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindOneMarketplaceAppQuery, FindOneMarketplaceAppQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindOneMarketplaceAppQuery, FindOneMarketplaceAppQueryVariables>(FindOneMarketplaceAppDocument, options);
}
export type FindOneMarketplaceAppQueryHookResult = ReturnType<typeof useFindOneMarketplaceAppQuery>;
export type FindOneMarketplaceAppLazyQueryHookResult = ReturnType<typeof useFindOneMarketplaceAppLazyQuery>;
export type FindOneMarketplaceAppQueryResult = Apollo.QueryResult<FindOneMarketplaceAppQuery, FindOneMarketplaceAppQueryVariables>;
export const CreateNavigationMenuItemDocument = gql`
mutation CreateNavigationMenuItem($input: CreateNavigationMenuItemInput!) {
createNavigationMenuItem(input: $input) {
@@ -12709,6 +12875,40 @@ export function useGetAdminAiModelsLazyQuery(baseOptions?: Apollo.LazyQueryHookO
export type GetAdminAiModelsQueryHookResult = ReturnType<typeof useGetAdminAiModelsQuery>;
export type GetAdminAiModelsLazyQueryHookResult = ReturnType<typeof useGetAdminAiModelsLazyQuery>;
export type GetAdminAiModelsQueryResult = Apollo.QueryResult<GetAdminAiModelsQuery, GetAdminAiModelsQueryVariables>;
export const FindAllApplicationRegistrationsDocument = gql`
query FindAllApplicationRegistrations {
findAllApplicationRegistrations {
...ApplicationRegistrationFragment
}
}
${ApplicationRegistrationFragmentFragmentDoc}`;
/**
* __useFindAllApplicationRegistrationsQuery__
*
* To run a query within a React component, call `useFindAllApplicationRegistrationsQuery` and pass it any options that fit your needs.
* When your component renders, `useFindAllApplicationRegistrationsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindAllApplicationRegistrationsQuery({
* variables: {
* },
* });
*/
export function useFindAllApplicationRegistrationsQuery(baseOptions?: Apollo.QueryHookOptions<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>(FindAllApplicationRegistrationsDocument, options);
}
export function useFindAllApplicationRegistrationsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>(FindAllApplicationRegistrationsDocument, options);
}
export type FindAllApplicationRegistrationsQueryHookResult = ReturnType<typeof useFindAllApplicationRegistrationsQuery>;
export type FindAllApplicationRegistrationsLazyQueryHookResult = ReturnType<typeof useFindAllApplicationRegistrationsLazyQuery>;
export type FindAllApplicationRegistrationsQueryResult = Apollo.QueryResult<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>;
export const CreateDatabaseConfigVariableDocument = gql`
mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) {
createDatabaseConfigVariable(key: $key, value: $value)
@@ -13368,6 +13568,44 @@ export function useRotateApplicationRegistrationClientSecretMutation(baseOptions
export type RotateApplicationRegistrationClientSecretMutationHookResult = ReturnType<typeof useRotateApplicationRegistrationClientSecretMutation>;
export type RotateApplicationRegistrationClientSecretMutationResult = Apollo.MutationResult<RotateApplicationRegistrationClientSecretMutation>;
export type RotateApplicationRegistrationClientSecretMutationOptions = Apollo.BaseMutationOptions<RotateApplicationRegistrationClientSecretMutation, RotateApplicationRegistrationClientSecretMutationVariables>;
export const TransferApplicationRegistrationOwnershipDocument = gql`
mutation TransferApplicationRegistrationOwnership($applicationRegistrationId: String!, $targetWorkspaceSubdomain: String!) {
transferApplicationRegistrationOwnership(
applicationRegistrationId: $applicationRegistrationId
targetWorkspaceSubdomain: $targetWorkspaceSubdomain
) {
id
name
}
}
`;
export type TransferApplicationRegistrationOwnershipMutationFn = Apollo.MutationFunction<TransferApplicationRegistrationOwnershipMutation, TransferApplicationRegistrationOwnershipMutationVariables>;
/**
* __useTransferApplicationRegistrationOwnershipMutation__
*
* To run a mutation, you first call `useTransferApplicationRegistrationOwnershipMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useTransferApplicationRegistrationOwnershipMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [transferApplicationRegistrationOwnershipMutation, { data, loading, error }] = useTransferApplicationRegistrationOwnershipMutation({
* variables: {
* applicationRegistrationId: // value for 'applicationRegistrationId'
* targetWorkspaceSubdomain: // value for 'targetWorkspaceSubdomain'
* },
* });
*/
export function useTransferApplicationRegistrationOwnershipMutation(baseOptions?: Apollo.MutationHookOptions<TransferApplicationRegistrationOwnershipMutation, TransferApplicationRegistrationOwnershipMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<TransferApplicationRegistrationOwnershipMutation, TransferApplicationRegistrationOwnershipMutationVariables>(TransferApplicationRegistrationOwnershipDocument, options);
}
export type TransferApplicationRegistrationOwnershipMutationHookResult = ReturnType<typeof useTransferApplicationRegistrationOwnershipMutation>;
export type TransferApplicationRegistrationOwnershipMutationResult = Apollo.MutationResult<TransferApplicationRegistrationOwnershipMutation>;
export type TransferApplicationRegistrationOwnershipMutationOptions = Apollo.BaseMutationOptions<TransferApplicationRegistrationOwnershipMutation, TransferApplicationRegistrationOwnershipMutationVariables>;
export const UpdateApplicationRegistrationDocument = gql`
mutation UpdateApplicationRegistration($input: UpdateApplicationRegistrationInput!) {
updateApplicationRegistration(input: $input) {
@@ -13441,6 +13679,39 @@ export function useUpdateApplicationRegistrationVariableMutation(baseOptions?: A
export type UpdateApplicationRegistrationVariableMutationHookResult = ReturnType<typeof useUpdateApplicationRegistrationVariableMutation>;
export type UpdateApplicationRegistrationVariableMutationResult = Apollo.MutationResult<UpdateApplicationRegistrationVariableMutation>;
export type UpdateApplicationRegistrationVariableMutationOptions = Apollo.BaseMutationOptions<UpdateApplicationRegistrationVariableMutation, UpdateApplicationRegistrationVariableMutationVariables>;
export const ApplicationRegistrationTarballUrlDocument = gql`
query ApplicationRegistrationTarballUrl($id: String!) {
applicationRegistrationTarballUrl(id: $id)
}
`;
/**
* __useApplicationRegistrationTarballUrlQuery__
*
* To run a query within a React component, call `useApplicationRegistrationTarballUrlQuery` and pass it any options that fit your needs.
* When your component renders, `useApplicationRegistrationTarballUrlQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useApplicationRegistrationTarballUrlQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useApplicationRegistrationTarballUrlQuery(baseOptions: Apollo.QueryHookOptions<ApplicationRegistrationTarballUrlQuery, ApplicationRegistrationTarballUrlQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ApplicationRegistrationTarballUrlQuery, ApplicationRegistrationTarballUrlQueryVariables>(ApplicationRegistrationTarballUrlDocument, options);
}
export function useApplicationRegistrationTarballUrlLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ApplicationRegistrationTarballUrlQuery, ApplicationRegistrationTarballUrlQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ApplicationRegistrationTarballUrlQuery, ApplicationRegistrationTarballUrlQueryVariables>(ApplicationRegistrationTarballUrlDocument, options);
}
export type ApplicationRegistrationTarballUrlQueryHookResult = ReturnType<typeof useApplicationRegistrationTarballUrlQuery>;
export type ApplicationRegistrationTarballUrlLazyQueryHookResult = ReturnType<typeof useApplicationRegistrationTarballUrlLazyQuery>;
export type ApplicationRegistrationTarballUrlQueryResult = Apollo.QueryResult<ApplicationRegistrationTarballUrlQuery, ApplicationRegistrationTarballUrlQueryVariables>;
export const FindApplicationRegistrationByClientIdDocument = gql`
query FindApplicationRegistrationByClientId($clientId: String!) {
findApplicationRegistrationByClientId(clientId: $clientId) {

View File

@@ -7,6 +7,7 @@ export const FIND_MANY_APPLICATIONS = gql`
name
description
version
universalIdentifier
applicationRegistrationId
applicationRegistration {
id

View File

@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const FIND_ONE_APPLICATION_BY_UNIVERSAL_IDENTIFIER = gql`
query FindOneApplicationByUniversalIdentifier($universalIdentifier: UUID!) {
findOneApplication(universalIdentifier: $universalIdentifier) {
id
}
}
`;

View File

@@ -50,6 +50,7 @@ export const MARKETPLACE_APP_FRAGMENT = gql`
description
}
sourcePackage
isFeatured
defaultRole {
id
label

View File

@@ -0,0 +1,7 @@
import gql from 'graphql-tag';
export const INSTALL_APPLICATION = gql`
mutation InstallApplication($appRegistrationId: String!, $version: String) {
installApplication(appRegistrationId: $appRegistrationId, version: $version)
}
`;

View File

@@ -1,7 +0,0 @@
import gql from 'graphql-tag';
export const INSTALL_NPM_APP = gql`
mutation InstallNpmApp($packageName: String!, $version: String) {
installNpmApp(packageName: $packageName, version: $version)
}
`;

View File

@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const REGISTER_NPM_PACKAGE = gql`
mutation RegisterNpmPackage($packageName: String!) {
registerNpmPackage(packageName: $packageName) {
id
universalIdentifier
name
}
}
`;

View File

@@ -0,0 +1,12 @@
import gql from 'graphql-tag';
import { MARKETPLACE_APP_FRAGMENT } from '@/marketplace/graphql/fragments/marketplaceAppFragment';
export const FIND_ONE_MARKETPLACE_APP = gql`
${MARKETPLACE_APP_FRAGMENT}
query FindOneMarketplaceApp($universalIdentifier: String!) {
findOneMarketplaceApp(universalIdentifier: $universalIdentifier) {
...MarketplaceAppFields
}
}
`;

View File

@@ -26,9 +26,11 @@ export const useInstallApp = <TVariables extends Record<string, unknown>>(
}
return false;
} catch {
} catch (error) {
const graphqlMessage = error instanceof Error ? error.message : undefined;
enqueueErrorSnackBar({
message: t`Failed to install the application.`,
message: graphqlMessage ?? t`Failed to install the application.`,
});
return false;

View File

@@ -1,13 +0,0 @@
import { useMutation } from '@apollo/client';
import { useInstallApp } from '~/modules/marketplace/hooks/useInstallApp';
import { INSTALL_NPM_APP } from '~/modules/marketplace/graphql/mutations/installNpmApp';
export const useInstallNpmApp = () => {
const [installNpmAppMutation] = useMutation(INSTALL_NPM_APP);
return useInstallApp<{
packageName: string;
version?: string;
}>(installNpmAppMutation);
};

View File

@@ -0,0 +1,50 @@
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useMutation } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { REGISTER_NPM_PACKAGE } from '~/modules/marketplace/graphql/mutations/registerNpmPackage';
export const useRegisterNpmPackage = () => {
const [registerNpmPackageMutation] = useMutation(REGISTER_NPM_PACKAGE);
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [isRegistering, setIsRegistering] = useState(false);
const register = async (params: {
packageName: string;
}): Promise<boolean> => {
setIsRegistering(true);
try {
const result = await registerNpmPackageMutation({
variables: { packageName: params.packageName },
});
const registration = result.data?.registerNpmPackage;
if (!isDefined(registration)) {
enqueueErrorSnackBar({ message: t`Registration failed.` });
return false;
}
enqueueSuccessSnackBar({
message: t`Package registered successfully.`,
});
return true;
} catch (error) {
const graphqlMessage = error instanceof Error ? error.message : undefined;
enqueueErrorSnackBar({
message: graphqlMessage ?? t`Failed to register npm package.`,
});
return false;
} finally {
setIsRegistering(false);
}
};
return { register, isRegistering };
};

View File

@@ -29,9 +29,11 @@ export const useUpgradeApplication = () => {
}
return false;
} catch {
} catch (error) {
const graphqlMessage = error instanceof Error ? error.message : undefined;
enqueueErrorSnackBar({
message: t`Failed to upgrade the application.`,
message: graphqlMessage ?? t`Failed to upgrade the application.`,
});
return false;

View File

@@ -8,6 +8,7 @@ import { UPLOAD_APP_TARBALL } from '~/modules/marketplace/graphql/mutations/uplo
type UploadResult =
| {
success: true;
registrationId: string;
universalIdentifier: string;
}
| {
@@ -29,7 +30,10 @@ export const useUploadAppTarball = () => {
const registration = result.data?.uploadAppTarball;
if (!isDefined(registration?.universalIdentifier)) {
if (
!isDefined(registration?.id) ||
!isDefined(registration?.universalIdentifier)
) {
enqueueErrorSnackBar({ message: t`Upload failed.` });
return { success: false };
@@ -37,11 +41,14 @@ export const useUploadAppTarball = () => {
return {
success: true,
registrationId: registration.id,
universalIdentifier: registration.universalIdentifier,
};
} catch {
} catch (error) {
const graphqlMessage = error instanceof Error ? error.message : undefined;
enqueueErrorSnackBar({
message: t`Failed to upload tarball.`,
message: graphqlMessage ?? t`Failed to upload tarball.`,
});
return { success: false };

View File

@@ -1,27 +1,17 @@
import { useState } from 'react';
import { styled } from '@linaria/react';
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { t } from '@lingui/core/macro';
import {
H2Title,
IconArchive,
IconFilter,
IconPlug,
IconRobot,
IconSearch,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { H2Title, IconArchive, IconPlug, IconRobot } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Card, Section } from 'twenty-ui/layout';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
useCreateDatabaseConfigVariableMutation,
useGetAdminAiModelsQuery,
@@ -30,17 +20,6 @@ import {
import { getModelIcon } from '~/pages/settings/ai/utils/getModelIcon';
import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel';
const StyledSearchAndFilterContainer = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[2]};
width: 100%;
`;
const StyledSearchInputContainer = styled.div`
flex: 1;
`;
export const SettingsAdminAI = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const [searchQuery, setSearchQuery] = useState('');
@@ -162,53 +141,41 @@ export const SettingsAdminAI = () => {
description={t`Toggle model availability across all workspaces`}
/>
<StyledSearchAndFilterContainer>
<StyledSearchInputContainer>
<SettingsTextInput
instanceId="admin-model-search"
LeftIcon={IconSearch}
placeholder={t`Search a model...`}
value={searchQuery}
onChange={setSearchQuery}
<SearchInput
placeholder={t`Search a model...`}
value={searchQuery}
onChange={setSearchQuery}
filterDropdown={(filterButton) => (
<Dropdown
dropdownId="admin-ai-models-filter-dropdown"
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 8 }}
clickableComponent={filterButton}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconPlug}
onToggleChange={() =>
setShowUnconfigured(!showUnconfigured)
}
toggled={showUnconfigured}
text={t`Unconfigured models`}
toggleSize="small"
/>
<MenuItemToggle
LeftIcon={IconArchive}
onToggleChange={() => setShowDeprecated(!showDeprecated)}
toggled={showDeprecated}
text={t`Deprecated models`}
toggleSize="small"
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</StyledSearchInputContainer>
<Dropdown
dropdownId="admin-ai-models-filter-dropdown"
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 8 }}
clickableComponent={
<Button
Icon={IconFilter}
size="medium"
variant="secondary"
accent="default"
ariaLabel={t`Filter`}
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconPlug}
onToggleChange={() =>
setShowUnconfigured(!showUnconfigured)
}
toggled={showUnconfigured}
text={t`Unconfigured models`}
toggleSize="small"
/>
<MenuItemToggle
LeftIcon={IconArchive}
onToggleChange={() => setShowDeprecated(!showDeprecated)}
toggled={showDeprecated}
text={t`Deprecated models`}
toggleSize="small"
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</StyledSearchAndFilterContainer>
)}
/>
<Card rounded>
{filteredModels.map((model, index) => (

View File

@@ -0,0 +1,107 @@
import { FIND_ALL_APPLICATION_REGISTRATIONS } from '@/settings/admin-panel/apps/graphql/queries/findAllApplicationRegistrations';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useQuery } from '@apollo/client';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { H2Title, Status } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type ApplicationRegistrationFragmentFragment } from '~/generated-metadata/graphql';
const StyledTableContainer = styled.div`
margin-top: ${themeCssVariables.spacing[3]};
`;
const StyledTableHeaderRowContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[2]};
`;
const TABLE_GRID = '1fr 1fr 100px 80px';
export const SettingsAdminApps = () => {
const [searchQuery, setSearchQuery] = useState('');
const { data } = useQuery(FIND_ALL_APPLICATION_REGISTRATIONS);
const registrations: ApplicationRegistrationFragmentFragment[] =
data?.findAllApplicationRegistrations ?? [];
const filtered =
searchQuery.trim().length === 0
? registrations
: registrations.filter((registration) => {
const query = searchQuery.toLowerCase();
return (
registration.name.toLowerCase().includes(query) ||
(registration.sourcePackage ?? '').toLowerCase().includes(query) ||
registration.universalIdentifier.toLowerCase().includes(query)
);
});
return (
<Section>
<H2Title
title={t`All App Registrations`}
description={t`All application registrations across the platform, including orphaned marketplace apps`}
/>
<SearchInput
placeholder={t`Search registrations...`}
value={searchQuery}
onChange={setSearchQuery}
/>
<StyledTableContainer>
<Table>
<StyledTableHeaderRowContainer>
<TableRow gridTemplateColumns={TABLE_GRID}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Source`}</TableHeader>
<TableHeader>{t`Listed`}</TableHeader>
<TableHeader>{t`Featured`}</TableHeader>
</TableRow>
</StyledTableHeaderRowContainer>
<TableBody>
{filtered.map((registration) => (
<UndecoratedLink
key={registration.id}
to={getSettingsPath(
SettingsPath.ApplicationRegistrationDetail,
{ applicationRegistrationId: registration.id },
)}
fullWidth
>
<TableRow gridTemplateColumns={TABLE_GRID} isClickable>
<TableCell>{registration.name}</TableCell>
<TableCell>
{registration.sourcePackage ?? registration.sourceType}
</TableCell>
<TableCell>
<Status
color={registration.isListed ? 'green' : 'gray'}
text={registration.isListed ? t`Yes` : t`No`}
/>
</TableCell>
<TableCell>
<Status
color={registration.isFeatured ? 'yellow' : 'gray'}
text={registration.isFeatured ? t`Yes` : t`No`}
/>
</TableCell>
</TableRow>
</UndecoratedLink>
))}
</TableBody>
</Table>
</StyledTableContainer>
</Section>
);
};

View File

@@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
import { APPLICATION_REGISTRATION_FRAGMENT } from '@/settings/application-registrations/graphql/fragments/applicationRegistrationFragment';
export const FIND_ALL_APPLICATION_REGISTRATIONS = gql`
query FindAllApplicationRegistrations {
findAllApplicationRegistrations {
...ApplicationRegistrationFragment
}
}
${APPLICATION_REGISTRATION_FRAGMENT}
`;

View File

@@ -5,6 +5,7 @@ import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/Setting
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { t } from '@lingui/core/macro';
import {
IconApps,
IconHeart,
IconSettings2,
IconSparkles,
@@ -24,6 +25,12 @@ export const SettingsAdminContent = () => {
Icon: IconSettings2,
disabled: !canAccessFullAdminPanel && !canImpersonate,
},
{
id: SETTINGS_ADMIN_TABS.APPS,
title: t`Apps`,
Icon: IconApps,
disabled: !canAccessFullAdminPanel,
},
{
id: SETTINGS_ADMIN_TABS.AI,
title: t`AI`,

View File

@@ -1,4 +1,5 @@
import { SettingsAdminAI } from '@/settings/admin-panel/ai/components/SettingsAdminAI';
import { SettingsAdminApps } from '@/settings/admin-panel/apps/components/SettingsAdminApps';
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
@@ -16,6 +17,8 @@ export const SettingsAdminTabContent = () => {
switch (activeTabId) {
case SETTINGS_ADMIN_TABS.GENERAL:
return <SettingsAdminGeneral />;
case SETTINGS_ADMIN_TABS.APPS:
return <SettingsAdminApps />;
case SETTINGS_ADMIN_TABS.AI:
return <SettingsAdminAI />;
case SETTINGS_ADMIN_TABS.CONFIG_VARIABLES:

View File

@@ -45,13 +45,13 @@ export const SettingsAdminTableCard = ({
<TableRow
key={index + item.label}
gridAutoColumns={gridAutoColumns}
height={themeCssVariables.spacing[6]}
>
<TableCell
align={labelAlign}
color={themeCssVariables.font.color.tertiary}
height={themeCssVariables.spacing[6]}
height="auto"
gap={themeCssVariables.spacing[2]}
padding={`${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]}`}
>
{item.Icon && <item.Icon size={theme.icon.size.md} />}
<span>{item.label}</span>
@@ -59,9 +59,10 @@ export const SettingsAdminTableCard = ({
<TableCell
align={valueAlign}
color={themeCssVariables.font.color.primary}
height={themeCssVariables.spacing[6]}
height="auto"
onClick={item.onClick}
clickable={isDefined(item.onClick)}
padding={`${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]}`}
>
{item.value}
</TableCell>

View File

@@ -1,11 +1,5 @@
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { IconSearch } from 'twenty-ui/display';
const StyledSearchInputContainer = styled.div`
width: 100%;
`;
import { SearchInput } from 'twenty-ui/input';
type ConfigVariableSearchInputProps = {
value: string;
@@ -17,15 +11,10 @@ export const ConfigVariableSearchInput = ({
onChange,
}: ConfigVariableSearchInputProps) => {
return (
<StyledSearchInputContainer>
<SettingsTextInput
instanceId="config-variable-search"
placeholder={t`Search config variables`}
value={value}
onChange={onChange}
autoFocus={false}
LeftIcon={IconSearch}
/>
</StyledSearchInputContainer>
<SearchInput
placeholder={t`Search config variables`}
value={value}
onChange={onChange}
/>
);
};

View File

@@ -1,5 +1,6 @@
export const SETTINGS_ADMIN_TABS = {
GENERAL: 'general',
APPS: 'apps',
AI: 'ai',
CONFIG_VARIABLES: 'config-variables',
HEALTH_STATUS: 'health-status',

View File

@@ -16,6 +16,9 @@ export const APPLICATION_REGISTRATION_FRAGMENT = gql`
latestAvailableVersion
websiteUrl
termsUrl
isListed
isFeatured
ownerWorkspaceId
createdAt
updatedAt
}

View File

@@ -0,0 +1,16 @@
import { gql } from '@apollo/client';
export const TRANSFER_APPLICATION_REGISTRATION_OWNERSHIP = gql`
mutation TransferApplicationRegistrationOwnership(
$applicationRegistrationId: String!
$targetWorkspaceSubdomain: String!
) {
transferApplicationRegistrationOwnership(
applicationRegistrationId: $applicationRegistrationId
targetWorkspaceSubdomain: $targetWorkspaceSubdomain
) {
id
name
}
}
`;

View File

@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const APPLICATION_REGISTRATION_TARBALL_URL = gql`
query ApplicationRegistrationTarballUrl($id: String!) {
applicationRegistrationTarballUrl(id: $id)
}
`;

View File

@@ -1,8 +1,10 @@
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { DELETE_APPLICATION_REGISTRATION } from '@/settings/application-registrations/graphql/mutations/deleteApplicationRegistration';
import { ROTATE_APPLICATION_REGISTRATION_CLIENT_SECRET } from '@/settings/application-registrations/graphql/mutations/rotateApplicationRegistrationClientSecret';
import { TRANSFER_APPLICATION_REGISTRATION_OWNERSHIP } from '@/settings/application-registrations/graphql/mutations/transferApplicationRegistrationOwnership';
import { UPDATE_APPLICATION_REGISTRATION } from '@/settings/application-registrations/graphql/mutations/updateApplicationRegistration';
import { UPDATE_APPLICATION_REGISTRATION_VARIABLE } from '@/settings/application-registrations/graphql/mutations/updateApplicationRegistrationVariable';
import { APPLICATION_REGISTRATION_TARBALL_URL } from '@/settings/application-registrations/graphql/queries/applicationRegistrationTarballUrl';
import { FIND_APPLICATION_REGISTRATION_STATS } from '@/settings/application-registrations/graphql/queries/findApplicationRegistrationStats';
import { FIND_APPLICATION_REGISTRATION_VARIABLES } from '@/settings/application-registrations/graphql/queries/findApplicationRegistrationVariables';
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
@@ -16,6 +18,7 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useInstallMarketplaceApp } from '~/modules/marketplace/hooks/useInstallMarketplaceApp';
import { useMutation, useQuery } from '@apollo/client';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -25,10 +28,15 @@ import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined, isValidUrl } from 'twenty-shared/utils';
import {
H1Title,
H1TitleFontColor,
H2Title,
IconArrowRight,
IconChartBar,
IconCheck,
IconBox,
IconDownload,
IconExternalLink,
IconKey,
IconRefresh,
IconShield,
@@ -38,15 +46,33 @@ import {
IconWorld,
Status,
} from 'twenty-ui/display';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
Card,
Section,
SectionAlignment,
SectionFontColor,
} from 'twenty-ui/layout';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import {
ApplicationRegistrationSourceType,
useFindManyApplicationsQuery,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
StyledAppModal,
StyledAppModalButton,
StyledAppModalSection,
StyledAppModalTitle,
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
import { applicationRegistrationClientSecretFamilyState } from '~/pages/settings/applications/states/applicationRegistrationClientSecretFamilyState';
const DELETE_REGISTRATION_MODAL_ID = 'delete-application-registration-modal';
const ROTATE_SECRET_MODAL_ID = 'rotate-application-registration-secret-modal';
const TRANSFER_OWNERSHIP_MODAL_ID =
'transfer-application-registration-ownership-modal';
const StyledInputContainer = styled.div`
align-items: center;
@@ -99,6 +125,32 @@ const StyledRotateContainer = styled.div`
padding-top: ${themeCssVariables.spacing[2]};
`;
const StyledDangerButtonGroup = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledSourceRow = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledDownloadLink = styled.a`
color: ${themeCssVariables.font.color.secondary};
cursor: pointer;
text-decoration: underline;
&:hover {
color: ${themeCssVariables.font.color.primary};
}
`;
const StyledMarketplaceActions = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
padding-top: ${themeCssVariables.spacing[2]};
`;
type ServerVariable = {
id: string;
key: string;
@@ -113,7 +165,7 @@ export const SettingsApplicationRegistrationDetails = () => {
const navigate = useNavigateSettings();
const { copyToClipboard } = useCopyToClipboard();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { openModal } = useModal();
const { openModal, closeModal } = useModal();
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
}>();
@@ -123,11 +175,14 @@ export const SettingsApplicationRegistrationDetails = () => {
applicationRegistrationId,
);
const [isInstalling, setIsInstalling] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formRedirectUris, setFormRedirectUris] = useState<string[]>([]);
const [newRedirectUri, setNewRedirectUri] = useState('');
const [hasChanges, setHasChanges] = useState(false);
const [rotatedSecret, setRotatedSecret] = useState<string | null>(null);
const [transferSubdomain, setTransferSubdomain] = useState('');
const [isTransferring, setIsTransferring] = useState(false);
const [variableValues, setVariableValues] = useState<Record<string, string>>(
{},
@@ -158,6 +213,14 @@ export const SettingsApplicationRegistrationDetails = () => {
skip: !applicationRegistrationId,
});
const { data: tarballUrlData } = useQuery(
APPLICATION_REGISTRATION_TARBALL_URL,
{
variables: { id: applicationRegistrationId },
skip: !applicationRegistrationId,
},
);
const [updateRegistration] = useMutation(UPDATE_APPLICATION_REGISTRATION, {
refetchQueries: [
FIND_ONE_APPLICATION_REGISTRATION,
@@ -176,15 +239,120 @@ export const SettingsApplicationRegistrationDetails = () => {
refetchQueries: [FIND_APPLICATION_REGISTRATION_VARIABLES],
},
);
const [transferOwnership] = useMutation(
TRANSFER_APPLICATION_REGISTRATION_OWNERSHIP,
{
refetchQueries: [
FIND_ONE_APPLICATION_REGISTRATION,
FIND_MANY_APPLICATION_REGISTRATIONS,
],
},
);
const { install } = useInstallMarketplaceApp();
const { data: applicationsData, refetch: refetchApplications } =
useFindManyApplicationsQuery();
const registration = data?.findOneApplicationRegistration;
const variables: ServerVariable[] =
variablesData?.findApplicationRegistrationVariables ?? [];
const isNpmSource =
registration?.sourceType === ApplicationRegistrationSourceType.NPM;
if (loading || !registration) {
return null;
}
const isInstalledOnWorkspace = (
applicationsData?.findManyApplications ?? []
).some(
(application) =>
application.universalIdentifier === registration.universalIdentifier,
);
const handleToggleListed = async () => {
try {
await updateRegistration({
variables: {
input: {
id: applicationRegistrationId,
update: {
isListed: !registration.isListed,
},
},
},
});
enqueueSuccessSnackBar({
message: registration.isListed
? t`App removed from marketplace`
: t`App listed on marketplace`,
});
} catch {
enqueueErrorSnackBar({
message: t`Error updating marketplace listing`,
});
}
};
const marketplacePageUrl = getSettingsPath(
SettingsPath.AvailableApplicationDetail,
{
availableApplicationId: registration.universalIdentifier,
},
);
const handleInstallOnWorkspace = async () => {
setIsInstalling(true);
try {
const success = await install({
universalIdentifier: registration.universalIdentifier,
});
if (success) {
await refetchApplications();
enqueueSuccessSnackBar({
message: t`App installed on this workspace`,
});
}
} catch {
enqueueErrorSnackBar({
message: t`Error installing app`,
});
} finally {
setIsInstalling(false);
}
};
const handleTransferOwnership = async () => {
const trimmed = transferSubdomain.trim();
if (!isNonEmptyString(trimmed)) {
return;
}
setIsTransferring(true);
try {
await transferOwnership({
variables: {
applicationRegistrationId,
targetWorkspaceSubdomain: trimmed,
},
});
enqueueSuccessSnackBar({
message: t`Ownership transferred successfully`,
});
setTransferSubdomain('');
navigate(SettingsPath.Applications);
} catch {
enqueueErrorSnackBar({
message: t`Failed to transfer ownership. Check that the subdomain is correct.`,
});
} finally {
setIsTransferring(false);
}
};
const markDirty = () => setHasChanges(true);
const handleSave = async () => {
@@ -364,6 +532,47 @@ export const SettingsApplicationRegistrationDetails = () => {
t`Universal identifier copied`,
),
},
...(isNpmSource && isNonEmptyString(registration.sourcePackage)
? [
{
Icon: IconBox,
label: t`Package`,
value: registration.sourcePackage,
},
]
: registration.sourceType === ApplicationRegistrationSourceType.TARBALL
? [
{
Icon: IconBox,
label: t`Source`,
value: isNonEmptyString(
tarballUrlData?.applicationRegistrationTarballUrl,
) ? (
<StyledSourceRow>
<span>
<Trans>Tarball upload</Trans>
</span>
<StyledDownloadLink
href={tarballUrlData.applicationRegistrationTarballUrl}
download
>
<Trans>Download</Trans>
</StyledDownloadLink>
</StyledSourceRow>
) : (
t`Tarball upload`
),
},
]
: registration.sourceType === ApplicationRegistrationSourceType.LOCAL
? [
{
Icon: IconBox,
label: t`Source`,
value: t`Local development`,
},
]
: []),
];
const stats = statsData?.findApplicationRegistrationStats;
@@ -447,6 +656,61 @@ export const SettingsApplicationRegistrationDetails = () => {
/>
</Section>
<Section>
<H2Title
title={t`Marketplace Listing`}
description={t`Control visibility on the marketplace. Unlisted apps are still accessible via direct link.`}
/>
<Card rounded>
<SettingsOptionCardContentToggle
title={t`Listed on marketplace`}
description={
isNpmSource
? t`Managed by the marketplace catalog sync for npm packages`
: t`When enabled, this app appears in the marketplace browse page`
}
checked={registration.isListed}
onChange={handleToggleListed}
disabled={isNpmSource}
divider
/>
<SettingsOptionCardContentToggle
title={t`Featured`}
description={t`Featured apps are curated. Open a PR to request featured status.`}
checked={registration.isFeatured}
onChange={() => {}}
disabled
/>
</Card>
<StyledMarketplaceActions>
<Button
Icon={IconExternalLink}
title={t`View marketplace page`}
variant="secondary"
to={marketplacePageUrl}
/>
</StyledMarketplaceActions>
</Section>
<Section>
<H2Title
title={t`Installation`}
description={t`Install this app on the current workspace`}
/>
<Button
Icon={isInstalledOnWorkspace ? IconCheck : IconDownload}
title={
isInstalledOnWorkspace
? t`Installed`
: t`Install on this workspace`
}
variant="secondary"
accent="blue"
disabled={isInstalledOnWorkspace || isInstalling}
onClick={handleInstallOnWorkspace}
/>
</Section>
<Section>
<H2Title
title={t`OAuth Credentials`}
@@ -574,17 +838,26 @@ export const SettingsApplicationRegistrationDetails = () => {
description={
hasActiveInstalls
? t`Uninstall this app from all workspaces before deleting it`
: t`Delete this app`
: t`Delete or transfer this app registration`
}
/>
<Button
accent="danger"
variant="secondary"
title={t`Delete`}
Icon={IconTrash}
disabled={hasActiveInstalls}
onClick={() => openModal(DELETE_REGISTRATION_MODAL_ID)}
/>
<StyledDangerButtonGroup>
<Button
accent="danger"
variant="secondary"
title={t`Delete`}
Icon={IconTrash}
disabled={hasActiveInstalls}
onClick={() => openModal(DELETE_REGISTRATION_MODAL_ID)}
/>
<Button
accent="default"
variant="secondary"
title={t`Transfer ownership`}
Icon={IconArrowRight}
onClick={() => openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
/>
</StyledDangerButtonGroup>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
@@ -622,6 +895,58 @@ export const SettingsApplicationRegistrationDetails = () => {
confirmButtonText={t`Delete`}
loading={isLoading}
/>
<StyledAppModal
modalId={TRANSFER_OWNERSHIP_MODAL_ID}
isClosable
onClose={() => setTransferSubdomain('')}
padding="large"
dataGloballyPreventClickOutside
>
<StyledAppModalTitle>
<H1Title
title={t`Transfer ownership`}
fontColor={H1TitleFontColor.Primary}
/>
</StyledAppModalTitle>
<StyledAppModalSection
alignment={SectionAlignment.Center}
fontColor={SectionFontColor.Primary}
>
{t`Enter the workspace subdomain to transfer this app to. You will lose access to manage it.`}
</StyledAppModalSection>
<Section>
<SettingsTextInput
instanceId="transfer-ownership-subdomain"
value={transferSubdomain}
onChange={setTransferSubdomain}
placeholder={t`e.g. my-workspace`}
fullWidth
disableHotkeys
label={t`Target workspace subdomain`}
autoFocusOnMount
/>
</Section>
<StyledAppModalButton
onClick={() => {
closeModal(TRANSFER_OWNERSHIP_MODAL_ID);
setTransferSubdomain('');
}}
variant="secondary"
title={t`Cancel`}
fullWidth
/>
<StyledAppModalButton
onClick={handleTransferOwnership}
variant="secondary"
accent="danger"
title={t`Transfer`}
disabled={
!isNonEmptyString(transferSubdomain.trim()) || isTransferring
}
fullWidth
/>
</StyledAppModal>
</>
);
};

View File

@@ -7,16 +7,18 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
IconApps,
IconBox,
IconCheck,
IconColumns,
IconCommand,
IconDownload,
IconEyeOff,
IconFileText,
IconInfoCircle,
IconLayoutGrid,
@@ -27,7 +29,11 @@ import {
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { PermissionFlagType } from '~/generated-metadata/graphql';
import {
PermissionFlagType,
useFindOneApplicationByUniversalIdentifierQuery,
useFindOneMarketplaceAppQuery,
} from '~/generated-metadata/graphql';
import { useMarketplaceApps } from '~/modules/marketplace/hooks/useMarketplaceApps';
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
import { SettingsAvailableApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab';
@@ -240,6 +246,19 @@ const StyledProviderItem = styled.li`
margin-bottom: ${themeCssVariables.spacing[1]};
`;
const StyledUnlistedBanner = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.secondary};
display: flex;
font-size: ${themeCssVariables.font.size.md};
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[4]};
padding: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]};
`;
export const SettingsAvailableApplicationDetails = () => {
const { availableApplicationId = '' } = useParams<{
availableApplicationId: string;
@@ -252,10 +271,44 @@ export const SettingsAvailableApplicationDetails = () => {
const canInstallMarketplaceApps = useHasPermissionFlag(
PermissionFlagType.MARKETPLACE_APPS,
);
const { data: installedAppData } =
useFindOneApplicationByUniversalIdentifierQuery({
variables: { universalIdentifier: availableApplicationId },
skip: !availableApplicationId,
});
const application = useMemo(() => {
return marketplaceApps?.find((app) => app.id === availableApplicationId);
}, [availableApplicationId, marketplaceApps]);
const listedApp = marketplaceApps?.find(
(app) => app.id === availableApplicationId,
);
const { data: singleAppData } = useFindOneMarketplaceAppQuery({
variables: { universalIdentifier: availableApplicationId },
skip: isDefined(listedApp) || !availableApplicationId,
});
const singleApp = singleAppData?.findOneMarketplaceApp;
const application = isDefined(listedApp)
? listedApp
: isDefined(singleApp)
? {
...singleApp,
content: {
objects: (singleApp.objects ?? []).length,
fields:
(singleApp.objects ?? []).reduce(
(count, appObject) => count + appObject.fields.length,
0,
) + (singleApp.fields ?? []).length,
functions: (singleApp.logicFunctions ?? []).length,
frontComponents: (singleApp.frontComponents ?? []).length,
},
}
: undefined;
const isUnlisted = !isDefined(listedApp) && isDefined(application);
const isAlreadyInstalled = isDefined(installedAppData?.findOneApplication);
const handleInstall = async () => {
if (isDefined(application)) {
@@ -438,6 +491,12 @@ export const SettingsAvailableApplicationDetails = () => {
]}
>
<SettingsPageContainer>
{isUnlisted && (
<StyledUnlistedBanner>
<IconEyeOff size={16} />
{t`This application is not listed on the marketplace. It was shared via a direct link.`}
</StyledUnlistedBanner>
)}
<StyledHeader>
<StyledHeaderLeft>
<StyledLogo>
@@ -461,12 +520,18 @@ export const SettingsAvailableApplicationDetails = () => {
</StyledHeaderLeft>
{canInstallMarketplaceApps && (
<Button
Icon={IconDownload}
title={isInstalling ? t`Installing...` : t`Install`}
variant="primary"
accent="blue"
Icon={isAlreadyInstalled ? IconCheck : IconDownload}
title={
isAlreadyInstalled
? t`Installed`
: isInstalling
? t`Installing...`
: t`Install`
}
variant={isAlreadyInstalled ? 'secondary' : 'primary'}
accent={isAlreadyInstalled ? 'default' : 'blue'}
onClick={handleInstall}
disabled={isInstalling}
disabled={isAlreadyInstalled || isInstalling}
/>
)}
</StyledHeader>

View File

@@ -6,7 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
import { IconCircleDot, IconStatusChange, IconUpload } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AppRegistrationSourceType,
ApplicationRegistrationSourceType,
type Application,
} from '~/generated-metadata/graphql';
import { isNewerSemver } from '~/pages/settings/applications/utils/isNewerSemver';
@@ -26,7 +26,7 @@ export const SettingsApplicationVersionContainer = ({
const currentVersion = application?.version;
const sourceType = application?.applicationRegistration?.sourceType;
const isNpmApp = sourceType === AppRegistrationSourceType.NPM;
const isNpmApp = sourceType === ApplicationRegistrationSourceType.NPM;
const latestVersion = isNpmApp
? (latestAvailableVersion ?? currentVersion)

View File

@@ -1,4 +1,4 @@
import { H2Title, IconChevronRight, IconSearch } from 'twenty-ui/display';
import { H2Title, IconChevronRight } from 'twenty-ui/display';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
@@ -10,13 +10,13 @@ import {
APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
SettingsApplicationTableRow,
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
import { useContext, useMemo, useState } from 'react';
import { useContext, useState } from 'react';
import { type ApplicationWithoutRelation } from '~/pages/settings/applications/types/applicationWithoutRelation';
import { isNewerSemver } from '~/pages/settings/applications/utils/isNewerSemver';
import { Section } from 'twenty-ui/layout';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { SearchInput } from 'twenty-ui/input';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { AppRegistrationSourceType } from '~/generated-metadata/graphql';
import { ApplicationRegistrationSourceType } from '~/generated-metadata/graphql';
const StyledTableContainer = styled.div`
margin-top: ${themeCssVariables.spacing[3]};
@@ -41,15 +41,13 @@ export const SettingsApplicationsTable = ({
const [searchTerm, setSearchTerm] = useState('');
const filteredApplications = useMemo(() => {
return applications.filter(
(application) =>
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(application.description ?? '')
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
}, [applications, searchTerm]);
const filteredApplications = applications.filter(
(application) =>
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(application.description ?? '')
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
return (
<Section>
@@ -58,9 +56,7 @@ export const SettingsApplicationsTable = ({
description={t`List installed applications. Use filter to search for a specific application`}
/>
<StyledSearchInputContainer>
<SettingsTextInput
instanceId="env-var-search"
LeftIcon={IconSearch}
<SearchInput
placeholder={t`Search an application`}
value={searchTerm}
onChange={setSearchTerm}
@@ -81,7 +77,7 @@ export const SettingsApplicationsTable = ({
{filteredApplications.map((application) => {
const isNpmApp =
application.applicationRegistration?.sourceType ===
AppRegistrationSourceType.NPM;
ApplicationRegistrationSourceType.NPM;
const latestVersion =
application.applicationRegistration?.latestAvailableVersion;

View File

@@ -1,14 +1,15 @@
import { styled } from '@linaria/react';
import { useState } from 'react';
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useApolloClient } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useInstallNpmApp } from '~/modules/marketplace/hooks/useInstallNpmApp';
import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
import { useRegisterNpmPackage } from '~/modules/marketplace/hooks/useRegisterNpmPackage';
import {
StyledAppModal,
StyledAppModalButton,
@@ -16,7 +17,7 @@ import {
StyledAppModalTitle,
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
export const INSTALL_NPM_APP_MODAL_ID = 'install-npm-app-modal';
export const REGISTER_NPM_APP_MODAL_ID = 'register-npm-app-modal';
const StyledInputGroup = styled.div`
display: flex;
@@ -24,51 +25,49 @@ const StyledInputGroup = styled.div`
gap: ${themeCssVariables.spacing[3]};
`;
export const SettingsInstallNpmAppModal = () => {
export const SettingsRegisterNpmAppModal = () => {
const { t } = useLingui();
const { closeModal } = useModal();
const { install, isInstalling } = useInstallNpmApp();
const { refetch } = useFindManyApplicationsQuery();
const { register, isRegistering } = useRegisterNpmPackage();
const apolloClient = useApolloClient();
const [packageName, setPackageName] = useState('');
const [version, setVersion] = useState('');
const isValid = packageName.trim().length > 0;
const handleInstall = async () => {
const handleRegister = async () => {
if (!isValid) {
return;
}
const success = await install({
const success = await register({
packageName: packageName.trim(),
version: version.trim() || undefined,
});
if (success) {
await refetch();
closeModal(INSTALL_NPM_APP_MODAL_ID);
await apolloClient.refetchQueries({
include: [FIND_MANY_APPLICATION_REGISTRATIONS],
});
closeModal(REGISTER_NPM_APP_MODAL_ID);
setPackageName('');
setVersion('');
}
};
const handleCancel = () => {
closeModal(INSTALL_NPM_APP_MODAL_ID);
closeModal(REGISTER_NPM_APP_MODAL_ID);
setPackageName('');
setVersion('');
};
return (
<StyledAppModal
modalId={INSTALL_NPM_APP_MODAL_ID}
modalId={REGISTER_NPM_APP_MODAL_ID}
isClosable={true}
padding="large"
dataGloballyPreventClickOutside
>
<StyledAppModalTitle>
<H1Title
title={t`Install from npm`}
title={t`Register from npm`}
fontColor={H1TitleFontColor.Primary}
/>
</StyledAppModalTitle>
@@ -76,7 +75,7 @@ export const SettingsInstallNpmAppModal = () => {
alignment={SectionAlignment.Center}
fontColor={SectionFontColor.Primary}
>
{t`Enter the npm package name of the application you want to install.`}
{t`Enter the npm package name to register as an application.`}
</StyledAppModalSection>
<Section>
@@ -91,15 +90,6 @@ export const SettingsInstallNpmAppModal = () => {
label={t`Package name`}
autoFocusOnMount
/>
<SettingsTextInput
instanceId="npm-version-input"
value={version}
onChange={setVersion}
placeholder={t`latest`}
fullWidth
disableHotkeys
label={t`Version (optional)`}
/>
</StyledInputGroup>
</Section>
@@ -110,11 +100,11 @@ export const SettingsInstallNpmAppModal = () => {
fullWidth
/>
<StyledAppModalButton
onClick={handleInstall}
onClick={handleRegister}
variant="secondary"
accent="blue"
title={t`Install`}
disabled={!isValid || isInstalling}
title={t`Register`}
disabled={!isValid || isRegistering}
fullWidth
/>
</StyledAppModal>

View File

@@ -1,14 +1,17 @@
import { styled } from '@linaria/react';
import { useRef } from 'react';
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useApolloClient, useMutation } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import { SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
import { INSTALL_APPLICATION } from '~/modules/marketplace/graphql/mutations/installApplication';
import { useUploadAppTarball } from '~/modules/marketplace/hooks/useUploadAppTarball';
import { useInstallMarketplaceApp } from '~/modules/marketplace/hooks/useInstallMarketplaceApp';
import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
import {
StyledAppModal,
StyledAppModalButton,
@@ -23,11 +26,12 @@ const StyledFileInput = styled.input`
`;
export const SettingsUploadTarballModal = () => {
const { t } = useLingui();
const { t: tFn } = useLingui();
const { closeModal } = useModal();
const { upload, isUploading } = useUploadAppTarball();
const { install } = useInstallMarketplaceApp();
const { refetch } = useFindManyApplicationsQuery();
const [installApplication] = useMutation(INSTALL_APPLICATION);
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const apolloClient = useApolloClient();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleSelectFile = () => {
@@ -46,16 +50,31 @@ export const SettingsUploadTarballModal = () => {
try {
const uploadResult = await upload(file);
if (uploadResult.success) {
const installResult = await install({
universalIdentifier: uploadResult.universalIdentifier,
});
if (!uploadResult.success) {
return;
}
if (installResult) {
await refetch();
closeModal(UPLOAD_TARBALL_MODAL_ID);
const registrationId = uploadResult.registrationId;
if (isDefined(registrationId)) {
try {
await installApplication({
variables: { appRegistrationId: registrationId },
});
enqueueSuccessSnackBar({
message: t`Application installed successfully.`,
});
} catch {
enqueueErrorSnackBar({
message: t`Tarball uploaded but installation failed.`,
});
}
}
await apolloClient.refetchQueries({
include: [FIND_MANY_APPLICATION_REGISTRATIONS],
});
closeModal(UPLOAD_TARBALL_MODAL_ID);
} finally {
if (isDefined(fileInputRef.current)) {
fileInputRef.current.value = '';
@@ -76,7 +95,7 @@ export const SettingsUploadTarballModal = () => {
>
<StyledAppModalTitle>
<H1Title
title={t`Upload tarball`}
title={tFn`Upload tarball`}
fontColor={H1TitleFontColor.Primary}
/>
</StyledAppModalTitle>
@@ -84,7 +103,7 @@ export const SettingsUploadTarballModal = () => {
alignment={SectionAlignment.Center}
fontColor={SectionFontColor.Primary}
>
{t`Select a .tar.gz application package to upload and install.`}
{tFn`Select a .tar.gz application package to upload and install.`}
</StyledAppModalSection>
<StyledFileInput
@@ -97,14 +116,14 @@ export const SettingsUploadTarballModal = () => {
<StyledAppModalButton
onClick={handleCancel}
variant="secondary"
title={t`Cancel`}
title={tFn`Cancel`}
fullWidth
/>
<StyledAppModalButton
onClick={handleSelectFile}
variant="secondary"
accent="blue"
title={t`Choose file`}
title={tFn`Choose file`}
disabled={isUploading}
fullWidth
/>

View File

@@ -1,11 +1,16 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { type ReactNode, useState } from 'react';
import { IconSparkles } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { SettingsAvailableApplicationCard } from '~/pages/settings/applications/components/SettingsAvailableApplicationCard';
import { useMarketplaceApps } from '~/modules/marketplace/hooks/useMarketplaceApps';
import { SettingsAvailableApplicationCard } from '~/pages/settings/applications/components/SettingsAvailableApplicationCard';
const StyledSearchInputContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
@@ -27,24 +32,43 @@ const StyledEmptyState = styled.div`
text-align: center;
`;
const StyledHintLink = styled.button`
background: none;
border: none;
color: ${themeCssVariables.color.blue};
cursor: pointer;
font-size: inherit;
padding: 0;
text-decoration: underline;
`;
export const SettingsApplicationsAvailableTab = () => {
const { t } = useLingui();
const [searchTerm, setSearchTerm] = useState('');
const [showFeaturedOnly, setShowFeaturedOnly] = useState(true);
const { data: marketplaceApps, isLoading } = useMarketplaceApps();
const applications = marketplaceApps;
const textFilteredApplications = searchTerm
? marketplaceApps.filter((application) => {
const lowerSearch = searchTerm.toLowerCase();
const filteredApplications = useMemo(() => {
return applications.filter(
(application) =>
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
application.description
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
application.author.toLowerCase().includes(searchTerm.toLowerCase()),
);
}, [applications, searchTerm]);
return (
application.name.toLowerCase().includes(lowerSearch) ||
application.description.toLowerCase().includes(lowerSearch) ||
application.author.toLowerCase().includes(lowerSearch)
);
})
: marketplaceApps;
const filteredApplications = showFeaturedOnly
? textFilteredApplications.filter((application) => application.isFeatured)
: textFilteredApplications;
const nonFeaturedCount = showFeaturedOnly
? textFilteredApplications.filter((application) => !application.isFeatured)
.length
: 0;
if (isLoading) {
return (
@@ -54,6 +78,9 @@ export const SettingsApplicationsAvailableTab = () => {
);
}
const showNonFeaturedHint =
filteredApplications.length === 0 && nonFeaturedCount > 0;
return (
<Section>
<StyledSearchInputContainer>
@@ -61,11 +88,43 @@ export const SettingsApplicationsAvailableTab = () => {
placeholder={t`Search an application`}
value={searchTerm}
onChange={setSearchTerm}
filterDropdown={(filterButton: ReactNode) => (
<Dropdown
dropdownId="marketplace-filter-dropdown"
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 8 }}
clickableComponent={filterButton}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconSparkles}
onToggleChange={() =>
setShowFeaturedOnly(!showFeaturedOnly)
}
toggled={showFeaturedOnly}
text={t`Featured only`}
toggleSize="small"
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
)}
/>
</StyledSearchInputContainer>
{filteredApplications.length === 0 ? (
<StyledEmptyState>{t`No applications available`}</StyledEmptyState>
<StyledEmptyState>
{showNonFeaturedHint
? t`No featured applications found. ${nonFeaturedCount} non-featured result(s) available — `
: t`No applications available`}
{showNonFeaturedHint && (
<StyledHintLink onClick={() => setShowFeaturedOnly(false)}>
{t`show all`}
</StyledHintLink>
)}
</StyledEmptyState>
) : (
<StyledCardsGrid>
{filteredApplications.map((application) => (

View File

@@ -2,6 +2,11 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
@@ -13,20 +18,40 @@ import {
CommandBlock,
H2Title,
IconApps,
IconChevronDown,
IconChevronRight,
IconCopy,
IconDownload,
IconFileInfo,
IconUpload,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Button, ButtonGroup, IconButton } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useContext } from 'react';
import { MenuItem } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import {
REGISTER_NPM_APP_MODAL_ID,
SettingsRegisterNpmAppModal,
} from '~/pages/settings/applications/components/SettingsRegisterNpmAppModal';
import {
SettingsUploadTarballModal,
UPLOAD_TARBALL_MODAL_ID,
} from '~/pages/settings/applications/components/SettingsUploadTarballModal';
const REGISTER_APP_DROPDOWN_ID = 'register-app-dropdown';
const StyledButtonContainer = styled.div`
margin: ${themeCssVariables.spacing[2]} 0;
`;
const StyledButtonGroupContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-bottom: ${themeCssVariables.spacing[4]};
`;
type ApplicationRegistration = {
id: string;
name: string;
@@ -38,6 +63,8 @@ export const SettingsApplicationsDeveloperTab = () => {
const { t } = useLingui();
const navigate = useNavigate();
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const { openModal } = useModal();
const { closeDropdown } = useCloseDropdown();
const { copyToClipboard } = useCopyToClipboard();
@@ -63,6 +90,16 @@ export const SettingsApplicationsDeveloperTab = () => {
/>
);
const handleRegisterFromNpm = () => {
closeDropdown(REGISTER_APP_DROPDOWN_ID);
openModal(REGISTER_NPM_APP_MODAL_ID);
};
const handleUploadTarball = () => {
closeDropdown(REGISTER_APP_DROPDOWN_ID);
openModal(UPLOAD_TARBALL_MODAL_ID);
};
return (
<>
<Section>
@@ -87,12 +124,12 @@ export const SettingsApplicationsDeveloperTab = () => {
/>
</StyledButtonContainer>
</Section>
{registrations.length > 0 && (
<Section>
<H2Title
title={t`My Apps`}
description={t`Apps you've created and published`}
/>
<Section>
<H2Title
title={t`My Apps`}
description={t`Apps you've created, registered, or published`}
/>
{registrations.length > 0 && (
<SettingsListCard
items={registrations}
getItemLabel={(registration) => registration.name}
@@ -112,8 +149,47 @@ export const SettingsApplicationsDeveloperTab = () => {
/>
)}
/>
</Section>
)}
)}
</Section>
<StyledButtonGroupContainer>
<ButtonGroup size="small" variant="secondary">
<Button
Icon={IconDownload}
title={t`Register from npm`}
onClick={handleRegisterFromNpm}
/>
<Dropdown
dropdownId={REGISTER_APP_DROPDOWN_ID}
clickableComponent={
<IconButton
size="small"
variant="secondary"
Icon={IconChevronDown}
position="right"
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconDownload}
text={t`Register from npm`}
onClick={handleRegisterFromNpm}
/>
<MenuItem
LeftIcon={IconUpload}
text={t`Upload tarball`}
onClick={handleUploadTarball}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</ButtonGroup>
</StyledButtonGroupContainer>
<SettingsRegisterNpmAppModal />
<SettingsUploadTarballModal />
</>
);
};

View File

@@ -1,99 +1,14 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { IconChevronDown, IconDownload, IconUpload } from 'twenty-ui/display';
import { Button, ButtonGroup, IconButton } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MenuItem } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
import {
INSTALL_NPM_APP_MODAL_ID,
SettingsInstallNpmAppModal,
} from '~/pages/settings/applications/components/SettingsInstallNpmAppModal';
import {
SettingsUploadTarballModal,
UPLOAD_TARBALL_MODAL_ID,
} from '~/pages/settings/applications/components/SettingsUploadTarballModal';
const INSTALL_APP_DROPDOWN_ID = 'install-app-dropdown';
const StyledButtonGroupContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-bottom: ${themeCssVariables.spacing[4]};
`;
export const SettingsApplicationsInstalledTab = () => {
const { t } = useLingui();
const { data } = useFindManyApplicationsQuery();
const { openModal } = useModal();
const { closeDropdown } = useCloseDropdown();
const applications = data?.findManyApplications ?? [];
const handleInstallFromNpm = () => {
closeDropdown(INSTALL_APP_DROPDOWN_ID);
openModal(INSTALL_NPM_APP_MODAL_ID);
};
if (applications.length === 0) {
return null;
}
const handleUploadTarball = () => {
closeDropdown(INSTALL_APP_DROPDOWN_ID);
openModal(UPLOAD_TARBALL_MODAL_ID);
};
return (
<>
{applications.length > 0 && (
<SettingsApplicationsTable applications={applications} />
)}
<Section>
<StyledButtonGroupContainer>
<ButtonGroup size="small" variant="secondary">
<Button
Icon={IconDownload}
title={t`Install app`}
onClick={handleInstallFromNpm}
/>
<Dropdown
dropdownId={INSTALL_APP_DROPDOWN_ID}
clickableComponent={
<IconButton
size="small"
variant="secondary"
Icon={IconChevronDown}
position="right"
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconDownload}
text={t`Install from npm`}
onClick={handleInstallFromNpm}
/>
<MenuItem
LeftIcon={IconUpload}
text={t`Upload tarball`}
onClick={handleUploadTarball}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</ButtonGroup>
</StyledButtonGroupContainer>
</Section>
<SettingsInstallNpmAppModal />
<SettingsUploadTarballModal />
</>
);
return <SettingsApplicationsTable applications={applications} />;
};

View File

@@ -21,11 +21,11 @@
"outDir": "dist/assets"
},
{
"include": "engine/core-modules/application/constants/seed-dependencies/**",
"include": "engine/core-modules/application/application-package/constants/seed-dependencies/**",
"outDir": "dist/assets"
},
{
"include": "engine/core-modules/application/constants/yarn-engine/**",
"include": "engine/core-modules/application/application-package/constants/yarn-engine/**",
"outDir": "dist/assets"
},
{

View File

@@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { AppVersionCheckCronCommand } from 'src/engine/core-modules/application/application-version-check/crons/commands/app-version-check.cron.command';
import { ApplicationVersionCheckCronCommand } from 'src/engine/core-modules/application/application-upgrade/crons/commands/application-version-check.cron.command';
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command';
import { EventLogCleanupCronCommand } from 'src/engine/core-modules/event-logs/cleanup/commands/event-log-cleanup.cron.command';
import { CheckPublicDomainsValidRecordsCronCommand } from 'src/engine/core-modules/public-domain/crons/commands/check-public-domains-valid-records.cron.command';
@@ -55,7 +55,7 @@ export class CronRegisterAllCommand extends CommandRunner {
private readonly trashCleanupCronCommand: TrashCleanupCronCommand,
private readonly eventLogCleanupCronCommand: EventLogCleanupCronCommand,
private readonly marketplaceCatalogSyncCronCommand: MarketplaceCatalogSyncCronCommand,
private readonly appVersionCheckCronCommand: AppVersionCheckCronCommand,
private readonly applicationVersionCheckCronCommand: ApplicationVersionCheckCronCommand,
) {
super();
}
@@ -145,8 +145,8 @@ export class CronRegisterAllCommand extends CommandRunner {
command: this.marketplaceCatalogSyncCronCommand,
},
{
name: 'AppVersionCheck',
command: this.appVersionCheckCronCommand,
name: 'ApplicationVersionCheck',
command: this.applicationVersionCheckCronCommand,
},
];

View File

@@ -9,7 +9,7 @@ import { UpgradeVersionCommandModule } from 'src/database/commands/upgrade-versi
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { GenerateApiKeyCommand } from 'src/engine/core-modules/api-key/commands/generate-api-key.command';
import { AppVersionCheckModule } from 'src/engine/core-modules/application/application-version-check/application-version-check.module';
import { ApplicationUpgradeModule } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.module';
import { MarketplaceModule } from 'src/engine/core-modules/application/application-marketplace/marketplace.module';
import { EventLogCleanupModule } from 'src/engine/core-modules/event-logs/cleanup/event-log-cleanup.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@@ -58,7 +58,7 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au
PublicDomainModule,
EventLogCleanupModule,
MarketplaceModule,
AppVersionCheckModule,
ApplicationUpgradeModule,
],
providers: [
DataSeedWorkspaceCommand,

View File

@@ -10,7 +10,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/application-package/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileSettings } from 'src/engine/core-modules/file/types/file-settings.types';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';

View File

@@ -0,0 +1,61 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddIsListedToAppRegistration1772732588833
implements MigrationInterface
{
name = 'AddIsListedToAppRegistration1772732588833';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ADD "isListed" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "FK_applicationRegistration_workspaceId"`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_applicationRegistration_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_applicationRegistration_workspaceId"`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_94ab20372e448d45088357f884e"`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_94ab20372e448d45088357f884e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_applicationRegistration_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_94ab20372e448d45088357f884e"`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_94ab20372e448d45088357f884e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "FK_applicationRegistration_workspaceId"`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_applicationRegistration_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."applicationRegistration" DROP COLUMN "isListed"`,
);
}
}

View File

@@ -6,6 +6,7 @@ import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/adm
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
@@ -40,6 +41,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
ImpersonationModule,
PermissionsModule,
SecureHttpClientModule,
ApplicationRegistrationModule,
],
providers: [
AdminPanelResolver,

View File

@@ -7,6 +7,8 @@ import { PermissionFlagType } from 'twenty-shared/constants';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
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 { AdminAIModelsDTO } from 'src/engine/core-modules/client-config/client-config.entity';
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { ConfigVariablesDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables.dto';
@@ -56,8 +58,9 @@ import { QueueMetricsDataDTO } from './dtos/queue-metrics-data.dto';
)
export class AdminPanelResolver {
constructor(
private adminService: AdminPanelService,
private adminPanelHealthService: AdminPanelHealthService,
private readonly adminService: AdminPanelService,
private readonly adminPanelHealthService: AdminPanelHealthService,
private readonly applicationRegistrationService: ApplicationRegistrationService,
private adminPanelQueueService: AdminPanelQueueService,
private featureFlagService: FeatureFlagService,
private readonly twentyConfigService: TwentyConfigService,
@@ -267,4 +270,12 @@ export class AdminPanelResolver {
jobIds,
);
}
@UseGuards(AdminPanelGuard)
@Query(() => [ApplicationRegistrationEntity])
async findAllApplicationRegistrations(): Promise<
ApplicationRegistrationEntity[]
> {
return this.applicationRegistrationService.findAll();
}
}

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
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 { 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';
@@ -12,9 +12,9 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
@Module({
imports: [
ApplicationInstallModule,
ApplicationRegistrationModule,
ApplicationModule,
ApplicationManifestModule,
ApplicationRegistrationModule,
FeatureFlagModule,
TokenModule,
FileStorageModule,

View File

@@ -1,5 +1,4 @@
import {
Logger,
UseFilters,
UseGuards,
UseInterceptors,
@@ -13,24 +12,22 @@ import { FileFolder, FeatureFlagKey } from 'twenty-shared/types';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration/application-registration-variable.service';
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 { AppRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum';
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 {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import { CreateApplicationInput } from 'src/engine/core-modules/application/dtos/create-application.input';
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/dtos/generate-application-token.input';
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput';
import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/dtos/workspace-migration.dto';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-install/application-sync.service';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/dtos/application-token-pair.dto';
import { ApplicationInput } from 'src/engine/core-modules/application/application-development/dtos/application.input';
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 { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
@@ -58,12 +55,10 @@ import { streamToBuffer } from 'src/utils/stream-to-buffer';
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
)
export class ApplicationDevelopmentResolver {
private readonly logger = new Logger(ApplicationDevelopmentResolver.name);
constructor(
private readonly applicationTokenService: ApplicationTokenService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationService: ApplicationService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationRegistrationService: ApplicationRegistrationService,
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
private readonly fileStorageService: FileStorageService,
@@ -87,19 +82,17 @@ export class ApplicationDevelopmentResolver {
@Args() { manifest }: ApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<WorkspaceMigrationDTO> {
const applicationRegistrationId =
await this.resolveApplicationRegistrationId(
manifest.application.universalIdentifier,
{
name: manifest.application.displayName,
description: manifest.application.description,
logoUrl: manifest.application.logoUrl,
author: manifest.application.author,
websiteUrl: manifest.application.websiteUrl,
termsUrl: manifest.application.termsUrl,
},
workspaceId,
);
const applicationRegistrationId = await this.findApplicationRegistrationId(
manifest.application.universalIdentifier,
workspaceId,
);
await this.ensureApplicationExists({
universalIdentifier: manifest.application.universalIdentifier,
name: manifest.application.displayName,
workspaceId,
applicationRegistrationId,
});
const workspaceMigration =
await this.applicationSyncService.synchronizeFromManifest({
@@ -121,19 +114,6 @@ export class ApplicationDevelopmentResolver {
};
}
@Mutation(() => ApplicationDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async createOneApplication(
@Args('input') input: CreateApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
return await this.applicationService.create({
...input,
sourceType: AppRegistrationSourceType.LOCAL,
workspaceId,
});
}
@Mutation(() => FileDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
@@ -176,16 +156,8 @@ export class ApplicationDevelopmentResolver {
});
}
private async resolveApplicationRegistrationId(
private async findApplicationRegistrationId(
universalIdentifier: string,
metadata: {
name: string;
description?: string;
logoUrl?: string;
author?: string;
websiteUrl?: string;
termsUrl?: string;
},
workspaceId: string,
): Promise<string> {
const existingRegistration =
@@ -193,35 +165,27 @@ export class ApplicationDevelopmentResolver {
universalIdentifier,
);
if (existingRegistration) {
const isOwner =
await this.applicationRegistrationService.isOwnedByWorkspace(
existingRegistration.id,
workspaceId,
);
if (!isOwner) {
throw new ApplicationException(
'Cannot sync application: registration is owned by another workspace',
ApplicationExceptionCode.FORBIDDEN,
);
}
return existingRegistration.id;
if (!existingRegistration) {
throw new ApplicationException(
`No registration found for "${universalIdentifier}". Create one first with createApplicationRegistration.`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
const { applicationRegistration: newRegistration } =
await this.applicationRegistrationService.create(
{ ...metadata, universalIdentifier },
const isOwner =
await this.applicationRegistrationService.isOwnedByWorkspace(
existingRegistration.id,
workspaceId,
null,
);
this.logger.log(
`Created app registration for ${metadata.name} (${universalIdentifier})`,
);
if (!isOwner) {
throw new ApplicationException(
'Cannot sync application: registration is owned by another workspace',
ApplicationExceptionCode.FORBIDDEN,
);
}
return newRegistration.id;
return existingRegistration.id;
}
private async syncRegistrationMetadata(
@@ -259,4 +223,29 @@ export class ApplicationDevelopmentResolver {
}
}
}
private async ensureApplicationExists(params: {
universalIdentifier: string;
name: string;
workspaceId: string;
applicationRegistrationId: string;
}): Promise<void> {
const existing = await this.applicationService.findByUniversalIdentifier({
universalIdentifier: params.universalIdentifier,
workspaceId: params.workspaceId,
});
if (existing) {
return;
}
await this.applicationService.create({
universalIdentifier: params.universalIdentifier,
name: params.name,
sourcePath: params.universalIdentifier,
sourceType: ApplicationRegistrationSourceType.LOCAL,
applicationRegistrationId: params.applicationRegistrationId,
workspaceId: params.workspaceId,
});
}
}

View File

@@ -21,6 +21,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
case ApplicationExceptionCode.FIELD_NOT_FOUND:
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
case ApplicationExceptionCode.APP_NOT_INSTALLED:
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
case ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND:
throw new NotFoundError(exception);

View File

@@ -4,66 +4,26 @@ 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 { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
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 { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
import { AppPackageFetcherService } from 'src/engine/core-modules/application/application-install/app-package-fetcher.service';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-install/application-manifest-migration.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-install/application-sync.service';
import { AppUpgradeService } from 'src/engine/core-modules/application/application-install/app-upgrade.service';
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/application/application-variable/application-variable.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.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';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
@Module({
imports: [
TypeOrmModule.forFeature([
FileEntity,
ApplicationRegistrationEntity,
ApplicationEntity,
]),
TypeOrmModule.forFeature([ApplicationRegistrationEntity]),
ApplicationModule,
ApplicationManifestModule,
ApplicationPackageModule,
CacheLockModule,
FeatureFlagModule,
ApplicationVariableEntityModule,
TokenModule,
WorkspaceMigrationModule,
PermissionsModule,
ObjectPermissionModule,
PermissionFlagModule,
WorkflowCommonModule,
CodeStepBuildModule,
FileStorageModule,
WorkspaceCacheModule,
WorkspaceMigrationRunnerModule,
TwentyConfigModule,
],
providers: [
ApplicationInstallResolver,
ApplicationManifestMigrationService,
ApplicationSyncService,
AppPackageFetcherService,
ApplicationInstallService,
AppUpgradeService,
WorkspaceMigrationGraphqlApiExceptionInterceptor,
],
exports: [
ApplicationSyncService,
AppPackageFetcherService,
ApplicationInstallService,
AppUpgradeService,
],
providers: [ApplicationInstallResolver, ApplicationInstallService],
exports: [ApplicationInstallService],
})
export class ApplicationInstallModule {}

View File

@@ -1,9 +1,4 @@
import {
UseFilters,
UseGuards,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
@@ -12,18 +7,10 @@ import { FeatureFlagKey } from 'twenty-shared/types';
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 {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-install/application-sync.service';
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/dtos/application-token-pair.dto';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { InstallApplicationInput } from 'src/engine/core-modules/application/dtos/install-application.input';
import { UninstallApplicationInput } from 'src/engine/core-modules/application/dtos/uninstallApplicationInput';
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 { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -31,25 +18,17 @@ import {
FeatureFlagGuard,
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
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';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
@UseFilters(ApplicationExceptionFilter, AuthGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
export class ApplicationInstallResolver {
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationTokenService: ApplicationTokenService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationInstallService: ApplicationInstallService,
) {}
@Query(() => [ApplicationDTO])
@@ -78,84 +57,19 @@ export class ApplicationInstallResolver {
});
}
@Mutation(() => ApplicationTokenPairDTO)
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async renewApplicationToken(
@Args('applicationRefreshToken') applicationRefreshToken: string,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ApplicationTokenPairDTO> {
const applicationRefreshTokenPayload =
this.applicationTokenService.validateApplicationRefreshToken(
applicationRefreshToken,
);
if (applicationRefreshTokenPayload.workspaceId !== workspaceId) {
throw new ApplicationException(
'Refresh token workspace does not match authenticated workspace',
ApplicationExceptionCode.FORBIDDEN,
);
}
return this.applicationTokenService.renewApplicationTokens(
applicationRefreshTokenPayload,
);
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async installApplication(
@Args() { workspaceMigration: { actions } }: InstallApplicationInput,
@Args('appRegistrationId') appRegistrationId: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute(
workspaceId,
['featureFlagsMap'],
);
if (
featureFlagsMap[
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED
] !== true
) {
throw new ApplicationException(
'Application installation from tarball is not enabled',
ApplicationExceptionCode.FORBIDDEN,
);
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
await this.workspaceMigrationRunnerService.run({
workspaceMigration: {
actions,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
): Promise<boolean> {
return this.applicationInstallService.installApplication({
appRegistrationId,
version,
workspaceId,
});
return true;
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async uninstallApplication(
@Args() { universalIdentifier }: UninstallApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.uninstallApplication({
applicationUniversalIdentifier: universalIdentifier,
workspaceId,
});
return true;
}
}

View File

@@ -8,14 +8,19 @@ import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { AppRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
AppPackageFetcherService,
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import {
ApplicationPackageFetcherService,
type ResolvedPackage,
} from 'src/engine/core-modules/application/application-install/app-package-fetcher.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-install/application-sync.service';
} from 'src/engine/core-modules/application/application-package/application-package-fetcher.service';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.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';
@@ -43,9 +48,8 @@ export class ApplicationInstallService {
constructor(
@InjectRepository(ApplicationRegistrationEntity)
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly appPackageFetcherService: AppPackageFetcherService,
private readonly applicationService: ApplicationService,
private readonly applicationPackageFetcherService: ApplicationPackageFetcherService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly fileStorageService: FileStorageService,
private readonly cacheLockService: CacheLockService,
@@ -56,11 +60,20 @@ export class ApplicationInstallService {
version?: string;
workspaceId: string;
}): Promise<boolean> {
const appRegistration = await this.appRegistrationRepository.findOneOrFail({
const appRegistration = await this.appRegistrationRepository.findOne({
where: { id: params.appRegistrationId },
});
if (appRegistration.sourceType === AppRegistrationSourceType.LOCAL) {
if (!appRegistration) {
throw new ApplicationException(
`Application registration with id ${params.appRegistrationId} not found`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
if (
appRegistration.sourceType === ApplicationRegistrationSourceType.LOCAL
) {
this.logger.log(
`Skipping install for LOCAL app ${appRegistration.universalIdentifier} (files synced by CLI watcher in dev mode)`,
);
@@ -88,18 +101,29 @@ export class ApplicationInstallService {
let resolvedPackage: ResolvedPackage | null = null;
try {
resolvedPackage = await this.appPackageFetcherService.resolvePackage(
appRegistration,
{ targetVersion: params.version },
);
resolvedPackage =
await this.applicationPackageFetcherService.resolvePackage(
appRegistration,
{ targetVersion: params.version },
);
if (!resolvedPackage) {
return true;
}
const universalIdentifier = appRegistration.universalIdentifier;
await this.ensureApplicationExists({
universalIdentifier,
name: resolvedPackage.manifest.application.displayName,
workspaceId: params.workspaceId,
applicationRegistrationId: appRegistration.id,
sourceType: appRegistration.sourceType,
});
await this.writeFilesToStorage(
resolvedPackage.extractedDir,
appRegistration.universalIdentifier,
universalIdentifier,
params.workspaceId,
);
@@ -109,14 +133,8 @@ export class ApplicationInstallService {
applicationRegistrationId: appRegistration.id,
});
await this.updateApplicationSourceType(
appRegistration.universalIdentifier,
params.workspaceId,
appRegistration.sourceType,
);
this.logger.log(
`Successfully installed app ${appRegistration.universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
`Successfully installed app ${universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
);
return true;
@@ -128,7 +146,7 @@ export class ApplicationInstallService {
throw error;
} finally {
if (resolvedPackage) {
await this.appPackageFetcherService.cleanupExtractedDir(
await this.applicationPackageFetcherService.cleanupExtractedDir(
resolvedPackage.cleanupDir,
);
}
@@ -202,14 +220,29 @@ export class ApplicationInstallService {
return result;
}
private async updateApplicationSourceType(
universalIdentifier: string,
workspaceId: string,
sourceType: AppRegistrationSourceType,
): Promise<void> {
await this.applicationRepository.update(
{ universalIdentifier, workspaceId },
{ sourceType },
);
private async ensureApplicationExists(params: {
universalIdentifier: string;
name: string;
workspaceId: string;
applicationRegistrationId: string;
sourceType: ApplicationRegistrationSourceType;
}): Promise<ApplicationEntity> {
const existing = await this.applicationService.findByUniversalIdentifier({
universalIdentifier: params.universalIdentifier,
workspaceId: params.workspaceId,
});
if (isDefined(existing)) {
return existing;
}
return this.applicationService.create({
universalIdentifier: params.universalIdentifier,
name: params.name,
sourcePath: params.universalIdentifier,
sourceType: params.sourceType,
applicationRegistrationId: params.applicationRegistrationId,
workspaceId: params.workspaceId,
});
}
}

View File

@@ -5,14 +5,14 @@ import axios from 'axios';
import { Repository } from 'typeorm';
import { z } from 'zod';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { AppRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
const npmPackageMetadataSchema = z.object({
@@ -20,8 +20,8 @@ const npmPackageMetadataSchema = z.object({
});
@Injectable()
export class AppUpgradeService {
private readonly logger = new Logger(AppUpgradeService.name);
export class ApplicationUpgradeService {
private readonly logger = new Logger(ApplicationUpgradeService.name);
constructor(
@InjectRepository(ApplicationRegistrationEntity)
@@ -35,7 +35,7 @@ export class AppUpgradeService {
async checkForUpdates(
appRegistration: ApplicationRegistrationEntity,
): Promise<string | null> {
if (appRegistration.sourceType !== AppRegistrationSourceType.NPM) {
if (appRegistration.sourceType !== ApplicationRegistrationSourceType.NPM) {
return null;
}
@@ -82,7 +82,7 @@ export class AppUpgradeService {
async checkAllForUpdates(): Promise<void> {
const npmRegistrations = await this.appRegistrationRepository.find({
where: { sourceType: AppRegistrationSourceType.NPM },
where: { sourceType: ApplicationRegistrationSourceType.NPM },
});
for (const registration of npmRegistrations) {
@@ -100,8 +100,8 @@ export class AppUpgradeService {
});
if (
appRegistration.sourceType === AppRegistrationSourceType.LOCAL ||
appRegistration.sourceType === AppRegistrationSourceType.TARBALL
appRegistration.sourceType === ApplicationRegistrationSourceType.LOCAL ||
appRegistration.sourceType === ApplicationRegistrationSourceType.TARBALL
) {
throw new ApplicationException(
'Cannot upgrade an app installed from a tarball or local source',

View File

@@ -0,0 +1,37 @@
import { Injectable, Logger } from '@nestjs/common';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { APPLICATION_VERSION_CHECK_CRON_PATTERN } from 'src/engine/core-modules/application/application-upgrade/crons/constants/application-version-check-cron-pattern.constant';
import { ApplicationUpgradeService } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
@Injectable()
@Processor(MessageQueue.cronQueue)
export class ApplicationVersionCheckCronJob {
private readonly logger = new Logger(ApplicationVersionCheckCronJob.name);
constructor(
private readonly applicationUpgradeService: ApplicationUpgradeService,
) {}
@Process(ApplicationVersionCheckCronJob.name)
@SentryCronMonitor(
ApplicationVersionCheckCronJob.name,
APPLICATION_VERSION_CHECK_CRON_PATTERN,
)
async handle(): Promise<void> {
this.logger.log('Starting application version check...');
try {
await this.applicationUpgradeService.checkAllForUpdates();
this.logger.log('Application version check completed successfully');
} catch (error) {
this.logger.error(
`Application version check failed: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
}
}
}

View File

@@ -1,7 +1,7 @@
import { Command, CommandRunner } from 'nest-commander';
import { APP_VERSION_CHECK_CRON_PATTERN } from 'src/engine/core-modules/application/application-version-check/crons/constants/app-version-check-cron-pattern.constant';
import { AppVersionCheckCronJob } from 'src/engine/core-modules/application/application-version-check/crons/app-version-check.cron.job';
import { APPLICATION_VERSION_CHECK_CRON_PATTERN } from 'src/engine/core-modules/application/application-upgrade/crons/constants/application-version-check-cron-pattern.constant';
import { ApplicationVersionCheckCronJob } from 'src/engine/core-modules/application/application-upgrade/crons/application-version-check.cron.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@@ -11,7 +11,7 @@ import { MessageQueueService } from 'src/engine/core-modules/message-queue/servi
description:
'Starts a cron job to check for app version updates on npm registries',
})
export class AppVersionCheckCronCommand extends CommandRunner {
export class ApplicationVersionCheckCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
@@ -21,11 +21,11 @@ export class AppVersionCheckCronCommand extends CommandRunner {
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: AppVersionCheckCronJob.name,
jobName: ApplicationVersionCheckCronJob.name,
data: undefined,
options: {
repeat: {
pattern: APP_VERSION_CHECK_CRON_PATTERN,
pattern: APPLICATION_VERSION_CHECK_CRON_PATTERN,
},
},
});

View File

@@ -0,0 +1,2 @@
// Every 6 hours
export const APPLICATION_VERSION_CHECK_CRON_PATTERN = '0 */6 * * *';

View File

@@ -10,9 +10,9 @@ import {
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util';
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';

View File

@@ -0,0 +1,39 @@
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 { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/application/application-variable/application-variable.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';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
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({
imports: [
ApplicationModule,
ApplicationVariableEntityModule,
FeatureFlagModule,
FileStorageModule,
ObjectPermissionModule,
PermissionFlagModule,
PermissionsModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
],
providers: [
ApplicationManifestMigrationService,
ApplicationManifestResolver,
ApplicationSyncService,
WorkspaceMigrationGraphqlApiExceptionInterceptor,
],
exports: [ApplicationManifestMigrationService, ApplicationSyncService],
})
export class ApplicationManifestModule {}

View File

@@ -0,0 +1,105 @@
import {
UseFilters,
UseGuards,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { FeatureFlagKey } from 'twenty-shared/types';
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 {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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 { 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 {
FeatureFlagGuard,
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
@UseFilters(ApplicationExceptionFilter, AuthGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
export class ApplicationManifestResolver {
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async runWorkspaceMigration(
@Args() { workspaceMigration: { actions } }: RunWorkspaceMigrationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute(
workspaceId,
['featureFlagsMap'],
);
if (
featureFlagsMap[
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED
] !== true
) {
throw new ApplicationException(
'Application installation from tarball is not enabled',
ApplicationExceptionCode.FORBIDDEN,
);
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
await this.workspaceMigrationRunnerService.run({
workspaceMigration: {
actions,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
workspaceId,
});
return true;
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async uninstallApplication(
@Args() { universalIdentifier }: UninstallApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.uninstallApplication({
applicationUniversalIdentifier: universalIdentifier,
workspaceId,
});
return true;
}
}

View File

@@ -11,12 +11,11 @@ import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-install/application-manifest-migration.service';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/build-from-to-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-application-sub-all-flat-entity-maps.util';
import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application/utils/get-default-application-package-fields.util';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
@@ -93,28 +92,18 @@ export class ApplicationSyncService {
).toString('utf-8'),
) as PackageJson;
const defaultPackageFields = await getDefaultApplicationPackageFields();
let application = await this.applicationService.findByUniversalIdentifier({
universalIdentifier: manifest.application.universalIdentifier,
workspaceId,
});
const application = await this.applicationService.findByUniversalIdentifier(
{
universalIdentifier: manifest.application.universalIdentifier,
workspaceId,
},
);
if (!application) {
application = await this.applicationService.create({
universalIdentifier: manifest.application.universalIdentifier,
name,
description: manifest.application.description,
version: packageJson.version,
sourcePath: 'cli-sync',
defaultRoleId: null,
workspaceId,
packageJsonChecksum: defaultPackageFields.packageJsonChecksum,
packageJsonFileId: null,
yarnLockChecksum: defaultPackageFields.yarnLockChecksum,
yarnLockFileId: null,
availablePackages: defaultPackageFields.availablePackages,
});
throw new ApplicationException(
`Application "${manifest.application.universalIdentifier}" is not installed in workspace "${workspaceId}". Install it first.`,
ApplicationExceptionCode.APP_NOT_INSTALLED,
);
}
await this.applicationVariableService.upsertManyApplicationVariableEntities(

View File

@@ -1,4 +1,4 @@
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/utils/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -1,5 +1,5 @@
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/utils/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
describe('fromPageLayoutManifestToUniversalFlatPageLayout', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -1,6 +1,6 @@
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/utils/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
describe('fromPageLayoutTabManifestToUniversalFlatPageLayoutTab', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -1,5 +1,5 @@
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/utils/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
describe('fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -1,6 +1,6 @@
import { AggregateOperations } from 'twenty-shared/types';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/utils/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util';
describe('fromViewFieldManifestToUniversalFlatViewField', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -1,6 +1,6 @@
import { ViewFilterOperand } from 'twenty-shared/types';
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/utils/from-view-filter-manifest-to-universal-flat-view-filter.util';
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-manifest-to-universal-flat-view-filter.util';
describe('fromViewFilterManifestToUniversalFlatViewFilter', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -4,7 +4,7 @@ import {
ViewVisibility,
} from 'twenty-shared/types';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
describe('fromViewManifestToUniversalFlatView', () => {
const now = '2026-01-01T00:00:00.000Z';

View File

@@ -46,7 +46,7 @@ export class WorkspaceMigrationInput {
}
@ArgsType()
export class InstallApplicationInput {
export class RunWorkspaceMigrationInput {
@Field(() => WorkspaceMigrationInput)
@ValidateNested()
@Type(() => WorkspaceMigrationInput)

View File

@@ -1,23 +1,23 @@
import { type Manifest } from 'twenty-shared/application';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/utils/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util';
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/utils/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/utils/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/utils/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/utils/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/utils/from-skill-manifest-to-universal-flat-skill.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/utils/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/utils/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/utils/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util';
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/utils/from-view-filter-manifest-to-universal-flat-view-filter.util';
import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/utils/from-view-group-manifest-to-universal-flat-view-group.util';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util';
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/application-manifest/converters/from-logic-function-manifest-to-universal-flat-logic-function.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util';
import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-group-manifest-to-universal-flat-view-filter-group.util';
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-manifest-to-universal-flat-view-filter.util';
import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-group-manifest-to-universal-flat-view-group.util';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';

View File

@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { MARKETPLACE_CATALOG_SYNC_CRON_PATTERN } from 'src/engine/core-modules/application/application-marketplace/crons/constants/marketplace-catalog-sync-cron-pattern.constant';
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/application/application-marketplace/services/marketplace-catalog-sync.service';
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';

View File

@@ -2,6 +2,7 @@ import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
IsArray,
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
@@ -285,4 +286,8 @@ export class MarketplaceAppDTO {
@IsString()
@Field({ nullable: true })
sourcePackage?: string;
@IsBoolean()
@Field(() => Boolean)
isFeatured: boolean;
}

View File

@@ -0,0 +1,88 @@
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 { MARKETPLACE_CATALOG_INDEX } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-catalog-index.constant';
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service';
@Injectable()
export class MarketplaceCatalogSyncService {
private readonly logger = new Logger(MarketplaceCatalogSyncService.name);
constructor(
private readonly applicationRegistrationService: ApplicationRegistrationService,
private readonly marketplaceService: MarketplaceService,
) {}
async syncCatalog(): Promise<void> {
await this.syncCuratedApps();
await this.syncNpmApps();
this.logger.log('Marketplace catalog sync completed');
}
private async syncCuratedApps(): Promise<void> {
for (const entry of MARKETPLACE_CATALOG_INDEX) {
try {
await this.applicationRegistrationService.upsertFromCatalog({
universalIdentifier: entry.universalIdentifier,
name: entry.name,
description:
entry.richDisplayData.aboutDescription ?? entry.description,
author: entry.author,
sourceType: ApplicationRegistrationSourceType.NPM,
sourcePackage: entry.sourcePackage,
logoUrl: entry.logoUrl ?? null,
websiteUrl: entry.websiteUrl ?? null,
termsUrl: entry.termsUrl ?? null,
latestAvailableVersion: entry.richDisplayData.version ?? null,
isListed: true,
isFeatured: entry.isFeatured,
marketplaceDisplayData: entry.richDisplayData,
ownerWorkspaceId: null,
});
} catch (error) {
this.logger.error(
`Failed to sync curated app "${entry.name}": ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
private async syncNpmApps(): Promise<void> {
const npmApps = await this.marketplaceService.fetchAppsFromNpmRegistry();
const curatedIdentifiers = new Set(
MARKETPLACE_CATALOG_INDEX.map((entry) => entry.universalIdentifier),
);
for (const app of npmApps) {
if (curatedIdentifiers.has(app.id)) {
continue;
}
try {
await this.applicationRegistrationService.upsertFromCatalog({
universalIdentifier: app.id,
name: app.name,
description: app.description,
author: app.author,
sourceType: ApplicationRegistrationSourceType.NPM,
sourcePackage: app.sourcePackage ?? app.name,
logoUrl: null,
websiteUrl: app.websiteUrl ?? null,
termsUrl: null,
latestAvailableVersion: app.version ?? null,
isListed: true,
isFeatured: false,
marketplaceDisplayData: null,
ownerWorkspaceId: null,
});
} catch (error) {
this.logger.error(
`Failed to sync npm app "${app.name}": ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
}

View File

@@ -1,17 +1,13 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import {
ApplicationRegistrationException,
ApplicationRegistrationExceptionCode,
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
import { assertValidNpmPackageName } from 'src/engine/core-modules/application/utils/assert-valid-npm-package-name.util';
import { AppRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@@ -28,8 +24,7 @@ export class MarketplaceQueryService {
private hasSyncBeenEnqueued = false;
constructor(
@InjectRepository(ApplicationRegistrationEntity)
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
private readonly applicationRegistrationService: ApplicationRegistrationService,
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@@ -39,9 +34,8 @@ export class MarketplaceQueryService {
return this.cachedApps;
}
const registrations = await this.appRegistrationRepository.find({
where: { sourceType: AppRegistrationSourceType.NPM },
});
const registrations =
await this.applicationRegistrationService.findManyListed();
if (registrations.length === 0) {
if (!this.hasSyncBeenEnqueued) {
@@ -66,12 +60,22 @@ export class MarketplaceQueryService {
return this.cachedApps;
}
async findOneMarketplaceApp(
universalIdentifier: string,
): Promise<MarketplaceAppDTO> {
const registration =
await this.findRegistrationByUniversalIdentifier(universalIdentifier);
return this.toMarketplaceAppDTO(registration);
}
async findRegistrationByUniversalIdentifier(
universalIdentifier: string,
): Promise<ApplicationRegistrationEntity> {
const registration = await this.appRegistrationRepository.findOne({
where: { universalIdentifier },
});
const registration =
await this.applicationRegistrationService.findOneByUniversalIdentifier(
universalIdentifier,
);
if (!isDefined(registration)) {
throw new ApplicationRegistrationException(
@@ -83,53 +87,6 @@ export class MarketplaceQueryService {
return registration;
}
async findOrCreateNpmRegistration(params: {
packageName: string;
ownerWorkspaceId: string;
}): Promise<ApplicationRegistrationEntity> {
assertValidNpmPackageName(params.packageName);
const existing = await this.appRegistrationRepository.findOne({
where: { sourcePackage: params.packageName },
});
if (isDefined(existing)) {
return existing;
}
this.logger.log(
`Creating new registration for npm package "${params.packageName}"`,
);
try {
const registration = this.appRegistrationRepository.create({
universalIdentifier: v4(),
name: params.packageName,
sourceType: AppRegistrationSourceType.NPM,
sourcePackage: params.packageName,
oAuthClientId: v4(),
oAuthRedirectUris: [],
oAuthScopes: [],
ownerWorkspaceId: params.ownerWorkspaceId,
});
return await this.appRegistrationRepository.save(registration);
} catch {
const concurrentlyCreated = await this.appRegistrationRepository.findOne({
where: { sourcePackage: params.packageName },
});
if (isDefined(concurrentlyCreated)) {
return concurrentlyCreated;
}
throw new ApplicationRegistrationException(
`Failed to create registration for package "${params.packageName}"`,
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_NOT_FOUND,
);
}
}
toMarketplaceAppDTO(
registration: ApplicationRegistrationEntity,
): MarketplaceAppDTO {
@@ -157,6 +114,7 @@ export class MarketplaceQueryService {
frontComponents: displayData?.frontComponents ?? [],
sourcePackage: registration.sourcePackage ?? undefined,
defaultRole: displayData?.defaultRole,
isFeatured: registration.isFeatured,
};
}
}

View File

@@ -1,21 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
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 { 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/services/marketplace-catalog-sync.service';
import { MarketplaceQueryService } from 'src/engine/core-modules/application/application-marketplace/services/marketplace-query.service';
import { MarketplaceResolver } from 'src/engine/core-modules/application/application-marketplace/resolvers/marketplace.resolver';
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/services/marketplace.service';
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service';
import { MarketplaceQueryService } from 'src/engine/core-modules/application/application-marketplace/marketplace-query.service';
import { MarketplaceResolver } from 'src/engine/core-modules/application/application-marketplace/marketplace.resolver';
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
imports: [
TypeOrmModule.forFeature([ApplicationRegistrationEntity]),
ApplicationRegistrationModule,
ApplicationInstallModule,
FeatureFlagModule,
PermissionsModule,

View File

@@ -6,15 +6,9 @@ import { FeatureFlagKey } from 'twenty-shared/types';
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 {
ApplicationRegistrationException,
ApplicationRegistrationExceptionCode,
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
import { AppRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { AppUpgradeService } from 'src/engine/core-modules/application/application-install/app-upgrade.service';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
import { MarketplaceQueryService } from 'src/engine/core-modules/application/application-marketplace/services/marketplace-query.service';
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 {
@@ -38,7 +32,6 @@ export class MarketplaceResolver {
constructor(
private readonly marketplaceQueryService: MarketplaceQueryService,
private readonly applicationInstallService: ApplicationInstallService,
private readonly appUpgradeService: AppUpgradeService,
) {}
@Query(() => [MarketplaceAppDTO])
@@ -47,6 +40,16 @@ export class MarketplaceResolver {
return this.marketplaceQueryService.findManyMarketplaceApps();
}
@Query(() => MarketplaceAppDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async findOneMarketplaceApp(
@Args('universalIdentifier') universalIdentifier: string,
): Promise<MarketplaceAppDTO> {
return this.marketplaceQueryService.findOneMarketplaceApp(
universalIdentifier,
);
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
@@ -61,54 +64,10 @@ export class MarketplaceResolver {
universalIdentifier,
);
if (registration.sourceType !== AppRegistrationSourceType.NPM) {
throw new ApplicationRegistrationException(
`Only NPM apps can be installed via the marketplace`,
ApplicationRegistrationExceptionCode.SOURCE_CHANNEL_MISMATCH,
);
}
return this.applicationInstallService.installApplication({
appRegistrationId: registration.id,
version,
workspaceId: workspace.id,
});
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async installNpmApp(
@Args('packageName') packageName: string,
@Args('version', { type: () => String, nullable: true })
version: string | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
const registration =
await this.marketplaceQueryService.findOrCreateNpmRegistration({
packageName,
ownerWorkspaceId: workspace.id,
});
return this.applicationInstallService.installApplication({
appRegistrationId: registration.id,
version,
workspaceId: workspace.id,
});
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async upgradeApplication(
@Args('appRegistrationId') appRegistrationId: string,
@Args('targetVersion') targetVersion: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
return this.appUpgradeService.upgradeApplication({
appRegistrationId,
targetVersion,
workspaceId: workspace.id,
});
}
}

View File

@@ -80,6 +80,7 @@ export class MarketplaceService {
logicFunctions: [],
frontComponents: [],
sourcePackage: name,
isFeatured: false,
};
})
.filter(isDefined);

View File

@@ -1,171 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { AppRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum';
import { MARKETPLACE_CATALOG_INDEX } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-catalog-index.constant';
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/services/marketplace.service';
import { getAdminWorkspaceId } from 'src/engine/core-modules/application/application-marketplace/utils/get-admin-workspace-id.util';
@Injectable()
export class MarketplaceCatalogSyncService {
private readonly logger = new Logger(MarketplaceCatalogSyncService.name);
constructor(
@InjectRepository(ApplicationRegistrationEntity)
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
private readonly marketplaceService: MarketplaceService,
) {}
async syncCatalog(): Promise<void> {
const dataSource = this.appRegistrationRepository.manager.connection;
const adminWorkspaceId = await getAdminWorkspaceId(dataSource);
if (!isDefined(adminWorkspaceId)) {
this.logger.warn(
'No admin workspace found. Skipping marketplace catalog sync.',
);
return;
}
await this.syncCuratedApps(adminWorkspaceId);
await this.syncNpmApps(adminWorkspaceId);
this.logger.log('Marketplace catalog sync completed');
}
private async syncCuratedApps(ownerWorkspaceId: string): Promise<void> {
for (const entry of MARKETPLACE_CATALOG_INDEX) {
try {
await this.upsertRegistration({
universalIdentifier: entry.universalIdentifier,
name: entry.name,
description:
entry.richDisplayData.aboutDescription ?? entry.description,
author: entry.author,
sourceType: AppRegistrationSourceType.NPM,
sourcePackage: entry.sourcePackage,
logoUrl: entry.logoUrl ?? null,
websiteUrl: entry.websiteUrl ?? null,
termsUrl: entry.termsUrl ?? null,
latestAvailableVersion: entry.richDisplayData.version ?? null,
isFeatured: entry.isFeatured,
marketplaceDisplayData: entry.richDisplayData,
ownerWorkspaceId,
});
} catch (error) {
this.logger.error(
`Failed to sync curated app "${entry.name}": ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
private async syncNpmApps(ownerWorkspaceId: string): Promise<void> {
const npmApps = await this.marketplaceService.fetchAppsFromNpmRegistry();
const curatedIdentifiers = new Set(
MARKETPLACE_CATALOG_INDEX.map((entry) => entry.universalIdentifier),
);
for (const app of npmApps) {
if (curatedIdentifiers.has(app.id)) {
continue;
}
try {
await this.upsertRegistration({
universalIdentifier: app.id,
name: app.name,
description: app.description,
author: app.author,
sourceType: AppRegistrationSourceType.NPM,
sourcePackage: app.sourcePackage ?? app.name,
logoUrl: null,
websiteUrl: app.websiteUrl ?? null,
termsUrl: null,
latestAvailableVersion: app.version ?? null,
isFeatured: false,
marketplaceDisplayData: null,
ownerWorkspaceId,
});
} catch (error) {
this.logger.error(
`Failed to sync npm app "${app.name}": ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
// Lookup by universalIdentifier only (matches the unique constraint).
// ownerWorkspaceId is only set on insert.
private async upsertRegistration(
params: Pick<
ApplicationRegistrationEntity,
| 'universalIdentifier'
| 'name'
| 'description'
| 'author'
| 'sourceType'
| 'sourcePackage'
| 'logoUrl'
| 'websiteUrl'
| 'termsUrl'
| 'latestAvailableVersion'
| 'isFeatured'
| 'marketplaceDisplayData'
| 'ownerWorkspaceId'
>,
): Promise<void> {
const existing = await this.appRegistrationRepository.findOne({
where: {
universalIdentifier: params.universalIdentifier,
},
});
if (isDefined(existing)) {
await this.appRegistrationRepository.save({
...existing,
name: params.name,
description: params.description,
author: params.author,
sourceType: params.sourceType,
sourcePackage: params.sourcePackage,
logoUrl: params.logoUrl,
websiteUrl: params.websiteUrl,
termsUrl: params.termsUrl,
latestAvailableVersion: params.latestAvailableVersion,
isFeatured: params.isFeatured,
marketplaceDisplayData: params.marketplaceDisplayData,
});
return;
}
const registration = this.appRegistrationRepository.create({
universalIdentifier: params.universalIdentifier,
name: params.name,
description: params.description,
author: params.author,
sourceType: params.sourceType,
sourcePackage: params.sourcePackage,
logoUrl: params.logoUrl,
websiteUrl: params.websiteUrl,
termsUrl: params.termsUrl,
latestAvailableVersion: params.latestAvailableVersion,
isFeatured: params.isFeatured,
marketplaceDisplayData: params.marketplaceDisplayData,
oAuthClientId: v4(),
oAuthRedirectUris: [],
oAuthScopes: [],
ownerWorkspaceId: params.ownerWorkspaceId,
});
await this.appRegistrationRepository.save(registration);
}
}

View File

@@ -1,36 +0,0 @@
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { type DataSource } from 'typeorm';
// Every ApplicationRegistration must be owned by a workspace (ownerWorkspaceId
// represents ownership / write-access, not visibility scoping — marketplace
// registrations are readable by all workspaces). When the catalog sync creates
// registrations for marketplace apps that no developer has explicitly claimed,
// we assign them to the "admin" workspace: the oldest active workspace whose
// owner has admin privileges.
//
// TODO: This heuristic is fragile — on fresh instances with no admin users the
// catalog sync is silently skipped, and if the admin workspace is later deleted
// all marketplace registrations become orphaned. Consider introducing a
// dedicated "platform" workspace or making ownerWorkspaceId nullable instead.
export const getAdminWorkspaceId = async (
dataSource: DataSource,
): Promise<string | null> => {
const result = await dataSource.query<Array<{ workspaceId: string }>>(
`SELECT uw."workspaceId"
FROM core."userWorkspace" uw
JOIN core."user" u ON u.id = uw."userId" AND u."deletedAt" IS NULL
JOIN core."workspace" w ON w.id = uw."workspaceId" AND w."deletedAt" IS NULL
WHERE (u."canAccessFullAdminPanel" = true OR u."canImpersonate" = true)
AND w."activationStatus" = $1
AND uw."deletedAt" IS NULL
ORDER BY w."createdAt" ASC
LIMIT 1`,
[WorkspaceActivationStatus.ACTIVE],
);
if (result.length === 0) {
return null;
}
return result[0].workspaceId;
};

View File

@@ -2,7 +2,7 @@ import {
ALL_OAUTH_SCOPES,
OAUTH_SCOPE_DESCRIPTIONS,
OAUTH_SCOPES,
} from 'src/engine/core-modules/application/application-registration/constants/oauth-scopes';
} from 'src/engine/core-modules/application/application-oauth/constants/oauth-scopes';
describe('OAuth Scopes', () => {
it('should have all scopes defined', () => {

View File

@@ -0,0 +1,42 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule as ApplicationCoreModule } from 'src/engine/core-modules/application/application.module';
import { ApplicationOAuthResolver } from 'src/engine/core-modules/application/application-oauth/application-oauth.resolver';
import { OAuthDiscoveryController } from 'src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller';
import { OAuthTokenController } from 'src/engine/core-modules/application/application-oauth/controllers/oauth-token.controller';
import { OAuthService } from 'src/engine/core-modules/application/application-oauth/oauth.service';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [
TypeOrmModule.forFeature([
AppTokenEntity,
ApplicationEntity,
UserWorkspaceEntity,
]),
ApplicationRegistrationModule,
ApplicationCoreModule,
ApplicationInstallModule,
TokenModule,
FeatureFlagModule,
PermissionsModule,
ThrottlerModule,
TwentyConfigModule,
WorkspaceCacheStorageModule,
],
controllers: [OAuthTokenController, OAuthDiscoveryController],
providers: [OAuthService, ApplicationOAuthResolver],
exports: [OAuthService],
})
export class ApplicationOAuthModule {}

View File

@@ -0,0 +1,56 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql';
import { FeatureFlagKey } from 'twenty-shared/types';
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 { 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 { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
FeatureFlagGuard,
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@UseFilters(AuthGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
export class ApplicationOAuthResolver {
constructor(
private readonly applicationTokenService: ApplicationTokenService,
) {}
@Mutation(() => ApplicationTokenPairDTO)
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async renewApplicationToken(
@Args('applicationRefreshToken') applicationRefreshToken: string,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ApplicationTokenPairDTO> {
const applicationRefreshTokenPayload =
this.applicationTokenService.validateApplicationRefreshToken(
applicationRefreshToken,
);
if (applicationRefreshTokenPayload.workspaceId !== workspaceId) {
throw new ApplicationException(
'Refresh token workspace does not match authenticated workspace',
ApplicationExceptionCode.FORBIDDEN,
);
}
return this.applicationTokenService.renewApplicationTokens(
applicationRefreshTokenPayload,
);
}
}

Some files were not shown because too many files have changed in this diff Show More