From 666ceb41d5f2c7e9225cfc934769a8aff1bd30a3 Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 10 Jul 2026 15:11:46 +0200 Subject: [PATCH] Fix SDK plan on non installed apps (#22805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Context `yarn twenty plan` fails when the app has never been installed in the target workspace: ``` Sync failed with error: Application "f5ce204f-..." is not installed in workspace "10f39a9d-...". Install it first. Hint: run `yarn twenty dev --once` to register the app in this workspace, then retry. ``` This forces developers to apply before they can plan, which defeats the purpose of `plan`. Planning a not-yet-installed app is well defined: the from-state is empty, so the plan is simply "create everything". ## Why it failed The dry-run sync required the application row to exist in two places: 1. `ApplicationSyncService.synchronizeFromManifest` threw `APP_NOT_INSTALLED` when the app row was missing, because the dry-run needs an owner `FlatApplication` to anchor the from → to metadata diff. 2. `WorkspaceMigrationFlatEntityMapsService.computeAllInvolvedApplicationIds` threw when the owner app id was absent from `flatApplicationMaps`, even though the only hard dependency of a build is the twenty standard application. `apply` never hit this because it registers the app row as a side effect before syncing (and even swallows this exact error on its pre-apply plan). # What this PR does Keeps `plan` strictly read-only, no registration or app row is created: - **`application-sync.service.ts`**: on dry-run, resolve the owner to the installed application when it exists (unchanged behavior), otherwise build a virtual, non-persisted `FlatApplication` from the manifest. Its freshly generated id matches no existing metadata, so the from-state slice resolves to empty and every manifest entity shows up as a create. - **`workspace-migration-flat-entity-maps.service.ts`**: relax the guard so only the twenty standard application is required. A missing owner app just contributes an empty from-slice instead of throwing. Installed apps take the exact same path as before (`applicationId` defined → identical behavior). --- .../application-sync.service.ts | 62 +++++++++++++++---- ...pace-migration-flat-entity-maps.service.ts | 8 ++- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts index 9fe830d885..7f62ac167d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts @@ -5,7 +5,9 @@ import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { PackageJson } from 'type-fest'; +import { v4 } from 'uuid'; +import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum'; import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service'; import { enrichApplicationManifestSyncError } from 'src/engine/core-modules/application/application-manifest/utils/enrich-application-manifest-sync-error.util'; import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util'; @@ -59,7 +61,7 @@ export class ApplicationSyncService { hasSchemaMetadataChanged: boolean; }> { const ownerFlatApplication: FlatApplication = dryRun - ? await this.findInstalledApplicationOrThrow({ workspaceId, manifest }) + ? await this.resolveDryRunOwnerFlatApplication({ workspaceId, manifest }) : await this.syncApplication({ workspaceId, manifest, @@ -110,28 +112,62 @@ export class ApplicationSyncService { return syncResult; } - private async findInstalledApplicationOrThrow({ + private async resolveDryRunOwnerFlatApplication({ workspaceId, manifest, }: { workspaceId: string; manifest: Manifest; - }): Promise { - const application = await this.applicationService.findByUniversalIdentifier( - { + }): Promise { + const installedApplication = + await this.applicationService.findByUniversalIdentifier({ universalIdentifier: manifest.application.universalIdentifier, workspaceId, - }, + }); + + return ( + installedApplication ?? + this.buildVirtualDryRunFlatApplication({ manifest, workspaceId }) ); + } - if (!application) { - throw new ApplicationException( - `Application "${manifest.application.universalIdentifier}" is not installed in workspace "${workspaceId}". Install it first.`, - ApplicationExceptionCode.APP_NOT_INSTALLED, - ); - } + private buildVirtualDryRunFlatApplication({ + manifest, + workspaceId, + }: { + manifest: Manifest; + workspaceId: string; + }): FlatApplication { + const now = new Date(); - return application; + return { + id: v4(), + workspaceId, + universalIdentifier: manifest.application.universalIdentifier, + name: manifest.application.displayName, + description: manifest.application.description ?? null, + logo: manifest.application.logoUrl ?? null, + logoFileId: null, + version: null, + sourceType: ApplicationRegistrationSourceType.LOCAL, + sourcePath: manifest.application.universalIdentifier, + packageJsonChecksum: null, + packageJsonFileId: null, + yarnLockChecksum: null, + yarnLockFileId: null, + availablePackages: {}, + logicFunctionLayerId: null, + defaultRoleId: null, + defaultRole: null, + settingsCustomTabFrontComponentId: null, + canBeUninstalled: true, + isSdkLayerStale: false, + applicationRegistrationId: null, + primaryPublicDomainId: null, + createdAt: now, + updatedAt: now, + deletedAt: null, + }; } // Registers the application + only the pre-install logic function in diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-flat-entity-maps.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-flat-entity-maps.service.ts index 855cce0ec0..116e1c0079 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-flat-entity-maps.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-flat-entity-maps.service.ts @@ -247,14 +247,16 @@ export class WorkspaceMigrationFlatEntityMapsService { TWENTY_STANDARD_APPLICATION.universalIdentifier ]; - if (!isDefined(twentyStandardApplicationId) || !isDefined(applicationId)) { + if (!isDefined(twentyStandardApplicationId)) { throw new FlatEntityMapsException( - 'Application to build and its dependent application not found', + 'Twenty standard application not found in workspace', FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, ); } - applicationIds.add(applicationId); + if (isDefined(applicationId)) { + applicationIds.add(applicationId); + } const isBuildingTwentyStandardApplication = applicationUniversalIdentifier ===