fix(server): derive system field universalIdentifiers from v5 and auto-attach them in manifest sync

Fixes app re-install on workspaces that have already received the v2.5
`NormalizeCompositeFieldDefaultsCommand` backfill: those workspaces now
have non-null `defaultValue` on every composite system field (createdBy,
updatedBy, position, searchVector, …). The next `installApplication` →
`synchronizeFromManifest(isSystemBuild: false)` rebuilds the diff and
hits `FIELD_MUTATION_NOT_ALLOWED` on each row because the manifest TO
state never declared system fields, so the dispatcher saw them as
FROM-only and emitted bad delete/update actions.

## Root cause

`computeApplicationManifestAllUniversalFlatEntityMaps` walks
`manifest.objects[].fields` and never scaffolds the eight standard
system fields the runtime actually attaches on object creation
(`buildDefaultFlatFieldMetadatasForCustomObject`). On top of that,
the scaffolder seeded each system field's `universalIdentifier` with
`v4()`, so even if we wanted to mirror them on the TO side from the
manifest converter, the random UIDs would never line up with the rows
already in the workspace.

## Fix (three pieces)

1. **Switch `buildDefaultFlatFieldMetadatasForCustomObject` from `v4` to
   `v5`.** Same pattern as `NAVIGATION_COMMAND_UUID_NAMESPACE`: every
   system field's universalIdentifier is derived deterministically from
   `${objectMetadataUniversalIdentifier}/${name}` against a new
   `SYSTEM_FIELD_UUID_NAMESPACE`. Exported as
   `computeSystemFieldUniversalIdentifier` so the manifest converter
   and the upgrade migration both compute identical UUIDs.

2. **Inject the eight system fields into manifest TO.** Inside
   `computeApplicationManifestAllUniversalFlatEntityMaps`, after each
   `flatObjectMetadata` is added, call
   `buildDefaultFlatFieldMetadatasForCustomObject({ skipNameField: true })`
   and merge its eight rows into `flatFieldMetadataMaps`. Apps that
   explicitly declare a field with one of the system names (e.g. an
   `id: TEXT` regression) are left untouched so the existing
   `validateObjectMetadataSystemFieldsIntegrity` validator still
   surfaces the mistake.

3. **2-5 workspace upgrade migration:**
   `2-5-workspace-command-1798600000000-refactor-system-field-universal-identifiers.command.ts`
   rewrites every existing `isSystem: true` field whose name is one of
   the eight standard names to the new v5 identifier (direct
   `UPDATE core."fieldMetadata"` on the `universalIdentifier` column —
   no FK references it, all DB joins are on the per-workspace `id`
   PK), then invalidates the `flatFieldMetadataMaps` and
   `flatObjectMetadataMaps` cache entries. Direct analogue of
   `1-21-workspace-command-1775500013000-refactor-navigation-commands`.

After (1)+(2)+(3) ship, the dispatcher computes an empty diff for
system fields on app re-installs: TO and FROM contain the same eight
rows with the same v5 universalIdentifiers and the same scaffolder
defaults. The validator path is never hit.

## CI

`sync-application-system-fields-auto-attached.integration-spec.ts`
exercises three scenarios that would have caught this regression:

- First sync of an app whose object declares zero system fields ends
  with the eight rows present and v5-addressed.
- Re-syncing the identical manifest is a no-op.
- A re-sync after a manually-cleared `defaultValue` on `createdBy`
  (the exact post-2.5-backfill shape) succeeds — this is the case
  that failed on prod after PR #20581 / the rolling 2.5 upgrade.

