Add workspace DDL lock env var and maintenance mode UI (#19130)
## Summary - Add `WORKSPACE_SCHEMA_DDL_LOCKED` env-only boolean config variable that blocks all workspace schema DDL changes when set to `true`. This is intended for hot upgrades where logical replication cannot handle DDL changes. Enforced at two chokepoints: - `WorkspaceMigrationRunnerService.run` — blocks all metadata-driven DDL (object/field/index CRUD, app sync/uninstall, standard app sync, upgrade commands) - `WorkspaceDataSourceService.createWorkspaceDBSchema` / `deleteWorkspaceDBSchema` — blocks workspace creation (sign-up) and hard deletion. Uses a dedicated `WorkspaceDataSourceException` (not ForbiddenException) - Add maintenance mode feature with Admin Panel UI and user-facing banner: - **Backend**: `MaintenanceModeService` stores maintenance window (startAt, endAt, optional link) in `core.keyValuePair` as `CONFIG_VARIABLE`. Validates endAt > startAt. Uses `GraphQLISODateTime` scalar for date fields. Exposed via `clientConfig` REST endpoint and admin GraphQL mutations (`setMaintenanceMode`, `clearMaintenanceMode`) - **Admin Panel**: New "Maintenance Mode" section in Health tab with UTC datetime pickers and activate/deactivate controls - **Banner**: `InformationBannerMaintenance` displayed at the top of `DefaultLayout` for all users, using Temporal API for timezone-aware formatting with an optional "Learn more" link These two features are **independent** — the DDL lock is controlled via env var for operational use, while maintenance mode is a UI notification mechanism controlled from the admin panel.
This commit is contained in:
+3
@@ -8,6 +8,7 @@ export const WorkspaceMigrationRunnerExceptionCode = {
|
||||
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
||||
EXECUTION_FAILED: 'EXECUTION_FAILED',
|
||||
APPLICATION_NOT_FOUND: 'APPLICATION_NOT_FOUND',
|
||||
DDL_LOCKED: 'DDL_LOCKED',
|
||||
} as const;
|
||||
|
||||
const getWorkspaceMigrationRunnerExceptionUserFriendlyMessage = (
|
||||
@@ -20,6 +21,8 @@ const getWorkspaceMigrationRunnerExceptionUserFriendlyMessage = (
|
||||
return msg`Migration execution failed.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.APPLICATION_NOT_FOUND:
|
||||
return msg`Application not found.`;
|
||||
case WorkspaceMigrationRunnerExceptionCode.DDL_LOCKED:
|
||||
return msg`Workspace schema changes are temporarily locked.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+10
@@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
@@ -35,6 +36,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
private getLegacyCacheInvalidationPromises({
|
||||
@@ -168,6 +170,14 @@ export class WorkspaceMigrationRunnerService {
|
||||
metadataEvents: MetadataEvent[];
|
||||
hasSchemaMetadataChanged: boolean;
|
||||
}> => {
|
||||
if (this.twentyConfigService.get('WORKSPACE_SCHEMA_DDL_LOCKED')) {
|
||||
throw new WorkspaceMigrationRunnerException({
|
||||
message:
|
||||
'Workspace schema DDL changes are locked. This is typically set during hot upgrades.',
|
||||
code: WorkspaceMigrationRunnerExceptionCode.DDL_LOCKED,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.time('Runner', 'Total execution');
|
||||
this.logger.time('Runner', 'Initial cache retrieval');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user