Refacto
This commit is contained in:
@@ -298,6 +298,7 @@ export type AppToken = {
|
||||
export type Application = {
|
||||
__typename?: 'Application';
|
||||
agents: Array<Agent>;
|
||||
applicationRegistration?: Maybe<ApplicationRegistrationSummary>;
|
||||
applicationRegistrationId?: Maybe<Scalars['UUID']>;
|
||||
applicationVariables: Array<ApplicationVariable>;
|
||||
availablePackages: Scalars['JSON'];
|
||||
@@ -339,6 +340,12 @@ export type ApplicationRegistration = {
|
||||
websiteUrl?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ApplicationRegistrationSummary = {
|
||||
__typename?: 'ApplicationRegistrationSummary';
|
||||
id: Scalars['UUID'];
|
||||
latestAvailableVersion?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ApplicationRegistrationStats = {
|
||||
__typename?: 'ApplicationRegistrationStats';
|
||||
activeInstalls: Scalars['Int'];
|
||||
@@ -5750,19 +5757,19 @@ 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, 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, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null } | null, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, 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 }> };
|
||||
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 } | 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, 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, applicationRegistration?: { __typename?: 'ApplicationRegistrationSummary', id: string, latestAvailableVersion?: string | null } | null, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, 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 UploadFileMutationVariables = Exact<{
|
||||
file: Scalars['Upload'];
|
||||
@@ -7485,6 +7492,10 @@ export const ApplicationFieldsFragmentDoc = gql`
|
||||
version
|
||||
universalIdentifier
|
||||
applicationRegistrationId
|
||||
applicationRegistration {
|
||||
id
|
||||
latestAvailableVersion
|
||||
}
|
||||
canBeUninstalled
|
||||
defaultRoleId
|
||||
settingsCustomTabFrontComponentId
|
||||
@@ -9299,6 +9310,10 @@ export const FindManyApplicationsDocument = gql`
|
||||
description
|
||||
version
|
||||
applicationRegistrationId
|
||||
applicationRegistration {
|
||||
id
|
||||
latestAvailableVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+4
@@ -14,6 +14,10 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
version
|
||||
universalIdentifier
|
||||
applicationRegistrationId
|
||||
applicationRegistration {
|
||||
id
|
||||
latestAvailableVersion
|
||||
}
|
||||
canBeUninstalled
|
||||
defaultRoleId
|
||||
settingsCustomTabFrontComponentId
|
||||
|
||||
@@ -8,6 +8,10 @@ export const FIND_MANY_APPLICATIONS = gql`
|
||||
description
|
||||
version
|
||||
applicationRegistrationId
|
||||
applicationRegistration {
|
||||
id
|
||||
latestAvailableVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useUpgradeApplicationMutation } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpgradeApplication = () => {
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const [upgradeApplicationMutation] = useUpgradeApplicationMutation();
|
||||
const [isUpgrading, setIsUpgrading] = useState(false);
|
||||
|
||||
const upgrade = async (params: {
|
||||
appRegistrationId: string;
|
||||
targetVersion: string;
|
||||
}) => {
|
||||
setIsUpgrading(true);
|
||||
|
||||
try {
|
||||
const result = await upgradeApplicationMutation({
|
||||
variables: params,
|
||||
});
|
||||
|
||||
if (isDefined(result.data)) {
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Application upgraded successfully.`,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to upgrade the application.`,
|
||||
});
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
setIsUpgrading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { upgrade, isUpgrading };
|
||||
};
|
||||
+7
-29
@@ -1,15 +1,11 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsAdminVersionDisplay } from '@/settings/admin-panel/components/SettingsAdminVersionDisplay';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconCircleDot, IconStatusChange, IconUpload } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
type Application,
|
||||
useUpgradeApplicationMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type Application } from '~/generated-metadata/graphql';
|
||||
import { isNewerSemver } from '~/pages/settings/applications/utils/isNewerSemver';
|
||||
|
||||
export const SettingsApplicationVersionContainer = ({
|
||||
@@ -32,35 +28,17 @@ export const SettingsApplicationVersionContainer = ({
|
||||
isDefined(currentVersion) &&
|
||||
isNewerSemver(latestAvailableVersion, currentVersion);
|
||||
|
||||
const [upgradeApplication] = useUpgradeApplicationMutation();
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const [isUpgrading, setIsUpgrading] = useState(false);
|
||||
const { upgrade, isUpgrading } = useUpgradeApplication();
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
if (!isDefined(appRegistrationId) || !isDefined(latestAvailableVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpgrading(true);
|
||||
|
||||
try {
|
||||
await upgradeApplication({
|
||||
variables: {
|
||||
appRegistrationId,
|
||||
targetVersion: latestAvailableVersion,
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Application upgraded successfully.`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to upgrade the application.`,
|
||||
});
|
||||
} finally {
|
||||
setIsUpgrading(false);
|
||||
}
|
||||
await upgrade({
|
||||
appRegistrationId,
|
||||
targetVersion: latestAvailableVersion,
|
||||
});
|
||||
};
|
||||
|
||||
const versionItems = [
|
||||
|
||||
+2
-5
@@ -32,10 +32,8 @@ const StyledSearchInput = styled(SettingsTextInput)`
|
||||
|
||||
export const SettingsApplicationsTable = ({
|
||||
applications,
|
||||
registrationVersionMap,
|
||||
}: {
|
||||
applications: ApplicationWithoutRelation[];
|
||||
registrationVersionMap?: Map<string, string>;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -74,9 +72,8 @@ export const SettingsApplicationsTable = ({
|
||||
<TableHeader />
|
||||
</StyledTableHeaderRow>
|
||||
{filteredApplications.map((application) => {
|
||||
const latestVersion = application.applicationRegistrationId
|
||||
? registrationVersionMap?.get(application.applicationRegistrationId)
|
||||
: undefined;
|
||||
const latestVersion =
|
||||
application.applicationRegistration?.latestAvailableVersion;
|
||||
|
||||
const hasUpdate =
|
||||
isDefined(latestVersion) &&
|
||||
|
||||
+1
-8
@@ -12,7 +12,6 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
type Application,
|
||||
useFindOneApplicationRegistrationQuery,
|
||||
useUninstallApplicationMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@@ -39,14 +38,8 @@ export const SettingsApplicationDetailAboutTab = ({
|
||||
|
||||
const registrationId = application?.applicationRegistrationId;
|
||||
|
||||
const { data: registrationData } = useFindOneApplicationRegistrationQuery({
|
||||
variables: { id: registrationId ?? '' },
|
||||
skip: !isDefined(registrationId),
|
||||
});
|
||||
|
||||
const latestAvailableVersion =
|
||||
registrationData?.findOneApplicationRegistration?.latestAvailableVersion ??
|
||||
null;
|
||||
application?.applicationRegistration?.latestAvailableVersion ?? null;
|
||||
|
||||
if (!isDefined(application)) {
|
||||
return null;
|
||||
|
||||
+2
-26
@@ -1,16 +1,11 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useMemo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconDownload, IconUpload } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
useFindManyApplicationRegistrationsQuery,
|
||||
useFindManyApplicationsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
|
||||
import {
|
||||
SettingsInstallNpmAppModal,
|
||||
@@ -30,26 +25,10 @@ const StyledButtonGroup = styled.div`
|
||||
export const SettingsApplicationsInstalledTab = () => {
|
||||
const { t } = useLingui();
|
||||
const { data } = useFindManyApplicationsQuery();
|
||||
const { data: registrationsData } =
|
||||
useFindManyApplicationRegistrationsQuery();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const applications = data?.findManyApplications ?? [];
|
||||
|
||||
const registrationVersionMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
const registrations =
|
||||
registrationsData?.findManyApplicationRegistrations ?? [];
|
||||
|
||||
for (const registration of registrations) {
|
||||
if (isDefined(registration.latestAvailableVersion)) {
|
||||
map.set(registration.id, registration.latestAvailableVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}, [registrationsData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
@@ -72,10 +51,7 @@ export const SettingsApplicationsInstalledTab = () => {
|
||||
</Section>
|
||||
|
||||
{applications.length > 0 && (
|
||||
<SettingsApplicationsTable
|
||||
applications={applications}
|
||||
registrationVersionMap={registrationVersionMap}
|
||||
/>
|
||||
<SettingsApplicationsTable applications={applications} />
|
||||
)}
|
||||
|
||||
<SettingsInstallNpmAppModal />
|
||||
|
||||
+6
-1
@@ -2,5 +2,10 @@ import { type Application } from '~/generated-metadata/graphql';
|
||||
|
||||
export type ApplicationWithoutRelation = Pick<
|
||||
Application,
|
||||
'id' | 'name' | 'description' | 'version' | 'applicationRegistrationId'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'description'
|
||||
| 'version'
|
||||
| 'applicationRegistrationId'
|
||||
| 'applicationRegistration'
|
||||
>;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application-registration/application-registration.exception';
|
||||
import {
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
@Catch(ApplicationRegistrationException)
|
||||
export class ApplicationRegistrationExceptionFilter
|
||||
implements ExceptionFilter
|
||||
{
|
||||
catch(exception: ApplicationRegistrationException) {
|
||||
switch (exception.code) {
|
||||
case ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_NOT_FOUND:
|
||||
case ApplicationRegistrationExceptionCode.VARIABLE_NOT_FOUND:
|
||||
throw new NotFoundError(exception);
|
||||
case ApplicationRegistrationExceptionCode.INVALID_INPUT:
|
||||
case ApplicationRegistrationExceptionCode.INVALID_SCOPE:
|
||||
case ApplicationRegistrationExceptionCode.INVALID_REDIRECT_URI:
|
||||
case ApplicationRegistrationExceptionCode.SOURCE_CHANNEL_MISMATCH:
|
||||
case ApplicationRegistrationExceptionCode.UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED:
|
||||
throw new UserInputError(exception);
|
||||
default:
|
||||
throw new InternalServerError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -176,7 +176,7 @@ export class ApplicationRegistrationVariableService {
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: { id: registrationId, workspaceId },
|
||||
where: { id: registrationId, ownerWorkspaceId: workspaceId },
|
||||
});
|
||||
|
||||
if (!registration) {
|
||||
|
||||
+2
-2
@@ -96,8 +96,8 @@ export class ApplicationRegistrationEntity {
|
||||
// Represents ownership (who can edit), not visibility scoping.
|
||||
// Marketplace registrations are readable by all workspaces but owned by the
|
||||
// admin workspace when no developer has explicitly claimed them.
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
@Column({ name: 'workspaceId', nullable: false, type: 'uuid' })
|
||||
ownerWorkspaceId: string;
|
||||
|
||||
@ManyToOne(() => WorkspaceEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
|
||||
+1
-6
@@ -6,7 +6,6 @@ import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationResolver } from 'src/engine/core-modules/application-registration/application-registration.resolver';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application-registration/application-registration.service';
|
||||
import { AppRegistrationUploadController } from 'src/engine/core-modules/application-registration/controllers/app-registration-upload.controller';
|
||||
import { OAuthDiscoveryController } from 'src/engine/core-modules/application-registration/controllers/oauth-discovery.controller';
|
||||
import { OAuthTokenController } from 'src/engine/core-modules/application-registration/controllers/oauth-token.controller';
|
||||
import { OAuthService } from 'src/engine/core-modules/application-registration/oauth.service';
|
||||
@@ -39,11 +38,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
FileStorageModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
],
|
||||
controllers: [
|
||||
OAuthTokenController,
|
||||
OAuthDiscoveryController,
|
||||
AppRegistrationUploadController,
|
||||
],
|
||||
controllers: [OAuthTokenController, OAuthDiscoveryController],
|
||||
providers: [
|
||||
ApplicationRegistrationService,
|
||||
ApplicationRegistrationVariableService,
|
||||
|
||||
+8
-6
@@ -6,6 +6,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application-registration/application-registration-exception-filter';
|
||||
import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application-registration/application-registration-variable.entity';
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application-registration/application-registration-variable.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application-registration/application-registration.entity';
|
||||
@@ -23,9 +24,9 @@ import { RotateClientSecretDTO } from 'src/engine/core-modules/application-regis
|
||||
import { UpdateApplicationRegistrationInput } from 'src/engine/core-modules/application-registration/dtos/update-application-registration.input';
|
||||
import { UpdateApplicationRegistrationVariableInput } from 'src/engine/core-modules/application-registration/dtos/update-application-registration-variable.input';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application-registration/application-registration.exception';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -43,6 +44,7 @@ import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@MetadataResolver()
|
||||
@UseFilters(
|
||||
ApplicationRegistrationExceptionFilter,
|
||||
AuthGraphqlApiExceptionFilter,
|
||||
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||
)
|
||||
@@ -241,16 +243,16 @@ export class ApplicationRegistrationResolver {
|
||||
const tarballBuffer = await streamToBuffer(stream);
|
||||
|
||||
if (tarballBuffer.length > MAX_TARBALL_UPLOAD_SIZE_BYTES) {
|
||||
throw new ApplicationException(
|
||||
throw new ApplicationRegistrationException(
|
||||
`Tarball exceeds maximum size of ${MAX_TARBALL_UPLOAD_SIZE_BYTES} bytes`,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
ApplicationRegistrationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return this.appTarballUploadService.uploadTarball({
|
||||
tarballBuffer,
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
ownerWorkspaceId: workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+20
-17
@@ -33,20 +33,20 @@ export class ApplicationRegistrationService {
|
||||
) {}
|
||||
|
||||
async findMany(
|
||||
workspaceId: string,
|
||||
ownerWorkspaceId: string,
|
||||
): Promise<ApplicationRegistrationEntity[]> {
|
||||
return this.applicationRegistrationRepository.find({
|
||||
where: { workspaceId },
|
||||
where: { ownerWorkspaceId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOneById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
ownerWorkspaceId: string,
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: { id, workspaceId },
|
||||
where: { id, ownerWorkspaceId },
|
||||
});
|
||||
|
||||
if (!registration) {
|
||||
@@ -93,10 +93,10 @@ export class ApplicationRegistrationService {
|
||||
async isOwnedByWorkspace(id: string, workspaceId: string): Promise<boolean> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: { id },
|
||||
select: ['id', 'workspaceId'],
|
||||
select: ['id', 'ownerWorkspaceId'],
|
||||
});
|
||||
|
||||
return registration?.workspaceId === workspaceId;
|
||||
return registration?.ownerWorkspaceId === workspaceId;
|
||||
}
|
||||
|
||||
// Global lookup — used by app sync to find existing registrations
|
||||
@@ -110,7 +110,7 @@ export class ApplicationRegistrationService {
|
||||
|
||||
async create(
|
||||
input: CreateApplicationRegistrationInput,
|
||||
workspaceId: string,
|
||||
ownerWorkspaceId: string,
|
||||
createdByUserId: string | null,
|
||||
): Promise<{
|
||||
applicationRegistration: ApplicationRegistrationEntity;
|
||||
@@ -152,7 +152,7 @@ export class ApplicationRegistrationService {
|
||||
oAuthRedirectUris: input.oAuthRedirectUris ?? [],
|
||||
oAuthScopes: input.oAuthScopes ?? [],
|
||||
createdByUserId,
|
||||
workspaceId,
|
||||
ownerWorkspaceId,
|
||||
websiteUrl: input.websiteUrl ?? null,
|
||||
termsUrl: input.termsUrl ?? null,
|
||||
});
|
||||
@@ -166,11 +166,11 @@ export class ApplicationRegistrationService {
|
||||
|
||||
async update(
|
||||
input: UpdateApplicationRegistrationInput,
|
||||
workspaceId: string,
|
||||
ownerWorkspaceId: string,
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
const { id, update } = input;
|
||||
|
||||
await this.findOneById(id, workspaceId);
|
||||
await this.findOneById(id, ownerWorkspaceId);
|
||||
|
||||
if (isDefined(update.oAuthRedirectUris)) {
|
||||
this.validateRedirectUris(update.oAuthRedirectUris);
|
||||
@@ -198,18 +198,21 @@ export class ApplicationRegistrationService {
|
||||
await this.applicationRegistrationRepository.update(id, updateData);
|
||||
}
|
||||
|
||||
return this.findOneById(id, workspaceId);
|
||||
return this.findOneById(id, ownerWorkspaceId);
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<boolean> {
|
||||
await this.findOneById(id, workspaceId);
|
||||
async delete(id: string, ownerWorkspaceId: string): Promise<boolean> {
|
||||
await this.findOneById(id, ownerWorkspaceId);
|
||||
await this.applicationRegistrationRepository.softDelete(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async rotateClientSecret(id: string, workspaceId: string): Promise<string> {
|
||||
await this.findOneById(id, workspaceId);
|
||||
async rotateClientSecret(
|
||||
id: string,
|
||||
ownerWorkspaceId: string,
|
||||
): Promise<string> {
|
||||
await this.findOneById(id, ownerWorkspaceId);
|
||||
|
||||
const { clientSecret, clientSecretHash } =
|
||||
await this.generateClientSecret();
|
||||
@@ -234,9 +237,9 @@ export class ApplicationRegistrationService {
|
||||
|
||||
async getStats(
|
||||
applicationRegistrationId: string,
|
||||
workspaceId: string,
|
||||
ownerWorkspaceId: string,
|
||||
): Promise<ApplicationRegistrationStatsDTO> {
|
||||
await this.findOneById(applicationRegistrationId, workspaceId);
|
||||
await this.findOneById(applicationRegistrationId, ownerWorkspaceId);
|
||||
|
||||
const versionDistribution: { version: string; count: number }[] =
|
||||
await this.applicationRepository
|
||||
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
Post,
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application-registration/application-registration.entity';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationRestApiExceptionFilter } from 'src/engine/core-modules/application/application-rest-api-exception-filter';
|
||||
import {
|
||||
AppTarballUploadService,
|
||||
MAX_TARBALL_UPLOAD_SIZE_BYTES,
|
||||
} from 'src/engine/core-modules/application-registration/services/app-tarball-upload.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
type TarballUploadBody = {
|
||||
tarball: string;
|
||||
universalIdentifier?: string;
|
||||
};
|
||||
|
||||
@Controller('api/app-registrations')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
@UseFilters(ApplicationRestApiExceptionFilter)
|
||||
export class AppRegistrationUploadController {
|
||||
constructor(
|
||||
private readonly appTarballUploadService: AppTarballUploadService,
|
||||
) {}
|
||||
|
||||
@Post('upload-tarball')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
|
||||
async uploadTarball(
|
||||
@Body() body: TarballUploadBody,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
const tarballBase64 = body.tarball;
|
||||
|
||||
if (!isDefined(tarballBase64)) {
|
||||
throw new ApplicationException(
|
||||
'Tarball data is required',
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const tarballBuffer = Buffer.from(tarballBase64, 'base64');
|
||||
|
||||
if (tarballBuffer.length > MAX_TARBALL_UPLOAD_SIZE_BYTES) {
|
||||
throw new ApplicationException(
|
||||
`Tarball exceeds maximum size of ${MAX_TARBALL_UPLOAD_SIZE_BYTES} bytes`,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return this.appTarballUploadService.uploadTarball({
|
||||
tarballBuffer,
|
||||
universalIdentifier: body.universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -35,7 +35,7 @@ export class AppTarballUploadService {
|
||||
async uploadTarball(params: {
|
||||
tarballBuffer: Buffer;
|
||||
universalIdentifier?: string;
|
||||
workspaceId: string;
|
||||
ownerWorkspaceId: string;
|
||||
}): Promise<ApplicationRegistrationEntity> {
|
||||
const tempDir = join(tmpdir(), 'twenty-tarball-upload', v4());
|
||||
|
||||
@@ -82,7 +82,7 @@ export class AppTarballUploadService {
|
||||
let appRegistration = await this.appRegistrationRepository.findOne({
|
||||
where: {
|
||||
universalIdentifier,
|
||||
workspaceId: params.workspaceId,
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ export class AppTarballUploadService {
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
workspaceId: params.workspaceId,
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
appRegistration =
|
||||
|
||||
-2
@@ -30,8 +30,6 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
|
||||
case ApplicationExceptionCode.SOURCE_CHANNEL_MISMATCH:
|
||||
throw new UserInputError(exception);
|
||||
case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED:
|
||||
case ApplicationExceptionCode.INSTALL_LOCK_TIMEOUT:
|
||||
case ApplicationExceptionCode.INSTALL_HOOK_EXECUTION_FAILED:
|
||||
case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED:
|
||||
case ApplicationExceptionCode.UPGRADE_FAILED:
|
||||
throw new InternalServerError(exception);
|
||||
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
import {
|
||||
type ArgumentsHost,
|
||||
Catch,
|
||||
type ExceptionFilter,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Response } from 'express';
|
||||
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application-registration/application-registration.exception';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||
|
||||
@Catch(ApplicationException, ApplicationRegistrationException)
|
||||
export class ApplicationRestApiExceptionFilter implements ExceptionFilter {
|
||||
constructor(
|
||||
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
catch(
|
||||
exception: ApplicationException | ApplicationRegistrationException,
|
||||
host: ArgumentsHost,
|
||||
) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
if (exception instanceof ApplicationRegistrationException) {
|
||||
return this.handleRegistrationException(exception, response);
|
||||
}
|
||||
|
||||
return this.handleApplicationException(exception, response);
|
||||
}
|
||||
|
||||
private handleApplicationException(
|
||||
exception: ApplicationException,
|
||||
response: Response,
|
||||
) {
|
||||
switch (exception.code) {
|
||||
case ApplicationExceptionCode.OBJECT_NOT_FOUND:
|
||||
case ApplicationExceptionCode.FIELD_NOT_FOUND:
|
||||
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
|
||||
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
|
||||
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
|
||||
case ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
404,
|
||||
);
|
||||
case ApplicationExceptionCode.FORBIDDEN:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
403,
|
||||
);
|
||||
case ApplicationExceptionCode.INVALID_INPUT:
|
||||
case ApplicationExceptionCode.SOURCE_CHANNEL_MISMATCH:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
400,
|
||||
);
|
||||
case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED:
|
||||
case ApplicationExceptionCode.INSTALL_LOCK_TIMEOUT:
|
||||
case ApplicationExceptionCode.INSTALL_HOOK_EXECUTION_FAILED:
|
||||
case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED:
|
||||
case ApplicationExceptionCode.UPGRADE_FAILED:
|
||||
default:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleRegistrationException(
|
||||
exception: ApplicationRegistrationException,
|
||||
response: Response,
|
||||
) {
|
||||
switch (exception.code) {
|
||||
case ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_NOT_FOUND:
|
||||
case ApplicationRegistrationExceptionCode.VARIABLE_NOT_FOUND:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
404,
|
||||
);
|
||||
case ApplicationRegistrationExceptionCode.INVALID_INPUT:
|
||||
case ApplicationRegistrationExceptionCode.INVALID_SCOPE:
|
||||
case ApplicationRegistrationExceptionCode.INVALID_REDIRECT_URI:
|
||||
case ApplicationRegistrationExceptionCode.SOURCE_CHANNEL_MISMATCH:
|
||||
case ApplicationRegistrationExceptionCode.UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
400,
|
||||
);
|
||||
default:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ export enum ApplicationExceptionCode {
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
SOURCE_CHANNEL_MISMATCH = 'SOURCE_CHANNEL_MISMATCH',
|
||||
PACKAGE_RESOLUTION_FAILED = 'PACKAGE_RESOLUTION_FAILED',
|
||||
INSTALL_LOCK_TIMEOUT = 'INSTALL_LOCK_TIMEOUT',
|
||||
INSTALL_HOOK_EXECUTION_FAILED = 'INSTALL_HOOK_EXECUTION_FAILED',
|
||||
TARBALL_EXTRACTION_FAILED = 'TARBALL_EXTRACTION_FAILED',
|
||||
UPGRADE_FAILED = 'UPGRADE_FAILED',
|
||||
}
|
||||
@@ -45,10 +43,6 @@ const getApplicationExceptionUserFriendlyMessage = (
|
||||
return msg`Source channel mismatch.`;
|
||||
case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED:
|
||||
return msg`Unable to retrieve the application package.`;
|
||||
case ApplicationExceptionCode.INSTALL_LOCK_TIMEOUT:
|
||||
return msg`Another installation is in progress.`;
|
||||
case ApplicationExceptionCode.INSTALL_HOOK_EXECUTION_FAILED:
|
||||
return msg`Install hook execution failed.`;
|
||||
case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED:
|
||||
return msg`Failed to extract tarball.`;
|
||||
case ApplicationExceptionCode.UPGRADE_FAILED:
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('ApplicationRegistrationSummary')
|
||||
export class ApplicationRegistrationSummaryDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
latestAvailableVersion?: string;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationRegistrationSummaryDTO } from 'src/engine/core-modules/application/dtos/application-registration-summary.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
|
||||
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
|
||||
@@ -98,4 +99,8 @@ export class ApplicationDTO {
|
||||
|
||||
@Field(() => [ApplicationVariableEntityDTO])
|
||||
applicationVariables?: ApplicationVariableEntityDTO[];
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => ApplicationRegistrationSummaryDTO, { nullable: true })
|
||||
applicationRegistration?: ApplicationRegistrationSummaryDTO;
|
||||
}
|
||||
|
||||
+2
@@ -126,6 +126,7 @@ export class ApplicationService {
|
||||
'applicationVariables',
|
||||
'packageJsonFile',
|
||||
'yarnLockFile',
|
||||
'applicationRegistration',
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -160,6 +161,7 @@ export class ApplicationService {
|
||||
'applicationVariables',
|
||||
'packageJsonFile',
|
||||
'yarnLockFile',
|
||||
'applicationRegistration',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApplicationSyncModule } from 'src/engine/core-modules/application/appli
|
||||
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/marketplace/crons/commands/marketplace-catalog-sync.cron.command';
|
||||
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/marketplace/crons/marketplace-catalog-sync.cron.job';
|
||||
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/marketplace/services/marketplace-catalog-sync.service';
|
||||
import { MarketplaceQueryService } from 'src/engine/core-modules/marketplace/services/marketplace-query.service';
|
||||
import { MarketplaceResolver } from 'src/engine/core-modules/marketplace/resolvers/marketplace.resolver';
|
||||
import { MarketplaceService } from 'src/engine/core-modules/marketplace/services/marketplace.service';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
@@ -21,10 +22,15 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
providers: [
|
||||
MarketplaceService,
|
||||
MarketplaceCatalogSyncService,
|
||||
MarketplaceQueryService,
|
||||
MarketplaceCatalogSyncCronJob,
|
||||
MarketplaceCatalogSyncCronCommand,
|
||||
MarketplaceResolver,
|
||||
],
|
||||
exports: [MarketplaceCatalogSyncService, MarketplaceCatalogSyncCronCommand],
|
||||
exports: [
|
||||
MarketplaceCatalogSyncService,
|
||||
MarketplaceQueryService,
|
||||
MarketplaceCatalogSyncCronCommand,
|
||||
],
|
||||
})
|
||||
export class MarketplaceModule {}
|
||||
|
||||
+9
-7
@@ -1,12 +1,13 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application-registration/application-registration-exception-filter';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/services/application-install.service';
|
||||
import { AppUpgradeService } from 'src/engine/core-modules/application/services/app-upgrade.service';
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/marketplace/dtos/marketplace-app.dto';
|
||||
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/marketplace/services/marketplace-catalog-sync.service';
|
||||
import { MarketplaceQueryService } from 'src/engine/core-modules/marketplace/services/marketplace-query.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
@@ -16,17 +17,18 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@MetadataResolver()
|
||||
@UseFilters(ApplicationRegistrationExceptionFilter)
|
||||
@UseGuards(UserAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
|
||||
export class MarketplaceResolver {
|
||||
constructor(
|
||||
private readonly marketplaceCatalogSyncService: MarketplaceCatalogSyncService,
|
||||
private readonly marketplaceQueryService: MarketplaceQueryService,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
private readonly appUpgradeService: AppUpgradeService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MarketplaceAppDTO])
|
||||
async findManyMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
return this.marketplaceCatalogSyncService.findManyMarketplaceApps();
|
||||
return this.marketplaceQueryService.findManyMarketplaceApps();
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@@ -38,7 +40,7 @@ export class MarketplaceResolver {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
const registration =
|
||||
await this.marketplaceCatalogSyncService.findRegistrationByUniversalIdentifier(
|
||||
await this.marketplaceQueryService.findRegistrationByUniversalIdentifier(
|
||||
universalIdentifier,
|
||||
);
|
||||
|
||||
@@ -58,9 +60,9 @@ export class MarketplaceResolver {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
const registration =
|
||||
await this.marketplaceCatalogSyncService.findOrCreateNpmRegistration({
|
||||
await this.marketplaceQueryService.findOrCreateNpmRegistration({
|
||||
packageName,
|
||||
workspaceId: workspace.id,
|
||||
ownerWorkspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.applicationInstallService.installApplication({
|
||||
|
||||
+8
-117
@@ -8,18 +8,9 @@ import { v4 } from 'uuid';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application-registration/application-registration.entity';
|
||||
import { AppRegistrationSourceType } from 'src/engine/core-modules/application-registration/enums/app-registration-source-type.enum';
|
||||
import { MARKETPLACE_CATALOG_INDEX } from 'src/engine/core-modules/marketplace/constants/marketplace-catalog-index.constant';
|
||||
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/marketplace/crons/marketplace-catalog-sync.cron.job';
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/marketplace/dtos/marketplace-app.dto';
|
||||
import { MarketplaceService } from 'src/engine/core-modules/marketplace/services/marketplace.service';
|
||||
import { type MarketplaceDisplayData } from 'src/engine/core-modules/marketplace/types/marketplace-display-data.type';
|
||||
import { getAdminWorkspaceId } from 'src/engine/core-modules/marketplace/utils/get-admin-workspace-id.util';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceCatalogSyncService {
|
||||
@@ -29,8 +20,6 @@ export class MarketplaceCatalogSyncService {
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
@InjectMessageQueue(MessageQueue.cronQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
async syncCatalog(): Promise<void> {
|
||||
@@ -51,106 +40,7 @@ export class MarketplaceCatalogSyncService {
|
||||
this.logger.log('Marketplace catalog sync completed');
|
||||
}
|
||||
|
||||
async findManyMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
const registrations = await this.appRegistrationRepository.find({
|
||||
where: { sourceType: AppRegistrationSourceType.NPM },
|
||||
});
|
||||
|
||||
if (registrations.length === 0) {
|
||||
this.logger.log(
|
||||
'No marketplace registrations found, enqueuing one-time sync job',
|
||||
);
|
||||
await this.messageQueueService.add(
|
||||
MarketplaceCatalogSyncCronJob.name,
|
||||
{},
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return registrations.map((registration) =>
|
||||
this.toMarketplaceAppDTO(registration),
|
||||
);
|
||||
}
|
||||
|
||||
async findRegistrationByUniversalIdentifier(
|
||||
universalIdentifier: string,
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
const registration = await this.appRegistrationRepository.findOne({
|
||||
where: { universalIdentifier },
|
||||
});
|
||||
|
||||
if (!isDefined(registration)) {
|
||||
throw new ApplicationException(
|
||||
`No application registration found for identifier "${universalIdentifier}"`,
|
||||
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
async findOrCreateNpmRegistration(params: {
|
||||
packageName: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ApplicationRegistrationEntity> {
|
||||
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}"`,
|
||||
);
|
||||
|
||||
const registration = this.appRegistrationRepository.create({
|
||||
universalIdentifier: v4(),
|
||||
name: params.packageName,
|
||||
sourceType: AppRegistrationSourceType.NPM,
|
||||
sourcePackage: params.packageName,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
return this.appRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
toMarketplaceAppDTO(
|
||||
registration: ApplicationRegistrationEntity,
|
||||
): MarketplaceAppDTO {
|
||||
const displayData = registration.marketplaceDisplayData;
|
||||
|
||||
return {
|
||||
id: registration.universalIdentifier,
|
||||
name: registration.name,
|
||||
description: registration.description ?? '',
|
||||
icon: displayData?.icon ?? 'IconApps',
|
||||
version:
|
||||
displayData?.version ?? registration.latestAvailableVersion ?? '0.0.0',
|
||||
author: registration.author ?? 'Unknown',
|
||||
category: displayData?.category ?? '',
|
||||
logo: displayData?.logo,
|
||||
screenshots: displayData?.screenshots ?? [],
|
||||
aboutDescription:
|
||||
displayData?.aboutDescription ?? registration.description ?? '',
|
||||
providers: displayData?.providers ?? [],
|
||||
websiteUrl: registration.websiteUrl ?? undefined,
|
||||
termsUrl: registration.termsUrl ?? undefined,
|
||||
objects: displayData?.objects ?? [],
|
||||
fields: displayData?.fields ?? [],
|
||||
logicFunctions: displayData?.logicFunctions ?? [],
|
||||
frontComponents: displayData?.frontComponents ?? [],
|
||||
sourcePackage: registration.sourcePackage ?? undefined,
|
||||
defaultRole: displayData?.defaultRole,
|
||||
};
|
||||
}
|
||||
|
||||
private async syncCuratedApps(workspaceId: string): Promise<void> {
|
||||
private async syncCuratedApps(ownerWorkspaceId: string): Promise<void> {
|
||||
for (const entry of MARKETPLACE_CATALOG_INDEX) {
|
||||
try {
|
||||
await this.upsertRegistration({
|
||||
@@ -167,7 +57,7 @@ export class MarketplaceCatalogSyncService {
|
||||
latestAvailableVersion: entry.richDisplayData.version ?? null,
|
||||
isFeatured: entry.isFeatured,
|
||||
marketplaceDisplayData: entry.richDisplayData,
|
||||
workspaceId,
|
||||
ownerWorkspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -177,7 +67,7 @@ export class MarketplaceCatalogSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncNpmApps(workspaceId: string): Promise<void> {
|
||||
private async syncNpmApps(ownerWorkspaceId: string): Promise<void> {
|
||||
const npmApps = await this.marketplaceService.fetchAppsFromNpmRegistry();
|
||||
|
||||
const curatedIdentifiers = new Set(
|
||||
@@ -203,7 +93,7 @@ export class MarketplaceCatalogSyncService {
|
||||
latestAvailableVersion: app.version ?? null,
|
||||
isFeatured: false,
|
||||
marketplaceDisplayData: null,
|
||||
workspaceId,
|
||||
ownerWorkspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -213,6 +103,8 @@ export class MarketplaceCatalogSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup by universalIdentifier only (matches the unique constraint).
|
||||
// ownerWorkspaceId is only set on insert.
|
||||
private async upsertRegistration(params: {
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
@@ -226,12 +118,11 @@ export class MarketplaceCatalogSyncService {
|
||||
latestAvailableVersion: string | null;
|
||||
isFeatured: boolean;
|
||||
marketplaceDisplayData: MarketplaceDisplayData | null;
|
||||
workspaceId: string;
|
||||
ownerWorkspaceId: string;
|
||||
}): Promise<void> {
|
||||
const existing = await this.appRegistrationRepository.findOne({
|
||||
where: {
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
workspaceId: params.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -269,7 +160,7 @@ export class MarketplaceCatalogSyncService {
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
workspaceId: params.workspaceId,
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
await this.appRegistrationRepository.save(registration);
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
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-registration/application-registration.entity';
|
||||
import {
|
||||
ApplicationRegistrationException,
|
||||
ApplicationRegistrationExceptionCode,
|
||||
} from 'src/engine/core-modules/application-registration/application-registration.exception';
|
||||
import { AppRegistrationSourceType } from 'src/engine/core-modules/application-registration/enums/app-registration-source-type.enum';
|
||||
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/marketplace/crons/marketplace-catalog-sync.cron.job';
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/marketplace/dtos/marketplace-app.dto';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceQueryService {
|
||||
private readonly logger = new Logger(MarketplaceQueryService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
@InjectMessageQueue(MessageQueue.cronQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
async findManyMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
const registrations = await this.appRegistrationRepository.find({
|
||||
where: { sourceType: AppRegistrationSourceType.NPM },
|
||||
});
|
||||
|
||||
if (registrations.length === 0) {
|
||||
this.logger.log(
|
||||
'No marketplace registrations found, enqueuing one-time sync job',
|
||||
);
|
||||
await this.messageQueueService.add(
|
||||
MarketplaceCatalogSyncCronJob.name,
|
||||
{},
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return registrations.map((registration) =>
|
||||
this.toMarketplaceAppDTO(registration),
|
||||
);
|
||||
}
|
||||
|
||||
async findRegistrationByUniversalIdentifier(
|
||||
universalIdentifier: string,
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
const registration = await this.appRegistrationRepository.findOne({
|
||||
where: { universalIdentifier },
|
||||
});
|
||||
|
||||
if (!isDefined(registration)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
`No application registration found for identifier "${universalIdentifier}"`,
|
||||
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
async findOrCreateNpmRegistration(params: {
|
||||
packageName: string;
|
||||
ownerWorkspaceId: string;
|
||||
}): Promise<ApplicationRegistrationEntity> {
|
||||
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}"`,
|
||||
);
|
||||
|
||||
const registration = this.appRegistrationRepository.create({
|
||||
universalIdentifier: v4(),
|
||||
name: params.packageName,
|
||||
sourceType: AppRegistrationSourceType.NPM,
|
||||
sourcePackage: params.packageName,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
return this.appRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
toMarketplaceAppDTO(
|
||||
registration: ApplicationRegistrationEntity,
|
||||
): MarketplaceAppDTO {
|
||||
const displayData = registration.marketplaceDisplayData;
|
||||
|
||||
return {
|
||||
id: registration.universalIdentifier,
|
||||
name: registration.name,
|
||||
description: registration.description ?? '',
|
||||
icon: displayData?.icon ?? 'IconApps',
|
||||
version:
|
||||
displayData?.version ?? registration.latestAvailableVersion ?? '0.0.0',
|
||||
author: registration.author ?? 'Unknown',
|
||||
category: displayData?.category ?? '',
|
||||
logo: displayData?.logo,
|
||||
screenshots: displayData?.screenshots ?? [],
|
||||
aboutDescription:
|
||||
displayData?.aboutDescription ?? registration.description ?? '',
|
||||
providers: displayData?.providers ?? [],
|
||||
websiteUrl: registration.websiteUrl ?? undefined,
|
||||
termsUrl: registration.termsUrl ?? undefined,
|
||||
objects: displayData?.objects ?? [],
|
||||
fields: displayData?.fields ?? [],
|
||||
logicFunctions: displayData?.logicFunctions ?? [],
|
||||
frontComponents: displayData?.frontComponents ?? [],
|
||||
sourcePackage: registration.sourcePackage ?? undefined,
|
||||
defaultRole: displayData?.defaultRole,
|
||||
};
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -1,12 +1,17 @@
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
// Every ApplicationRegistration must be owned by a workspace (workspaceId
|
||||
// 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> => {
|
||||
|
||||
Reference in New Issue
Block a user