The existing dev-sync vitest suite never exercised
`installApplication` directly because the LOCAL sourceType short-
circuits the install handler, so the bug stayed silent until the CD
pipeline hit it on the registry-install path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Charles Bochet
2026-05-15 16:06:20 +02:00
parent 0c20b8bc88
commit de8f2addfb
5 changed files with 416 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { RebuildUniquePhoneIndexesCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-workspace-command-1778000000000-rebuild-unique-phone-indexes.command';
import { NormalizeCompositeFieldDefaultsCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command';
import { RefactorSystemFieldUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-workspace-command-1798600000000-refactor-system-field-universal-identifiers.command';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -17,6 +18,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
providers: [
RebuildUniquePhoneIndexesCommand,
NormalizeCompositeFieldDefaultsCommand,
RefactorSystemFieldUniversalIdentifiersCommand,
],
})
export class V2_5_UpgradeVersionCommandModule {}

View File

@@ -0,0 +1,139 @@
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
import { computeSystemFieldUniversalIdentifier } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
// Names of the eight standard system fields scaffolded by
// `buildDefaultFlatFieldMetadatasForCustomObject` for every custom object.
// Anything outside this set is left untouched, even if `isSystem = true`
// (e.g. relation-source fields the SDK marks system but doesn't scaffold).
const SYSTEM_FIELD_NAMES_TO_REFACTOR = Object.values(
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS,
).map((field) => field.name);
type SystemFieldRow = {
id: string;
current_universal_identifier: string;
object_metadata_universal_identifier: string;
name: string;
};
@RegisteredWorkspaceCommand('2.5.0', 1798600000000)
@Command({
name: 'upgrade:2-5:refactor-system-field-universal-identifiers',
description:
'Rewrite system field universalIdentifiers to v5(`${objectMetadataUniversalIdentifier}/${name}`, SYSTEM_FIELD_UUID_NAMESPACE) so app-install diffs do not see them as changed rows.',
})
export class RefactorSystemFieldUniversalIdentifiersCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
dataSource,
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (!dataSource) {
this.logger.log(`No data source for workspace ${workspaceId}, skipping`);
return;
}
const rows: SystemFieldRow[] = await dataSource.query(
`SELECT fm.id,
fm."universalIdentifier" AS current_universal_identifier,
om."universalIdentifier" AS object_metadata_universal_identifier,
fm.name
FROM core."fieldMetadata" fm
JOIN core."objectMetadata" om ON om.id = fm."objectMetadataId"
WHERE fm."workspaceId" = $1
AND fm."isSystem" = true
AND fm.name = ANY($2)`,
[workspaceId, SYSTEM_FIELD_NAMES_TO_REFACTOR],
);
if (rows.length === 0) {
this.logger.log(
`No system fields to refactor for workspace ${workspaceId}, skipping`,
);
return;
}
const updates = rows
.map((row) => ({
id: row.id,
from: row.current_universal_identifier,
to: computeSystemFieldUniversalIdentifier({
objectMetadataUniversalIdentifier:
row.object_metadata_universal_identifier,
name: row.name,
}),
name: row.name,
}))
.filter((row) => row.from !== row.to);
if (updates.length === 0) {
this.logger.log(
`All ${rows.length} system field universalIdentifiers already match the v5 derivation for workspace ${workspaceId}, skipping`,
);
return;
}
if (options.dryRun) {
this.logger.log(
`[DRY RUN] Would rewrite ${updates.length}/${rows.length} system field universalIdentifier(s) for workspace ${workspaceId}: ${updates
.map((u) => `${u.name}@${u.id} ${u.from} -> ${u.to}`)
.slice(0, 5)
.join(', ')}${updates.length > 5 ? ', ...' : ''}`,
);
return;
}
// We update one row at a time with the FK-safe SET universalIdentifier
// — universalIdentifier is purely the cross-workspace flat-entity
// identifier; no DB foreign key references it (other tables join on
// the per-workspace `id` PK), so a plain UPDATE is safe.
//
// The unique index `(workspaceId, universalIdentifier)` is preserved
// because v5(`${objectMetadataUniversalIdentifier}/${name}`, NAMESPACE)
// is unique per (object, field name) — and a given workspace cannot
// host two custom objects with the same universalIdentifier.
let updatedCount = 0;
for (const update of updates) {
await dataSource.query(
`UPDATE core."fieldMetadata"
SET "universalIdentifier" = $2,
"updatedAt" = NOW()
WHERE id = $1`,
[update.id, update.to],
);
updatedCount += 1;
}
this.logger.log(
`Refactored ${updatedCount} system field universalIdentifier(s) to v5 for workspace ${workspaceId}`,
);
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatFieldMetadataMaps',
'flatObjectMetadataMaps',
]);
}
}
export const _SYSTEM_FIELD_NAMES_TO_REFACTOR_FOR_TEST =
SYSTEM_FIELD_NAMES_TO_REFACTOR;

View File

