feat(app-dev): add dry-run preview to dev sync (#21251)
Split out of #21240. Stacked on #21250 (review/merge that first). `yarn twenty dev --once --dry-run` computes the migration plan and prints the diff **without applying anything** (no migration, no app-record update, no SDK generation). Also renders the diff on a normal `dev --once` sync. <img width="646" height="179" alt="image" src="https://github.com/user-attachments/assets/59f3ddcd-2a5b-4b8a-b21a-c659abe16af0" />
This commit is contained in:
+16
-1
@@ -133,7 +133,7 @@ export class ApplicationDevelopmentResolver {
|
||||
|
||||
@Mutation(() => WorkspaceMigrationDTO)
|
||||
async syncApplication(
|
||||
@Args() { manifest }: ApplicationInput,
|
||||
@Args() { manifest, dryRun }: ApplicationInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<WorkspaceMigrationDTO> {
|
||||
await this.throttlePerApplication(
|
||||
@@ -141,6 +141,21 @@ export class ApplicationDevelopmentResolver {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (dryRun === true) {
|
||||
const { workspaceMigration } =
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId,
|
||||
manifest,
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
return {
|
||||
applicationUniversalIdentifier:
|
||||
workspaceMigration.applicationUniversalIdentifier,
|
||||
actions: workspaceMigration.actions,
|
||||
};
|
||||
}
|
||||
|
||||
return this.cacheLockService.withLock(
|
||||
() => this.applyManifestSync(manifest, workspaceId),
|
||||
`app-sync:${workspaceId}`,
|
||||
|
||||
+3
@@ -7,4 +7,7 @@ import { Manifest } from 'twenty-shared/application';
|
||||
export class ApplicationInput {
|
||||
@Field(() => GraphQLJSON, { nullable: false })
|
||||
manifest: Manifest;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
+11
-6
@@ -162,10 +162,12 @@ export class ApplicationManifestMigrationService {
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
dryRun = false,
|
||||
}: {
|
||||
manifest: Manifest;
|
||||
workspaceId: string;
|
||||
ownerFlatApplication: FlatApplication;
|
||||
dryRun?: boolean;
|
||||
}): Promise<{
|
||||
workspaceMigration: WorkspaceMigration;
|
||||
hasSchemaMetadataChanged: boolean;
|
||||
@@ -225,6 +227,7 @@ export class ApplicationManifestMigrationService {
|
||||
workspaceId,
|
||||
dependencyAllFlatEntityMaps,
|
||||
additionalCacheDataMaps: { featureFlagsMap },
|
||||
dryRun,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -236,14 +239,16 @@ export class ApplicationManifestMigrationService {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Metadata migration completed for application ${ownerFlatApplication.universalIdentifier}`,
|
||||
`Metadata migration ${dryRun ? 'plan computed (dry run)' : 'completed'} for application ${ownerFlatApplication.universalIdentifier}`,
|
||||
);
|
||||
|
||||
await this.syncDefaultRoleAndSettingsCustomTab({
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
if (!dryRun) {
|
||||
await this.syncDefaultRoleAndSettingsCustomTab({
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceMigration: validateAndBuildResult.workspaceMigration,
|
||||
|
||||
+18
-24
@@ -41,30 +41,38 @@ export class ApplicationSyncService {
|
||||
workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
dryRun = false,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
manifest: Manifest;
|
||||
applicationRegistrationId?: string;
|
||||
dryRun?: boolean;
|
||||
}): Promise<{
|
||||
workspaceMigration: WorkspaceMigration;
|
||||
hasSchemaMetadataChanged: boolean;
|
||||
}> {
|
||||
const application = await this.syncApplication({
|
||||
workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
});
|
||||
|
||||
const ownerFlatApplication: FlatApplication = application;
|
||||
const ownerFlatApplication: FlatApplication = dryRun
|
||||
? await this.applicationService.findOneApplicationOrThrow({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
})
|
||||
: await this.syncApplication({
|
||||
workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
});
|
||||
|
||||
const syncResult =
|
||||
await this.applicationManifestMigrationService.syncMetadataFromManifest({
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
this.logger.log('Application sync from manifest completed');
|
||||
this.logger.log(
|
||||
`Application sync from manifest ${dryRun ? 'plan computed (dry run)' : 'completed'}`,
|
||||
);
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
@@ -129,20 +137,13 @@ export class ApplicationSyncService {
|
||||
).toString('utf-8'),
|
||||
) as PackageJson;
|
||||
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
const application = await this.applicationService.findOneApplicationOrThrow(
|
||||
{
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (!application) {
|
||||
throw new ApplicationException(
|
||||
`Application "${manifest.application.universalIdentifier}" is not installed in workspace "${workspaceId}". Install it first.`,
|
||||
ApplicationExceptionCode.APP_NOT_INSTALLED,
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedRegistrationId =
|
||||
applicationRegistrationId ?? application.applicationRegistrationId;
|
||||
|
||||
@@ -165,17 +166,10 @@ export class ApplicationSyncService {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<WorkspaceMigration> {
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
const application = await this.applicationService.findOneApplicationOrThrow(
|
||||
{ universalIdentifier: applicationUniversalIdentifier, workspaceId },
|
||||
);
|
||||
|
||||
if (!isDefined(application)) {
|
||||
throw new ApplicationException(
|
||||
`Application with universalIdentifier ${applicationUniversalIdentifier} not found`,
|
||||
ApplicationExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!application.canBeUninstalled) {
|
||||
throw new ApplicationException(
|
||||
'This application cannot be uninstalled.',
|
||||
|
||||
Reference in New Issue
Block a user