3ee93b5ec9
## Context When an object is created via the metadata API, `createOneObject` creates its side-effect entities (INDEX view + viewFields, indexes, navigation menu item, "go to" command menu item, record-page fields view, page layout/tabs/widgets) across **three separate `validateBuildAndRunWorkspaceMigration` calls**, purely because the protection behavior (mutations → overrides, delete → deactivate, reset → reactivate) was keyed on *"owned by the standard app"*, forcing the side effects into batches with different application owners. This misrepresents ownership and breaks atomicity. This PR separates two orthogonal concepts: - **Ownership** (`applicationId`), the true owner: the caller's application (the workspace custom app today, 3rd-party apps later). - **Protection** (`isSystemSideEffect`), the row was generated by the system, so user mutations route to overrides, deletion becomes deactivation, and reset restores defaults. Once side effects are re-owned to the caller, the old `applicationId === standardApp` check can no longer tell an original side-effect row from a user-added one so a dedicated `isSystemSideEffect` flag carries the protection instead. This is **PR 1 of 2** (forward-only). It makes newly created objects and fields correct; existing workspaces are handled by a follow-up backfill (see *Out of scope*). ## What this PR does - **`isSystemSideEffect` column** on the 8 affected entities (`view`, `viewField`, `indexMetadata`, `commandMenuItem`, `pageLayout`, `pageLayoutTab`, `pageLayoutWidget`, `fieldMetadata`), with `@WasIntroducedInUpgrade` + an entry in the flat-entity property configuration (`toCompare: true`, read-only). - **Single atomic migration in `createOneObject`**: the three `validateBuildAndRunWorkspaceMigration` calls are merged into one, owned by the caller (`resolvedOwnerFlatApplication`) and the record-page view/fields, page layout, and navigation command item are re-owned to the caller and flagged `isSystemSideEffect: true`. `buildNavigationFlatCommandMenuItem` is parameterized with `applicationUniversalIdentifier` (no longer hardcoded to the standard app). - **Field-creation side effects** (`createManyFields`/`createOneField` already run as a single caller-owned migration, so no re-ownership/merge was needed): the auto-created viewField is flagged `isSystemSideEffect: true`, and a new field now also propagates to the object's **INDEX/table view** (added there as a **hidden** column, `isVisible: false`) in addition to the record-page FIELDS widget. The INDEX view is targeted directly by `key = INDEX` (it is not a page-layout widget), de-duplicated per `(viewId, fieldMetadataUniversalIdentifier)` to respect the per-view unique index. The unique-field index is likewise flagged the inverse relation field stays unflagged (`isSystem: false`). - **Protection predicate** extended: `isCallerOverridingEntity` and the removal/reset split strategies now treat `isSystemSideEffect` rows as protected even when caller-owned (route to overrides / deactivate / reset) and the page-layout-reset guards allow resetting flagged entities. - **Standard compute maps** set the flag consistently so a re-sync produces no diff (standard-object side effects stay `false`; per-object nav command items and custom-object base fields are `true`). - **Read-only GraphQL exposure** of `isSystemSideEffect` on the view / view-field / page-layout / tab / widget / command-menu-item DTOs (not exposed on create/update inputs). => Todo: needs to take this new flag into account. This is fine for now because isSystem remains on object/field. - **Fast instance command** (`2-14`) adding the 8 columns (`NOT NULL DEFAULT false`). ## Scope decisions - **`pageLayout` is not an `OverridableEntity`**, its own row has nothing user-overridable (all customization lives on tabs/widgets). It's dual-purpose (`RECORD_PAGE` side-effect vs. user `DASHBOARD`), so it gets `isSystemSideEffect` for protection only, no `overrides` jsonb. - **`navigationMenuItem` is out of scope.**: Those are side effects only for the metadata API and not marked as "system" (they can be deleted/updated etc...) - **`viewFieldGroup` is not a side effect**, it's only created via the explicit view-field-group API, never by object/field creation, so it gets no flag. ## Out of scope (follow-ups) **PR 2** — slow per-workspace backfill (re-own + flag existing side effects, recreate missing ones) and deterministic v5 identifiers for base fields / pageLayout / tab. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21673?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
129 lines
4.9 KiB
TypeScript
129 lines
4.9 KiB
TypeScript
import { v4 } from 'uuid';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
|
|
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
|
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
|
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
|
import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
|
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
|
import {
|
|
getSeedFrontComponentCommandMenuItemDefinitions,
|
|
getSeedFrontComponentIds,
|
|
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-definitions.util';
|
|
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
|
|
|
export const prefillFrontComponentCommandMenuItems = async ({
|
|
workspaceId,
|
|
applicationService,
|
|
flatEntityMapsCacheService,
|
|
workspaceMigrationValidateBuildAndRunService,
|
|
}: {
|
|
workspaceId: string;
|
|
applicationService: ApplicationService;
|
|
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService;
|
|
workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService;
|
|
}): Promise<void> => {
|
|
const { helloWorldId } = getSeedFrontComponentIds(workspaceId);
|
|
|
|
const { flatCommandMenuItemMaps, flatFrontComponentMaps } =
|
|
await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
|
|
workspaceId,
|
|
flatMapsKeys: ['flatCommandMenuItemMaps', 'flatFrontComponentMaps'],
|
|
});
|
|
|
|
const frontComponent = findFlatEntityByIdInFlatEntityMaps({
|
|
flatEntityId: helloWorldId,
|
|
flatEntityMaps: flatFrontComponentMaps,
|
|
});
|
|
|
|
if (!isDefined(frontComponent)) {
|
|
return;
|
|
}
|
|
|
|
const { workspaceCustomFlatApplication } =
|
|
await applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
{ workspaceId },
|
|
);
|
|
|
|
const definitions =
|
|
getSeedFrontComponentCommandMenuItemDefinitions(workspaceId);
|
|
|
|
const now = new Date().toISOString();
|
|
|
|
const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = definitions
|
|
.filter(
|
|
(definition) =>
|
|
!isDefined(
|
|
flatCommandMenuItemMaps.byUniversalIdentifier[
|
|
definition.universalIdentifier
|
|
],
|
|
),
|
|
)
|
|
.map((definition) => {
|
|
const definitionFrontComponent = findFlatEntityByIdInFlatEntityMaps({
|
|
flatEntityId: definition.frontComponentId,
|
|
flatEntityMaps: flatFrontComponentMaps,
|
|
});
|
|
|
|
return {
|
|
id: v4(),
|
|
universalIdentifier: definition.universalIdentifier,
|
|
applicationId: frontComponent.applicationId,
|
|
applicationUniversalIdentifier:
|
|
frontComponent.applicationUniversalIdentifier,
|
|
workspaceId,
|
|
workflowVersionId: null,
|
|
frontComponentId: definition.frontComponentId,
|
|
frontComponentUniversalIdentifier:
|
|
definitionFrontComponent?.universalIdentifier ?? null,
|
|
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
|
|
label: definition.label,
|
|
icon: definition.icon,
|
|
shortLabel: definition.label,
|
|
position: definition.position,
|
|
isPinned: definition.isPinned ?? false,
|
|
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
|
conditionalAvailabilityExpression: null,
|
|
availabilityObjectMetadataId: null,
|
|
availabilityObjectMetadataUniversalIdentifier: null,
|
|
payload: null,
|
|
hotKeys: null,
|
|
pageLayoutId: definition.pageLayoutId ?? null,
|
|
pageLayoutUniversalIdentifier: definition.pageLayoutId ?? null,
|
|
isActive: true,
|
|
isSystemSideEffect: false,
|
|
overrides: null,
|
|
universalOverrides: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
});
|
|
|
|
if (flatCommandMenuItemsToCreate.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const result =
|
|
await workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
|
{
|
|
allFlatEntityOperationByMetadataName: {
|
|
commandMenuItem: {
|
|
flatEntityToCreate: flatCommandMenuItemsToCreate,
|
|
flatEntityToDelete: [],
|
|
flatEntityToUpdate: [],
|
|
},
|
|
},
|
|
workspaceId,
|
|
applicationUniversalIdentifier:
|
|
workspaceCustomFlatApplication.universalIdentifier,
|
|
},
|
|
);
|
|
|
|
if (result.status === 'fail') {
|
|
throw new Error(
|
|
`Failed to create front component command menu items for workspace ${workspaceId}: ${JSON.stringify(result, null, 2)}`,
|
|
);
|
|
}
|
|
};
|