@@ -3,6 +3,8 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { buildDefaultFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
@@ -61,6 +63,56 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
allUniversalFlatEntityMaps.flatObjectMetadataMaps,
});
// Inject the 8 standard system fields (id / createdAt / createdBy /
// deletedAt / position / searchVector / updatedAt / updatedBy) into
// the manifest-derived TO map. The runtime attaches the same set to
// every custom object on creation; without these here, the diff
// sees `FROM = { …system fields }` and `TO = { }` and either tries
// to delete (rejected by the deletion validator) or update them
// (rejected by `FIELD_MUTATION_NOT_ALLOWED` once any property
// drifts — e.g. after NormalizeCompositeFieldDefaultsCommand).
//
// universalIdentifiers are derived deterministically via v5 from
// `${objectMetadataUniversalIdentifier}/${name}` so they line up
// byte-for-byte with the rows the workspace already has (post the
// matching 2-5 refactor-system-field-universal-identifiers migration).
//
// If the manifest already declares a field with one of the system
// names (e.g. an app trying to override `id` with a TEXT type), we
// intentionally skip the injection so the existing
// `validateObjectMetadataSystemFieldsIntegrity` validator still gets
// to surface the mistake as `INVALID_SYSTEM_FIELD`.
// `skipNameField: true` because the manifest is responsible for
// declaring the label-identifier field itself.
const fieldNamesDeclaredInObjectManifest = new Set(
objectManifest.fields.map((field) => field.name),
);
const defaultFlatFieldsForObject =
buildDefaultFlatFieldMetadatasForCustomObject({
flatObjectMetadata: {
applicationUniversalIdentifier,
universalIdentifier: flatObjectMetadata.universalIdentifier,
},
skipNameField: true,
});
const systemFlatFieldMetadatasToInject: UniversalFlatFieldMetadata[] =
Object.values(defaultFlatFieldsForObject.fields);
for (const systemFlatFieldMetadata of systemFlatFieldMetadatasToInject) {
if (
fieldNamesDeclaredInObjectManifest.has(systemFlatFieldMetadata.name)
) {
continue;
}
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: systemFlatFieldMetadata,
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
});
}
for (const fieldManifest of objectManifest.fields) {
const enrichedFieldManifest =
fieldManifest.type === FieldMetadataType.TS_VECTOR &&

View File

@@ -1,7 +1,30 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { v4, v5 } from 'uuid';
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
// Namespace used to derive deterministic v5 UUIDs for an object's
// standard system fields (id, createdAt, createdBy, deletedAt, position,
// searchVector, updatedAt, updatedBy). Same approach as
// `NAVIGATION_COMMAND_UUID_NAMESPACE` for navigation command menu items:
// any consumer that can compute the seed `${objectMetadataUniversalIdentifier}/${name}`
// can reproduce the exact same universalIdentifier, which is what lets
// the app-install diff path emit zero updates for system fields it does
// not control.
export const SYSTEM_FIELD_UUID_NAMESPACE =
'7c8e0e1c-2d4f-4ab1-b25b-9d3a8d6a1f02';
export const computeSystemFieldUniversalIdentifier = ({
objectMetadataUniversalIdentifier,
name,
}: {
objectMetadataUniversalIdentifier: string;
name: string;
}): string =>
v5(
`${objectMetadataUniversalIdentifier}/${name}`,
SYSTEM_FIELD_UUID_NAMESPACE,
);
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
@@ -40,10 +63,21 @@ const buildObjectSystemFlatFieldMetadatas = ({
updatedBy,
} = PARTIAL_SYSTEM_FLAT_FIELD_METADATAS;
// Each system field's universalIdentifier is derived deterministically
// from `${objectMetadataUniversalIdentifier}/${name}` so this scaffolder
// produces the same UUIDs every time, for both runtime object creation
// and manifest-derived "TO" computation. That alignment is what makes
// app-install diffs empty for system fields the app doesn't define.
const systemFieldUniversalIdentifier = (name: string): string =>
computeSystemFieldUniversalIdentifier({
objectMetadataUniversalIdentifier,
name,
});
return {
id: {
...id,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(id.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -51,7 +85,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
createdAt: {
...createdAt,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(createdAt.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -59,7 +93,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
createdBy: {
...createdBy,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(createdBy.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -67,7 +101,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
deletedAt: {
...deletedAt,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(deletedAt.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -75,7 +109,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
position: {
...position,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(position.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -83,7 +117,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
searchVector: {
...searchVector,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(searchVector.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -92,7 +126,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
updatedAt: {
...updatedAt,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(updatedAt.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,
@@ -100,7 +134,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
},
updatedBy: {
...updatedBy,
universalIdentifier: v4(),
universalIdentifier: systemFieldUniversalIdentifier(updatedBy.name),
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
createdAt: now,

View File

@@ -0,0 +1,180 @@
import { type Manifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
import { SYSTEM_FIELD_UUID_NAMESPACE } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util';
// Regression test for the bug discovered after the v2.5 upgrade rolled
// `NormalizeCompositeFieldDefaultsCommand` to prod: app re-installs
// failed with `FIELD_MUTATION_NOT_ALLOWED` on 30 system fields because
// the manifest-derived TO map didn't include them while the workspace
// FROM map did (now with non-null defaultValues from the backfill).
//
// Post-fix, the server attaches the eight standard system fields to TO
// itself, with deterministic v5 universalIdentifiers derived from
// `${objectMetadataUniversalIdentifier}/${name}` so they line up with
// the rows already in the workspace.
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const OBJECT_UNIVERSAL_IDENTIFIER = uuidv4();
const LABEL_FIELD_UNIVERSAL_IDENTIFIER = uuidv4();
const TEST_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const SYSTEM_FIELD_NAMES = [
'id',
'createdAt',
'createdBy',
'deletedAt',
'position',
'searchVector',
'updatedAt',
'updatedBy',
] as const;
const buildManifestWithObjectThatHasNoSystemFields = (): Manifest =>
buildBaseManifest({
appId: TEST_APP_ID,
roleId: TEST_ROLE_ID,
overrides: {
objects: [
{
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
labelIdentifierFieldMetadataUniversalIdentifier:
LABEL_FIELD_UNIVERSAL_IDENTIFIER,
nameSingular: 'autoSystemFieldTicket',
namePlural: 'autoSystemFieldTickets',
labelSingular: 'Auto System Field Ticket',
labelPlural: 'Auto System Field Tickets',
description: 'Object that does not declare its own system fields',
icon: 'IconTicket',
fields: [
{
universalIdentifier: LABEL_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldMetadataType.TEXT,
name: 'title',
label: 'Title',
description: 'Label identifier field',
icon: 'IconTextCaption',
},
],
},
],
},
});
const expectedSystemFieldUniversalIdentifier = (name: string): string =>
uuidv5(
`${OBJECT_UNIVERSAL_IDENTIFIER}/${name}`,
SYSTEM_FIELD_UUID_NAMESPACE,
);
const fetchSystemFieldRows = async () => {
return (await globalThis.testDataSource.query(
`SELECT name, "universalIdentifier", "isSystem", "defaultValue"
FROM core."fieldMetadata"
WHERE "workspaceId" = $1
AND "objectMetadataId" IN (
SELECT id FROM core."objectMetadata"
WHERE "workspaceId" = $1
AND "universalIdentifier" = $2
)
AND name = ANY($3)`,
[TEST_WORKSPACE_ID, OBJECT_UNIVERSAL_IDENTIFIER, SYSTEM_FIELD_NAMES],
)) as Array<{
name: string;
universalIdentifier: string;
isSystem: boolean;
defaultValue: unknown;
}>;
};
describe('Sync application auto-attaches system fields to a custom object', () => {
beforeEach(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Auto System Fields App',
description:
'App used to exercise auto-attached system fields on object sync',
sourcePath: 'test-auto-system-fields',
});
}, 60000);
afterEach(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: TEST_APP_ID,
});
});
it('creates the eight standard system fields with v5 universalIdentifiers on first sync', async () => {
const manifest = buildManifestWithObjectThatHasNoSystemFields();
const { errors } = await syncApplication({ manifest });
expect(errors).toBeUndefined();
const rows = await fetchSystemFieldRows();
expect(rows).toHaveLength(SYSTEM_FIELD_NAMES.length);
for (const name of SYSTEM_FIELD_NAMES) {
const row = rows.find((r) => r.name === name);
expect(row).toBeDefined();
expect(row?.isSystem).toBe(true);
expect(row?.universalIdentifier).toBe(
expectedSystemFieldUniversalIdentifier(name),
);
}
}, 60000);
it('re-syncing the same manifest is a no-op for system fields', async () => {
const manifest = buildManifestWithObjectThatHasNoSystemFields();
const firstSync = await syncApplication({ manifest });
expect(firstSync.errors).toBeUndefined();
const secondSync = await syncApplication({ manifest });
expect(secondSync.errors).toBeUndefined();
const rows = await fetchSystemFieldRows();
expect(rows).toHaveLength(SYSTEM_FIELD_NAMES.length);
}, 60000);
// This is the precise shape that failed on prod after the v2.5
// NormalizeCompositeFieldDefaultsCommand ran: the workspace had its
// composite system fields' defaultValue rewritten, then a subsequent
// app re-sync diffed the (FROM with new defaults) against the
// (TO without system fields) and was rejected on every one.
it('handles a re-sync after the workspace mutated a system field defaultValue (post-2.5-backfill shape)', async () => {
const manifest = buildManifestWithObjectThatHasNoSystemFields();
const firstSync = await syncApplication({ manifest });
expect(firstSync.errors).toBeUndefined();
// Simulate `NormalizeCompositeFieldDefaultsCommand` clearing the
// composite system field's defaultValue. The v5 universalIdentifier
// and every other property are preserved.
await globalThis.testDataSource.query(
`UPDATE core."fieldMetadata"
SET "defaultValue" = NULL
WHERE "workspaceId" = $1
AND "universalIdentifier" = $2`,
[
TEST_WORKSPACE_ID,
expectedSystemFieldUniversalIdentifier('createdBy'),
],
);
const secondSync = await syncApplication({ manifest });
expect(secondSync.errors).toBeUndefined();
}, 60000);
});