From 0e89c96170e9d003307b9746b6a58802c10c28bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 5 Mar 2026 10:34:08 +0100 Subject: [PATCH] feat: add npm and tarball app distribution with upgrade mechanism (#18358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **npm + tarball app distribution**: Apps can be installed from the npm registry (public or private) or uploaded as `.tar.gz` tarballs, with `AppRegistrationSourceType` tracking the origin - **Upgrade mechanism**: `AppUpgradeService` checks for newer versions, supports rollback for npm-sourced apps, and a cron job runs every 6 hours to update `latestAvailableVersion` on registrations - **Security hardening**: Tarball extraction uses path traversal protection, and `enableScripts: false` in `.yarnrc.yml` disables all lifecycle scripts during `yarn install` to prevent RCE - **Frontend**: "Install from npm" and "Upload tarball" modals, upgrade button on app detail page, blue "Update" badge on installed apps table when a newer version is available - **Marketplace catalog sync**: Hourly cron job syncs a hardcoded catalog index into `ApplicationRegistration` entities - **Integration tests**: Coverage for install, upgrade, tarball upload, and catalog sync flows ## Backend changes | Area | Files | |------|-------| | Entity & migration | `ApplicationRegistrationEntity` (sourceType, sourcePackage, latestAvailableVersion), `ApplicationEntity` (applicationRegistrationId), migration | | Services | `AppPackageResolverService`, `ApplicationInstallService`, `AppUpgradeService`, `MarketplaceCatalogSyncService` | | Cron jobs | `MarketplaceCatalogSyncCronJob` (hourly), `AppVersionCheckCronJob` (every 6h) | | REST endpoint | `AppRegistrationUploadController` — tarball upload with secure extraction | | Resolver | `MarketplaceResolver` — simplified `installMarketplaceApp` (removed redundant `sourcePackage` arg) | | Security | `.yarnrc.yml` — `enableScripts: false` to block postinstall RCE | ## Frontend changes | Area | Files | |------|-------| | Modals | `SettingsInstallNpmAppModal`, `SettingsUploadTarballModal`, `SettingsAppModalLayout` | | Hooks | `useUploadAppTarball`, `useInstallMarketplaceApp` (cleaned up) | | Upgrade UI | `SettingsApplicationVersionContainer`, `SettingsApplicationDetailAboutTab` | | Badge | `SettingsApplicationTableRow` — blue "Update" tag, `SettingsApplicationsInstalledTab` — fetches registrations for version comparison | | Styling | Migrated to Linaria (matching main) | ## Test plan - [ ] Install an app from npm via the "Install from npm" modal - [ ] Upload a `.tar.gz` tarball via the "Upload tarball" modal - [ ] Verify upgrade badge appears when `latestAvailableVersion > version` - [ ] Verify upgrade flow from app detail page - [ ] Run integration tests: `app-distribution.integration-spec.ts`, `marketplace-catalog-sync.integration-spec.ts` - [ ] Verify `enableScripts: false` blocks postinstall scripts during yarn install Made with [Cursor](https://cursor.com) --- packages/twenty-apps/hello-world/package.json | 4 +- packages/twenty-apps/hello-world/yarn.lock | 34 +- .../src/generated-metadata/graphql.ts | 256 ++++++++++++- .../graphql/fragments/applicationFragment.ts | 6 + .../graphql/queries/findManyApplications.ts | 6 + .../fragments/marketplaceAppFragment.ts | 1 + .../mutations/installMarketplaceApp.ts | 10 +- .../graphql/mutations/installNpmApp.ts | 7 + .../graphql/mutations/upgradeApplication.ts | 13 + .../graphql/mutations/uploadAppTarball.ts | 11 + .../marketplace/hooks/useInstallApp.ts | 41 +++ .../hooks/useInstallMarketplaceApp.ts | 16 +- .../marketplace/hooks/useInstallNpmApp.ts | 13 + .../marketplace}/hooks/useMarketplaceApps.ts | 0 .../hooks/useUpgradeApplication.ts | 44 +++ .../marketplace/hooks/useUploadAppTarball.ts | 54 +++ .../applicationRegistrationFragment.ts | 3 + .../SettingsAvailableApplicationDetails.tsx | 11 +- .../components/SettingsAppModalLayout.tsx | 94 +++++ .../SettingsApplicationTableRow.tsx | 18 +- .../SettingsApplicationVersionContainer.tsx | 95 +++-- .../components/SettingsApplicationsTable.tsx | 52 ++- .../components/SettingsInstallNpmAppModal.tsx | 122 ++++++ .../components/SettingsUploadTarballModal.tsx | 113 ++++++ .../SettingsApplicationDetailAboutTab.tsx | 13 +- .../tabs/SettingsApplicationsAvailableTab.tsx | 2 +- .../tabs/SettingsApplicationsInstalledTab.tsx | 93 ++++- .../types/applicationWithoutRelation.ts | 7 +- .../applications/utils/isNewerSemver.ts | 15 + packages/twenty-sdk/package.json | 1 + .../src/cli/commands/app-command.ts | 26 ++ .../src/cli/commands/app/app-pack.ts | 30 ++ .../src/cli/commands/app/app-push.ts | 62 ++++ .../src/cli/public-operations/app-pack.ts | 39 ++ .../src/cli/public-operations/index.ts | 2 + .../src/cli/utilities/api/api-service.ts | 150 +++++++- packages/twenty-sdk/vite.config.node.ts | 13 +- packages/twenty-server/package.json | 2 + .../commands/cron-register-all.command.ts | 12 + .../commands/database-command.module.ts | 4 + ...kfill-application-package-files.command.ts | 2 +- .../1-17-identify-webhook-metadata.command.ts | 2 +- ...ge-channel-throttle-retry-after.command.ts | 2 +- ...y-rich-text-attachment-file-ids.command.ts | 2 +- .../1-18-migrate-attachment-files.command.ts | 2 +- ...orites-to-navigation-menu-items.command.ts | 2 +- ...-18-migrate-person-avatar-files.command.ts | 2 +- ...workflow-send-email-attachments.command.ts | 2 +- ...1-18-migrate-workspace-pictures.command.ts | 2 +- ...sage-association-message-folder.command.ts | 2 +- .../1-19-backfill-page-layouts.command.ts | 2 +- ...67875870-addAppRegistrationSourceFields.ts | 70 ++++ .../application-development.module.ts | 28 ++ .../application-development.resolver.ts | 125 ++++++- .../application-exception-filter.ts | 8 + .../app-package-fetcher.service.ts | 285 ++++++++++++++ .../app-upgrade.service.ts | 134 +++++++ .../application-install.module.ts} | 45 ++- .../application-install.resolver.ts} | 26 +- .../application-install.service.ts | 215 +++++++++++ .../application-manifest-migration.service.ts | 2 +- .../application-sync.service.ts | 113 +----- .../marketplace-catalog-index.constant.ts | 179 +++++++++ .../marketplace-catalog-sync.cron.command.ts | 35 ++ ...lace-catalog-sync-cron-pattern.constant.ts | 1 + .../marketplace-catalog-sync.cron.job.ts | 37 ++ .../dtos/marketplace-app.dto.ts | 9 + .../marketplace.module.ts | 38 ++ .../mocks/data-enrichment-logo.svg | 0 .../mocks/mock-manifest.json | 0 .../resolvers/marketplace.resolver.ts | 114 ++++++ .../marketplace-catalog-sync.service.ts | 171 +++++++++ .../services/marketplace-query.service.ts | 162 ++++++++ .../services/marketplace.service.ts | 94 +++++ .../types/marketplace-display-data.type.ts | 75 ++++ .../utils/get-admin-workspace-id.util.ts | 36 ++ .../__tests__/oauth-scopes.spec.ts | 2 +- ...plication-registration-exception-filter.ts | 30 ++ ...pplication-registration-variable.entity.ts | 2 +- ...plication-registration-variable.service.ts | 12 +- .../application-registration.entity.ts | 49 ++- .../application-registration.exception.ts | 6 + .../application-registration.module.ts | 26 +- .../application-registration.resolver.ts | 100 ++++- .../application-registration.service.ts | 51 +-- .../constants/oauth-scopes.ts | 0 .../controllers/oauth-discovery.controller.ts | 2 +- .../controllers/oauth-token.controller.ts | 12 +- .../application-registration-stats.dto.ts | 0 ...application-registration-variable.input.ts | 0 .../create-application-registration.dto.ts | 2 +- .../create-application-registration.input.ts | 0 .../dtos/oauth-introspect.input.ts | 0 .../dtos/oauth-revoke.input.ts | 0 .../dtos/oauth-token.input.ts | 0 .../public-application-registration.dto.ts | 0 .../dtos/rotate-client-secret.dto.ts | 0 ...application-registration-variable.input.ts | 0 .../update-application-registration.input.ts | 0 .../app-registration-source-type.enum.ts | 11 + .../application-registration/oauth.service.ts | 39 +- .../services/app-tarball-upload.service.ts | 137 +++++++ .../types/oauth-error-response.type.ts | 0 .../types/oauth-token-response.type.ts | 0 .../application-variable.service.spec.ts | 8 +- .../application-variable-exception-filter.ts | 2 +- .../application-variable.entity.ts | 0 .../application-variable.exception.ts | 0 .../application-variable.module.ts | 8 +- .../application-variable.resolver.ts | 10 +- .../application-variable.service.ts | 6 +- ...cret-application-variable-mask.constant.ts | 0 .../dtos/application-variable.dto.ts | 0 .../dtos/update-application-variable.input.ts | 0 ...-application-variable-map-cache.service.ts | 6 +- .../application-variable-cache-maps.type.ts | 2 +- .../types/flat-application-variable.type.ts | 2 +- ...ntity-to-flat-application-variable.util.ts | 4 +- .../application-version-check.module.ts | 12 + .../crons/app-version-check.cron.job.ts | 35 ++ .../app-version-check.cron.command.ts | 33 ++ ...app-version-check-cron-pattern.constant.ts | 2 + .../application/application.entity.ts | 9 +- .../application/application.exception.ts | 12 + .../application/application.module.ts | 19 +- .../{services => }/application.service.ts | 49 ++- .../constants/yarn-engine/.yarnrc.yml | 2 + .../application-registration-summary.dto.ts | 20 + .../application/dtos/application.dto.ts | 16 +- .../resolvers/marketplace.resolver.ts | 30 -- .../services/marketplace.service.ts | 347 ------------------ .../mocked-marketplace-app.constant.ts | 204 ---------- .../assert-valid-npm-package-name.util.ts | 18 + .../utils/extract-tarball-securely.util.ts | 49 +++ .../application/utils/read-json-file.util.ts | 38 ++ .../application/utils/tarball-utils.ts | 21 ++ ...pace-flat-application-map-cache.service.ts | 0 .../engine/core-modules/auth/auth.module.ts | 2 +- .../auth/services/auth.service.spec.ts | 2 +- .../auth/services/auth.service.ts | 2 +- .../auth/services/sign-in-up.service.ts | 2 +- .../engine/core-modules/core-engine.module.ts | 10 +- .../file-storage/file-storage.module.ts | 2 +- .../services/file-ai-chat.service.ts | 2 +- .../services/file-core-picture.service.ts | 2 +- .../services/file-workflow.service.ts | 2 +- .../file/interfaces/file-folder.interface.ts | 3 + .../logic-function-executor.service.ts | 2 +- .../utils/__tests__/build-env-var.spec.ts | 2 +- .../utils/build-env-var.ts | 2 +- .../code-interpreter-tool.ts | 2 +- .../twenty-config/config-variables.ts | 20 + .../user/services/user.service.spec.ts | 2 +- .../workspace/workspace.resolver.ts | 2 +- .../ai/ai-agent/agent.service.ts | 2 +- .../command-menu-item.service.ts | 2 +- .../services/field-metadata.service.ts | 2 +- ...-to-flat-field-metadatas-to-create.spec.ts | 3 +- ...-relation-flat-field-metadata-pair.spec.ts | 3 +- .../front-component.service.ts | 2 +- ...gic-function-from-source-helper.service.ts | 2 +- .../logic-function-from-source.service.ts | 2 +- .../navigation-menu-item.service.ts | 2 +- .../navigation-menu-item-deletion.service.ts | 2 +- .../object-metadata.service.ts | 2 +- .../services/page-layout-tab.service.ts | 2 +- .../services/page-layout-widget.service.ts | 2 +- .../page-layout-duplication.service.ts | 2 +- .../services/page-layout-update.service.ts | 2 +- .../services/page-layout.service.ts | 2 +- .../__tests__/permissions.service.spec.ts | 2 +- .../permissions/permissions.service.ts | 2 +- .../services/role-target.service.ts | 2 +- .../metadata-modules/role/role.resolver.ts | 2 +- .../metadata-modules/role/role.service.ts | 2 +- .../row-level-permission-predicate.service.ts | 2 +- .../metadata-modules/skill/skill.service.ts | 2 +- .../services/fields-widget-upsert.service.ts | 2 +- .../services/view-field-group.service.ts | 2 +- .../view-field/services/view-field.service.ts | 2 +- .../tests/view-filter-group.service.spec.ts | 2 +- .../services/view-filter-group.service.ts | 2 +- .../services/view-filter.service.ts | 2 +- .../view-group/services/view-group.service.ts | 2 +- .../view-sort/services/view-sort.service.ts | 2 +- .../view/services/view.service.ts | 2 +- .../webhook/webhook.service.ts | 2 +- .../types/workspace-cache-key.type.ts | 2 +- .../core/utils/seed-core-schema.util.ts | 2 +- .../dev-seeder/services/dev-seeder.service.ts | 2 +- .../constants/twenty-standard-applications.ts | 3 +- .../twenty-standard-application.service.ts | 2 +- .../all-non-workspace-related-entity.type.ts | 2 +- .../workspace-manager.service.ts | 2 +- .../dashboard-tool.workspace-service.ts | 2 +- .../types/dashboard-tool-dependencies.type.ts | 2 +- .../workflow-execution-context.service.ts | 2 +- ...lication-registration-variable-api.util.ts | 2 +- ...ure-flag-disabled.integration-spec.ts.snap | 4 +- .../app-distribution.integration-spec.ts | 305 +++++++++++++++ ...rketplace-catalog-sync.integration-spec.ts | 193 ++++++++++ .../upload-app-tarball-query-factory.util.ts | 21 ++ .../utils/upload-app-tarball.util.ts | 56 +++ .../twenty-shared/src/types/FileFolder.ts | 1 + yarn.lock | 59 ++- 205 files changed, 4741 insertions(+), 1014 deletions(-) create mode 100644 packages/twenty-front/src/modules/marketplace/graphql/mutations/installNpmApp.ts create mode 100644 packages/twenty-front/src/modules/marketplace/graphql/mutations/upgradeApplication.ts create mode 100644 packages/twenty-front/src/modules/marketplace/graphql/mutations/uploadAppTarball.ts create mode 100644 packages/twenty-front/src/modules/marketplace/hooks/useInstallApp.ts create mode 100644 packages/twenty-front/src/modules/marketplace/hooks/useInstallNpmApp.ts rename packages/twenty-front/src/{pages/settings/applications => modules/marketplace}/hooks/useMarketplaceApps.ts (100%) create mode 100644 packages/twenty-front/src/modules/marketplace/hooks/useUpgradeApplication.ts create mode 100644 packages/twenty-front/src/modules/marketplace/hooks/useUploadAppTarball.ts create mode 100644 packages/twenty-front/src/pages/settings/applications/components/SettingsAppModalLayout.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/components/SettingsInstallNpmAppModal.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/components/SettingsUploadTarballModal.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/utils/isNewerSemver.ts create mode 100644 packages/twenty-sdk/src/cli/commands/app/app-pack.ts create mode 100644 packages/twenty-sdk/src/cli/commands/app/app-push.ts create mode 100644 packages/twenty-sdk/src/cli/public-operations/app-pack.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1772267875870-addAppRegistrationSourceFields.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts rename packages/twenty-server/src/engine/core-modules/application/{resolvers => application-development}/application-development.resolver.ts (59%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-install/app-package-fetcher.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-install/app-upgrade.service.ts rename packages/twenty-server/src/engine/core-modules/application/{application-sync.module.ts => application-install/application-install.module.ts} (56%) rename packages/twenty-server/src/engine/core-modules/application/{resolvers/application.resolver.ts => application-install/application-install.resolver.ts} (93%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts rename packages/twenty-server/src/engine/core-modules/application/{services => application-install}/application-manifest-migration.service.ts (99%) rename packages/twenty-server/src/engine/core-modules/application/{services => application-install}/application-sync.service.ts (70%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/constants/marketplace-catalog-index.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/crons/constants/marketplace-catalog-sync-cron-pattern.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job.ts rename packages/twenty-server/src/engine/core-modules/application/{ => application-marketplace}/dtos/marketplace-app.dto.ts (96%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace.module.ts rename packages/twenty-server/src/engine/core-modules/{marketplace => application/application-marketplace}/mocks/data-enrichment-logo.svg (100%) rename packages/twenty-server/src/engine/core-modules/{marketplace => application/application-marketplace}/mocks/mock-manifest.json (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/resolvers/marketplace.resolver.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/services/marketplace-catalog-sync.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/services/marketplace-query.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/services/marketplace.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/types/marketplace-display-data.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-marketplace/utils/get-admin-workspace-id.util.ts rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/__tests__/oauth-scopes.spec.ts (88%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-exception-filter.ts rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration-variable.entity.ts (96%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration-variable.service.ts (90%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration.entity.ts (63%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration.exception.ts (83%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration.module.ts (60%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration.resolver.ts (66%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/application-registration.service.ts (86%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/constants/oauth-scopes.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/controllers/oauth-discovery.controller.ts (96%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/controllers/oauth-token.controller.ts (93%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/application-registration-stats.dto.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/create-application-registration-variable.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/create-application-registration.dto.ts (81%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/create-application-registration.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/oauth-introspect.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/oauth-revoke.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/oauth-token.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/public-application-registration.dto.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/rotate-client-secret.dto.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/update-application-registration-variable.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/dtos/update-application-registration.input.ts (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-registration/enums/app-registration-source-type.enum.ts rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/oauth.service.ts (92%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-registration/services/app-tarball-upload.service.ts rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/types/oauth-error-response.type.ts (100%) rename packages/twenty-server/src/engine/core-modules/{ => application}/application-registration/types/oauth-token-response.type.ts (100%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/__tests__/application-variable.service.spec.ts (96%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/application-variable-exception-filter.ts (87%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/application-variable.entity.ts (100%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/application-variable.exception.ts (100%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/application-variable.module.ts (79%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/application-variable.resolver.ts (82%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/application-variable.service.ts (93%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/constants/secret-application-variable-mask.constant.ts (100%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/dtos/application-variable.dto.ts (100%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/dtos/update-application-variable.input.ts (100%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/services/workspace-application-variable-map-cache.service.ts (88%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/types/application-variable-cache-maps.type.ts (75%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/types/flat-application-variable.type.ts (78%) rename packages/twenty-server/src/engine/core-modules/{applicationVariable => application/application-variable}/utils/from-application-variable-entity-to-flat-application-variable.util.ts (77%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-version-check/application-version-check.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-version-check/crons/app-version-check.cron.job.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-version-check/crons/commands/app-version-check.cron.command.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-version-check/crons/constants/app-version-check-cron-pattern.constant.ts rename packages/twenty-server/src/engine/core-modules/application/{services => }/application.service.ts (92%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/dtos/application-registration-summary.dto.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/application/resolvers/marketplace.resolver.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/application/services/marketplace.service.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/utils/assert-valid-npm-package-name.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/utils/extract-tarball-securely.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/utils/read-json-file.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/utils/tarball-utils.ts rename packages/twenty-server/src/engine/core-modules/application/{services => }/workspace-flat-application-map-cache.service.ts (100%) create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/app-distribution.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/marketplace-catalog-sync.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/upload-app-tarball-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/upload-app-tarball.util.ts diff --git a/packages/twenty-apps/hello-world/package.json b/packages/twenty-apps/hello-world/package.json index 1e8141e3016..33888defe51 100644 --- a/packages/twenty-apps/hello-world/package.json +++ b/packages/twenty-apps/hello-world/package.json @@ -1,6 +1,6 @@ { - "name": "hello-world", - "version": "0.1.0", + "name": "@twentyhq/hello-world", + "version": "0.2.2", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/hello-world/yarn.lock b/packages/twenty-apps/hello-world/yarn.lock index 67087601951..d84e9c64885 100644 --- a/packages/twenty-apps/hello-world/yarn.lock +++ b/packages/twenty-apps/hello-world/yarn.lock @@ -1591,6 +1591,22 @@ __metadata: languageName: node linkType: hard +"@twentyhq/hello-world@workspace:.": + version: 0.0.0-use.local + resolution: "@twentyhq/hello-world@workspace:." + dependencies: + "@types/node": "npm:^24.7.2" + "@types/react": "npm:^18.2.0" + eslint: "npm:^9.32.0" + react: "npm:^18.2.0" + twenty-sdk: "npm:0.6.3" + typescript: "npm:^5.9.3" + typescript-eslint: "npm:^8.50.0" + vite-tsconfig-paths: "npm:^4.2.1" + vitest: "npm:^3.1.1" + languageName: unknown + linkType: soft + "@types/chai@npm:^5.2.2": version: 5.2.3 resolution: "@types/chai@npm:5.2.3" @@ -1604,7 +1620,7 @@ __metadata: "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" - checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + checksum: 10c0-bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 languageName: node linkType: hard @@ -4676,22 +4692,6 @@ __metadata: languageName: node linkType: hard -"hello-world@workspace:.": - version: 0.0.0-use.local - resolution: "hello-world@workspace:." - dependencies: - "@types/node": "npm:^24.7.2" - "@types/react": "npm:^18.2.0" - eslint: "npm:^9.32.0" - react: "npm:^18.2.0" - twenty-sdk: "npm:0.6.3" - typescript: "npm:^5.9.3" - typescript-eslint: "npm:^8.50.0" - vite-tsconfig-paths: "npm:^4.2.1" - vitest: "npm:^3.1.1" - languageName: unknown - linkType: soft - "hoist-non-react-statics@npm:^3.3.1": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index aefa4a3c9ec..99663c9de55 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -316,6 +316,12 @@ export type ApiKeyToken = { token: Scalars['String']; }; +export enum AppRegistrationSourceType { + LOCAL = 'LOCAL', + NPM = 'NPM', + TARBALL = 'TARBALL' +} + export type AppToken = { __typename?: 'AppToken'; createdAt: Scalars['DateTime']; @@ -328,12 +334,14 @@ export type AppToken = { export type Application = { __typename?: 'Application'; agents: Array; + applicationRegistration?: Maybe; + applicationRegistrationId?: Maybe; applicationVariables: Array; availablePackages: Scalars['JSON']; canBeUninstalled: Scalars['Boolean']; defaultLogicFunctionRole?: Maybe; defaultRoleId?: Maybe; - description: Scalars['String']; + description?: Maybe; id: Scalars['UUID']; logicFunctions: Array; name: Scalars['String']; @@ -342,7 +350,7 @@ export type Application = { packageJsonFileId?: Maybe; settingsCustomTabFrontComponentId?: Maybe; universalIdentifier: Scalars['String']; - version: Scalars['String']; + version?: Maybe; yarnLockChecksum?: Maybe; yarnLockFileId?: Maybe; }; @@ -353,11 +361,15 @@ export type ApplicationRegistration = { createdAt: Scalars['DateTime']; description?: Maybe; id: Scalars['UUID']; + isFeatured: Scalars['Boolean']; + latestAvailableVersion?: Maybe; logoUrl?: Maybe; name: Scalars['String']; oAuthClientId: Scalars['String']; oAuthRedirectUris: Array; oAuthScopes: Array; + sourcePackage?: Maybe; + sourceType: AppRegistrationSourceType; termsUrl?: Maybe; universalIdentifier: Scalars['String']; updatedAt: Scalars['DateTime']; @@ -371,6 +383,13 @@ export type ApplicationRegistrationStats = { versionDistribution: Array; }; +export type ApplicationRegistrationSummary = { + __typename?: 'ApplicationRegistrationSummary'; + id: Scalars['UUID']; + latestAvailableVersion?: Maybe; + sourceType: AppRegistrationSourceType; +}; + export type ApplicationRegistrationVariable = { __typename?: 'ApplicationRegistrationVariable'; createdAt: Scalars['DateTime']; @@ -1772,6 +1791,7 @@ export type File = { export enum FileFolder { AgentChat = 'AgentChat', + AppTarball = 'AppTarball', Attachment = 'Attachment', BuiltFrontComponent = 'BuiltFrontComponent', BuiltLogicFunction = 'BuiltLogicFunction', @@ -2218,6 +2238,7 @@ export type MarketplaceApp = { objects: Array; providers: Array; screenshots: Array; + sourcePackage?: Maybe; termsUrl?: Maybe; version: Scalars['String']; websiteUrl?: Maybe; @@ -2432,6 +2453,7 @@ export type Mutation = { initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioning; installApplication: Scalars['Boolean']; installMarketplaceApp: Scalars['Boolean']; + installNpmApp: Scalars['Boolean']; removeQueryFromEventStream: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewApplicationToken: ApplicationTokenPair; @@ -2490,7 +2512,9 @@ export type Mutation = { updateWorkspace: Workspace; updateWorkspaceFeatureFlag: Scalars['Boolean']; updateWorkspaceMemberRole: WorkspaceMember; + upgradeApplication: Scalars['Boolean']; uploadAIChatFile: FileWithSignedUrl; + uploadAppTarball: ApplicationRegistration; uploadApplicationFile: File; uploadFilesFieldFile: FileWithSignedUrl; uploadFilesFieldFileByUniversalIdentifier: FileWithSignedUrl; @@ -3019,6 +3043,18 @@ export type MutationInstallApplicationArgs = { }; +export type MutationInstallMarketplaceAppArgs = { + universalIdentifier: Scalars['String']; + version?: InputMaybe; +}; + + +export type MutationInstallNpmAppArgs = { + packageName: Scalars['String']; + version?: InputMaybe; +}; + + export type MutationRemoveQueryFromEventStreamArgs = { input: RemoveQueryFromEventStreamInput; }; @@ -3324,11 +3360,23 @@ export type MutationUpdateWorkspaceMemberRoleArgs = { }; +export type MutationUpgradeApplicationArgs = { + appRegistrationId: Scalars['String']; + targetVersion: Scalars['String']; +}; + + export type MutationUploadAiChatFileArgs = { file: Scalars['Upload']; }; +export type MutationUploadAppTarballArgs = { + file: Scalars['Upload']; + universalIdentifier?: InputMaybe; +}; + + export type MutationUploadApplicationFileArgs = { applicationUniversalIdentifier: Scalars['String']; file: Scalars['Upload']; @@ -5765,19 +5813,19 @@ export type UpdateOneApplicationVariableMutationVariables = Exact<{ export type UpdateOneApplicationVariableMutation = { __typename?: 'Mutation', updateOneApplicationVariable: boolean }; -export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, 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, 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> | 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: 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, 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> | 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, version: string }> }; +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 FindOneApplicationQueryVariables = Exact<{ id: Scalars['UUID']; }>; -export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, 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, 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> | 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: 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, 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> | 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 AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string }; @@ -6172,12 +6220,44 @@ 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, aboutDescription: string, providers: Array, websiteUrl?: string | null, termsUrl?: 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, 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, aboutDescription: string, providers: Array, 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, 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 InstallMarketplaceAppMutationVariables = Exact<{ + universalIdentifier: Scalars['String']; + version?: InputMaybe; +}>; + + +export type InstallMarketplaceAppMutation = { __typename?: 'Mutation', installMarketplaceApp: boolean }; + +export type InstallNpmAppMutationVariables = Exact<{ + packageName: Scalars['String']; + version?: InputMaybe; +}>; + + +export type InstallNpmAppMutation = { __typename?: 'Mutation', installNpmApp: boolean }; + +export type UpgradeApplicationMutationVariables = Exact<{ + appRegistrationId: Scalars['String']; + targetVersion: Scalars['String']; +}>; + + +export type UpgradeApplicationMutation = { __typename?: 'Mutation', upgradeApplication: boolean }; + +export type UploadAppTarballMutationVariables = Exact<{ + file: Scalars['Upload']; + universalIdentifier?: InputMaybe; +}>; + + +export type UploadAppTarballMutation = { __typename?: 'Mutation', uploadAppTarball: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string } }; 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, aboutDescription: string, providers: Array, websiteUrl?: string | null, termsUrl?: 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, 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, aboutDescription: string, providers: Array, 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, 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 }; @@ -6464,7 +6544,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, oAuthScopes: Array, 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, oAuthScopes: Array, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string }; export type DeleteApplicationRegistrationMutationVariables = Exact<{ id: Scalars['String']; @@ -6485,7 +6565,7 @@ export type UpdateApplicationRegistrationMutationVariables = Exact<{ }>; -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, oAuthScopes: Array, 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, oAuthScopes: Array, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string } }; export type UpdateApplicationRegistrationVariableMutationVariables = Exact<{ input: UpdateApplicationRegistrationVariableInput; @@ -6518,14 +6598,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, oAuthScopes: Array, 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, oAuthScopes: Array, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: 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, oAuthScopes: Array, 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, oAuthScopes: Array, sourceType: AppRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, websiteUrl?: string | null, termsUrl?: string | null, createdAt: string, updatedAt: string } }; export type UninstallApplicationMutationVariables = Exact<{ universalIdentifier: Scalars['String']; @@ -7422,6 +7502,12 @@ export const ApplicationFieldsFragmentDoc = gql` description version universalIdentifier + applicationRegistrationId + applicationRegistration { + id + latestAvailableVersion + sourceType + } canBeUninstalled defaultRoleId settingsCustomTabFrontComponentId @@ -7790,6 +7876,7 @@ export const MarketplaceAppFieldsFragmentDoc = gql` name description } + sourcePackage defaultRole { id label @@ -7856,6 +7943,9 @@ export const ApplicationRegistrationFragmentFragmentDoc = gql` oAuthClientId oAuthRedirectUris oAuthScopes + sourceType + sourcePackage + latestAvailableVersion websiteUrl termsUrl createdAt @@ -9283,6 +9373,12 @@ export const FindManyApplicationsDocument = gql` name description version + applicationRegistrationId + applicationRegistration { + id + latestAvailableVersion + sourceType + } } } `; @@ -11329,6 +11425,144 @@ export function useGetLogicFunctionSourceCodeLazyQuery(baseOptions?: Apollo.Lazy export type GetLogicFunctionSourceCodeQueryHookResult = ReturnType; export type GetLogicFunctionSourceCodeLazyQueryHookResult = ReturnType; export type GetLogicFunctionSourceCodeQueryResult = Apollo.QueryResult; +export const InstallMarketplaceAppDocument = gql` + mutation InstallMarketplaceApp($universalIdentifier: String!, $version: String) { + installMarketplaceApp( + universalIdentifier: $universalIdentifier + version: $version + ) +} + `; +export type InstallMarketplaceAppMutationFn = Apollo.MutationFunction; + +/** + * __useInstallMarketplaceAppMutation__ + * + * To run a mutation, you first call `useInstallMarketplaceAppMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInstallMarketplaceAppMutation` 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 [installMarketplaceAppMutation, { data, loading, error }] = useInstallMarketplaceAppMutation({ + * variables: { + * universalIdentifier: // value for 'universalIdentifier' + * version: // value for 'version' + * }, + * }); + */ +export function useInstallMarketplaceAppMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InstallMarketplaceAppDocument, options); + } +export type InstallMarketplaceAppMutationHookResult = ReturnType; +export type InstallMarketplaceAppMutationResult = Apollo.MutationResult; +export type InstallMarketplaceAppMutationOptions = Apollo.BaseMutationOptions; +export const InstallNpmAppDocument = gql` + mutation InstallNpmApp($packageName: String!, $version: String) { + installNpmApp(packageName: $packageName, version: $version) +} + `; +export type InstallNpmAppMutationFn = Apollo.MutationFunction; + +/** + * __useInstallNpmAppMutation__ + * + * 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: + * - 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({ + * variables: { + * packageName: // value for 'packageName' + * version: // value for 'version' + * }, + * }); + */ +export function useInstallNpmAppMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InstallNpmAppDocument, options); + } +export type InstallNpmAppMutationHookResult = ReturnType; +export type InstallNpmAppMutationResult = Apollo.MutationResult; +export type InstallNpmAppMutationOptions = Apollo.BaseMutationOptions; +export const UpgradeApplicationDocument = gql` + mutation UpgradeApplication($appRegistrationId: String!, $targetVersion: String!) { + upgradeApplication( + appRegistrationId: $appRegistrationId + targetVersion: $targetVersion + ) +} + `; +export type UpgradeApplicationMutationFn = Apollo.MutationFunction; + +/** + * __useUpgradeApplicationMutation__ + * + * To run a mutation, you first call `useUpgradeApplicationMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpgradeApplicationMutation` 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 [upgradeApplicationMutation, { data, loading, error }] = useUpgradeApplicationMutation({ + * variables: { + * appRegistrationId: // value for 'appRegistrationId' + * targetVersion: // value for 'targetVersion' + * }, + * }); + */ +export function useUpgradeApplicationMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpgradeApplicationDocument, options); + } +export type UpgradeApplicationMutationHookResult = ReturnType; +export type UpgradeApplicationMutationResult = Apollo.MutationResult; +export type UpgradeApplicationMutationOptions = Apollo.BaseMutationOptions; +export const UploadAppTarballDocument = gql` + mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) { + uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) { + id + universalIdentifier + name + } +} + `; +export type UploadAppTarballMutationFn = Apollo.MutationFunction; + +/** + * __useUploadAppTarballMutation__ + * + * To run a mutation, you first call `useUploadAppTarballMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUploadAppTarballMutation` 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 [uploadAppTarballMutation, { data, loading, error }] = useUploadAppTarballMutation({ + * variables: { + * file: // value for 'file' + * universalIdentifier: // value for 'universalIdentifier' + * }, + * }); + */ +export function useUploadAppTarballMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UploadAppTarballDocument, options); + } +export type UploadAppTarballMutationHookResult = ReturnType; +export type UploadAppTarballMutationResult = Apollo.MutationResult; +export type UploadAppTarballMutationOptions = Apollo.BaseMutationOptions; export const FindManyMarketplaceAppsDocument = gql` query FindManyMarketplaceApps { findManyMarketplaceApps { diff --git a/packages/twenty-front/src/modules/applications/graphql/fragments/applicationFragment.ts b/packages/twenty-front/src/modules/applications/graphql/fragments/applicationFragment.ts index 882e253fb4b..126e174c6ec 100644 --- a/packages/twenty-front/src/modules/applications/graphql/fragments/applicationFragment.ts +++ b/packages/twenty-front/src/modules/applications/graphql/fragments/applicationFragment.ts @@ -13,6 +13,12 @@ export const APPLICATION_FRAGMENT = gql` description version universalIdentifier + applicationRegistrationId + applicationRegistration { + id + latestAvailableVersion + sourceType + } canBeUninstalled defaultRoleId settingsCustomTabFrontComponentId diff --git a/packages/twenty-front/src/modules/applications/graphql/queries/findManyApplications.ts b/packages/twenty-front/src/modules/applications/graphql/queries/findManyApplications.ts index ceb1ba708fd..5144c9c928b 100644 --- a/packages/twenty-front/src/modules/applications/graphql/queries/findManyApplications.ts +++ b/packages/twenty-front/src/modules/applications/graphql/queries/findManyApplications.ts @@ -7,6 +7,12 @@ export const FIND_MANY_APPLICATIONS = gql` name description version + applicationRegistrationId + applicationRegistration { + id + latestAvailableVersion + sourceType + } } } `; diff --git a/packages/twenty-front/src/modules/marketplace/graphql/fragments/marketplaceAppFragment.ts b/packages/twenty-front/src/modules/marketplace/graphql/fragments/marketplaceAppFragment.ts index d4e6f625389..6ebbe4afdb1 100644 --- a/packages/twenty-front/src/modules/marketplace/graphql/fragments/marketplaceAppFragment.ts +++ b/packages/twenty-front/src/modules/marketplace/graphql/fragments/marketplaceAppFragment.ts @@ -49,6 +49,7 @@ export const MARKETPLACE_APP_FRAGMENT = gql` name description } + sourcePackage defaultRole { id label diff --git a/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts b/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts index 223be5ab51d..aba1aaf25bc 100644 --- a/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts +++ b/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts @@ -1,7 +1,13 @@ import gql from 'graphql-tag'; export const INSTALL_MARKETPLACE_APP = gql` - mutation InstallMarketplaceApp() { - installMarketplaceApp() + mutation InstallMarketplaceApp( + $universalIdentifier: String! + $version: String + ) { + installMarketplaceApp( + universalIdentifier: $universalIdentifier + version: $version + ) } `; diff --git a/packages/twenty-front/src/modules/marketplace/graphql/mutations/installNpmApp.ts b/packages/twenty-front/src/modules/marketplace/graphql/mutations/installNpmApp.ts new file mode 100644 index 00000000000..575a7dea763 --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/graphql/mutations/installNpmApp.ts @@ -0,0 +1,7 @@ +import gql from 'graphql-tag'; + +export const INSTALL_NPM_APP = gql` + mutation InstallNpmApp($packageName: String!, $version: String) { + installNpmApp(packageName: $packageName, version: $version) + } +`; diff --git a/packages/twenty-front/src/modules/marketplace/graphql/mutations/upgradeApplication.ts b/packages/twenty-front/src/modules/marketplace/graphql/mutations/upgradeApplication.ts new file mode 100644 index 00000000000..fd2eb6d883b --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/graphql/mutations/upgradeApplication.ts @@ -0,0 +1,13 @@ +import gql from 'graphql-tag'; + +export const UPGRADE_APPLICATION = gql` + mutation UpgradeApplication( + $appRegistrationId: String! + $targetVersion: String! + ) { + upgradeApplication( + appRegistrationId: $appRegistrationId + targetVersion: $targetVersion + ) + } +`; diff --git a/packages/twenty-front/src/modules/marketplace/graphql/mutations/uploadAppTarball.ts b/packages/twenty-front/src/modules/marketplace/graphql/mutations/uploadAppTarball.ts new file mode 100644 index 00000000000..05d3911683a --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/graphql/mutations/uploadAppTarball.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const UPLOAD_APP_TARBALL = gql` + mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) { + uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) { + id + universalIdentifier + name + } + } +`; diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useInstallApp.ts b/packages/twenty-front/src/modules/marketplace/hooks/useInstallApp.ts new file mode 100644 index 00000000000..31570c632fd --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/hooks/useInstallApp.ts @@ -0,0 +1,41 @@ +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'; + +export const useInstallApp = >( + mutationFn: (options: { + variables: TVariables; + }) => Promise<{ data?: Record | null }>, +) => { + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); + const [isInstalling, setIsInstalling] = useState(false); + + const install = async (variables: TVariables) => { + setIsInstalling(true); + + try { + const result = await mutationFn({ variables }); + + if (isDefined(result.data)) { + enqueueSuccessSnackBar({ + message: t`Application installed successfully.`, + }); + + return true; + } + + return false; + } catch { + enqueueErrorSnackBar({ + message: t`Failed to install the application.`, + }); + + return false; + } finally { + setIsInstalling(false); + } + }; + + return { install, isInstalling }; +}; diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts b/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts index c629422f20a..a69c63c05b3 100644 --- a/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts +++ b/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts @@ -1,9 +1,11 @@ -export const useInstallMarketplaceApp = () => { - const install = async () => { - return true; - }; +import { useInstallApp } from '~/modules/marketplace/hooks/useInstallApp'; +import { useInstallMarketplaceAppMutation } from '~/generated-metadata/graphql'; - return { - install, - }; +export const useInstallMarketplaceApp = () => { + const [installMarketplaceAppMutation] = useInstallMarketplaceAppMutation(); + + return useInstallApp<{ + universalIdentifier: string; + version?: string; + }>(installMarketplaceAppMutation); }; diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useInstallNpmApp.ts b/packages/twenty-front/src/modules/marketplace/hooks/useInstallNpmApp.ts new file mode 100644 index 00000000000..9168b8a596f --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/hooks/useInstallNpmApp.ts @@ -0,0 +1,13 @@ +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); +}; diff --git a/packages/twenty-front/src/pages/settings/applications/hooks/useMarketplaceApps.ts b/packages/twenty-front/src/modules/marketplace/hooks/useMarketplaceApps.ts similarity index 100% rename from packages/twenty-front/src/pages/settings/applications/hooks/useMarketplaceApps.ts rename to packages/twenty-front/src/modules/marketplace/hooks/useMarketplaceApps.ts diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useUpgradeApplication.ts b/packages/twenty-front/src/modules/marketplace/hooks/useUpgradeApplication.ts new file mode 100644 index 00000000000..af1ff1e9027 --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/hooks/useUpgradeApplication.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useUploadAppTarball.ts b/packages/twenty-front/src/modules/marketplace/hooks/useUploadAppTarball.ts new file mode 100644 index 00000000000..dc96f5713d5 --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/hooks/useUploadAppTarball.ts @@ -0,0 +1,54 @@ +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 { UPLOAD_APP_TARBALL } from '~/modules/marketplace/graphql/mutations/uploadAppTarball'; + +type UploadResult = + | { + success: true; + universalIdentifier: string; + } + | { + success: false; + }; + +export const useUploadAppTarball = () => { + const [uploadAppTarball] = useMutation(UPLOAD_APP_TARBALL); + const { enqueueErrorSnackBar } = useSnackBar(); + const [isUploading, setIsUploading] = useState(false); + + const upload = async (file: File): Promise => { + setIsUploading(true); + + try { + const result = await uploadAppTarball({ + variables: { file }, + }); + + const registration = result.data?.uploadAppTarball; + + if (!isDefined(registration?.universalIdentifier)) { + enqueueErrorSnackBar({ message: t`Upload failed.` }); + + return { success: false }; + } + + return { + success: true, + universalIdentifier: registration.universalIdentifier, + }; + } catch { + enqueueErrorSnackBar({ + message: t`Failed to upload tarball.`, + }); + + return { success: false }; + } finally { + setIsUploading(false); + } + }; + + return { upload, isUploading }; +}; diff --git a/packages/twenty-front/src/modules/settings/application-registrations/graphql/fragments/applicationRegistrationFragment.ts b/packages/twenty-front/src/modules/settings/application-registrations/graphql/fragments/applicationRegistrationFragment.ts index e9756655e61..f129e72cecb 100644 --- a/packages/twenty-front/src/modules/settings/application-registrations/graphql/fragments/applicationRegistrationFragment.ts +++ b/packages/twenty-front/src/modules/settings/application-registrations/graphql/fragments/applicationRegistrationFragment.ts @@ -11,6 +11,9 @@ export const APPLICATION_REGISTRATION_FRAGMENT = gql` oAuthClientId oAuthRedirectUris oAuthScopes + sourceType + sourcePackage + latestAvailableVersion websiteUrl termsUrl createdAt diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx index 038c0d9f89a..96fb23b0d16 100644 --- a/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx +++ b/packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx @@ -28,7 +28,7 @@ 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 { useMarketplaceApps } from '~/pages/settings/applications/hooks/useMarketplaceApps'; +import { useMarketplaceApps } from '~/modules/marketplace/hooks/useMarketplaceApps'; import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab'; import { SettingsAvailableApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab'; @@ -248,7 +248,7 @@ export const SettingsAvailableApplicationDetails = () => { const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0); const { data: marketplaceApps } = useMarketplaceApps(); - const { install } = useInstallMarketplaceApp(); + const { install, isInstalling } = useInstallMarketplaceApp(); const canInstallMarketplaceApps = useHasPermissionFlag( PermissionFlagType.MARKETPLACE_APPS, ); @@ -259,7 +259,9 @@ export const SettingsAvailableApplicationDetails = () => { const handleInstall = async () => { if (isDefined(application)) { - await install(); + await install({ + universalIdentifier: application.id, + }); } }; @@ -460,10 +462,11 @@ export const SettingsAvailableApplicationDetails = () => { {canInstallMarketplaceApps && (