88424611ec
# Introduction ## Centralize system field definitions - Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`), eliminating duplication across custom object and standard app field builders - Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper ## Mark system fields as `isSystem: true` - Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` are now properly flagged as system fields across all standard objects and custom object creation - Standard app field builders for all ~30 standard objects updated to set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy` - System-only standard objects (blocklist, calendar channels, message threads, etc.) now also include `createdBy`, `updatedBy`, `position`, `searchVector` field definitions that were previously missing ## Validate system fields on object creation - New transversal validation (`crossEntityTransversalValidation`) runs after all atomic entity validations in the build orchestrator, ensuring all 8 system fields are present with correct `type` and `isSystem: true` when an object is created - New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to resolve field names to universal identifiers for a given object - New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD` on `ObjectMetadataExceptionCode` ## Protect system fields and objects from mutation - Field validators now block update/delete of `isSystem` fields by non-system callers (`FIELD_MUTATION_NOT_ALLOWED`) - Object validators now block update/delete of `isSystem` objects by non-system callers - `POSITION` and `TS_VECTOR` field type validators replaced: instead of rejecting creation outright, they now validate that the field is named correctly (`position` / `searchVector`) and has `isSystem: true` ## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp` - New `isCallerTwentyStandardApp` utility checks whether the caller's `applicationUniversalIdentifier` matches the twenty standard app - Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`, `areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use `isCallerTwentyStandardApp` for custom suffix decisions, keeping `isSystemBuild` for mutation permission checks - `WorkspaceMigrationBuilderOptions` type updated to include `applicationUniversalIdentifier` ## Adapt frontend filtering - New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`, `searchVector`) and `isHiddenSystemField` utility to only hide truly internal fields while keeping user-facing system fields (`createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI - ~20 frontend files updated to replace `!field.isSystem` checks with `!isHiddenSystemField(field)` across record index, settings, data model, charts, workflows, spreadsheet import, aggregations, and role permissions ## Add 1.19 upgrade commands - **`backfill-system-fields-is-system`**: Raw SQL command to set `isSystem = true` on existing workspace fields matching system field names, and fix `position` field type from `NUMBER` to `POSITION` for `favorite`/`favoriteFolder` objects. Includes proper cache invalidation. - **`add-missing-system-fields-to-standard-objects`**: Codegen'd workspace migration to create missing `position`, `searchVector`, `createdBy`, `updatedBy` fields on standard objects that didn't previously have them. Runs via `WorkspaceMigrationRunnerService` in a single transaction with idempotency check. **Known limitation**: assumes all standard objects exist and are valid in the target workspace. ## Add `universalIdentifier` for system fields in standard object constants - `standard-object.constant.ts` updated to include `universalIdentifier` for `createdBy`, `updatedBy`, `position`, and `searchVector` across all standard objects - `fieldManifestType.ts` updated to support the new field manifest shape ## System relation Completely removed and backfilled all `isSystem` relation to be false false As we won't require an object to have any relation system fields ## Add integration tests - New test suite `failing-sync-application-object-system-fields` covering: missing system fields, wrong field types (`id` as TEXT, `createdAt` as TEXT, `position` as TEXT), system field deletion attempts, and system field update attempts - New test utilities: `buildDefaultObjectManifest` (builds an object manifest with all 8 system fields) and `setupApplicationForSync` (centralizes application setup) - Existing successful sync test updated to verify system fields are created with correct properties ## Next step Make the builder scope the compared entity to be the currently built app + nor twenty standard app
170 lines
7.0 KiB
TypeScript
170 lines
7.0 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
|
|
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
|
import { DataSource } from 'typeorm';
|
|
|
|
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
|
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
|
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
import { SeededWorkspacesIds } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
|
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
|
|
import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util';
|
|
import { seedFrontComponentsAndCommandMenuItems } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-front-components-and-command-menu-items.util';
|
|
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
|
|
import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util';
|
|
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
|
|
import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service';
|
|
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
|
|
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
|
|
|
|
@Injectable()
|
|
export class DevSeederService {
|
|
constructor(
|
|
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
|
private readonly twentyConfigService: TwentyConfigService,
|
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
private readonly dataSourceService: DataSourceService,
|
|
private readonly twentyStandardApplicationService: TwentyStandardApplicationService,
|
|
private readonly devSeederMetadataService: DevSeederMetadataService,
|
|
private readonly devSeederPermissionsService: DevSeederPermissionsService,
|
|
private readonly devSeederDataService: DevSeederDataService,
|
|
private readonly applicationService: ApplicationService,
|
|
private readonly workspaceCacheService: WorkspaceCacheService,
|
|
@InjectDataSource()
|
|
private readonly coreDataSource: DataSource,
|
|
) {}
|
|
|
|
public async seedDev(workspaceId: SeededWorkspacesIds): Promise<void> {
|
|
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
|
|
const appVersion = this.twentyConfigService.get('APP_VERSION');
|
|
|
|
await seedCoreSchema({
|
|
dataSource: this.coreDataSource,
|
|
workspaceId,
|
|
applicationService: this.applicationService,
|
|
seedBilling: isBillingEnabled,
|
|
appVersion,
|
|
});
|
|
|
|
const schemaName =
|
|
await this.workspaceDataSourceService.createWorkspaceDBSchema(
|
|
workspaceId,
|
|
);
|
|
|
|
const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute(
|
|
workspaceId,
|
|
['flatApplicationMaps', 'featureFlagsMap'],
|
|
);
|
|
|
|
const dataSourceMetadata =
|
|
await this.dataSourceService.createDataSourceMetadata(
|
|
workspaceId,
|
|
schemaName,
|
|
);
|
|
|
|
const { workspaceCustomFlatApplication, twentyStandardFlatApplication } =
|
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
await this.twentyStandardApplicationService.synchronizeTwentyStandardApplicationOrThrow(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
await this.devSeederMetadataService.seed({
|
|
dataSourceMetadata,
|
|
workspaceId,
|
|
});
|
|
|
|
await this.devSeederMetadataService.seedRelations({
|
|
workspaceId,
|
|
});
|
|
|
|
await this.devSeederPermissionsService.initPermissions({
|
|
workspaceId,
|
|
twentyStandardFlatApplication,
|
|
workspaceCustomFlatApplication,
|
|
});
|
|
|
|
await seedPageLayouts(
|
|
this.coreDataSource,
|
|
'core',
|
|
workspaceId,
|
|
workspaceCustomFlatApplication.id,
|
|
);
|
|
await seedPageLayoutTabs({
|
|
applicationId: workspaceCustomFlatApplication.id,
|
|
workspaceId,
|
|
dataSource: this.coreDataSource,
|
|
schemaName: 'core',
|
|
});
|
|
|
|
const objectMetadataRepository =
|
|
this.coreDataSource.getRepository(ObjectMetadataEntity);
|
|
const objectMetadataItems = await objectMetadataRepository.find({
|
|
where: { workspaceId },
|
|
relations: { fields: true },
|
|
});
|
|
|
|
const isDashboardV2Enabled =
|
|
featureFlagsMap[FeatureFlagKey.IS_DASHBOARD_V2_ENABLED] ?? false;
|
|
|
|
await seedPageLayoutWidgets({
|
|
dataSource: this.coreDataSource,
|
|
schemaName: 'core',
|
|
workspaceId,
|
|
objectMetadataItems,
|
|
isDashboardV2Enabled,
|
|
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
|
});
|
|
|
|
const relatedPageLayoutCacheKeysToInvalidate = [
|
|
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.pageLayout),
|
|
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.pageLayoutTab),
|
|
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.pageLayoutWidget),
|
|
].map(getMetadataFlatEntityMapsKey);
|
|
|
|
await this.workspaceCacheService.invalidateAndRecompute(
|
|
workspaceId,
|
|
relatedPageLayoutCacheKeysToInvalidate,
|
|
);
|
|
|
|
await this.devSeederDataService.seed({
|
|
schemaName: dataSourceMetadata.schema,
|
|
workspaceId,
|
|
featureFlags: featureFlagsMap,
|
|
});
|
|
|
|
await seedFrontComponentsAndCommandMenuItems({
|
|
dataSource: this.coreDataSource,
|
|
schemaName: 'core',
|
|
workspaceId,
|
|
applicationId: workspaceCustomFlatApplication.id,
|
|
});
|
|
|
|
const relatedCommandMenuItemAndFrontComponentCacheKeysToInvalidate = [
|
|
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.commandMenuItem),
|
|
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.frontComponent),
|
|
].map(getMetadataFlatEntityMapsKey);
|
|
|
|
await this.workspaceCacheService.invalidateAndRecompute(
|
|
workspaceId,
|
|
relatedCommandMenuItemAndFrontComponentCacheKeysToInvalidate,
|
|
);
|
|
|
|
await this.workspaceCacheStorageService.flush(workspaceId, undefined);
|
|
}
|
|
}
|