Fix SDK plan on non installed apps (#22805)
# 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).
This commit is contained in:
+49
-13
@@ -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<ApplicationEntity> {
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
{
|
||||
}): Promise<FlatApplication> {
|
||||
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
|
||||
|
||||
+5
-3
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user