[BREAKING-CHANGE] Centralize system View viewField side effect (#23081)
# Introduction Closes https://github.com/twentyhq/core-team-issues/issues/2669 Part of the `isSystemSideEffect` engine-ownership effort. Until now, a custom object's default **INDEX** table view (`All {objectLabelPlural}`) and its view fields were built imperatively in `ObjectMetadataService` with random `v4()` identifiers, while `twenty-standard` authored its own copies with hardcoded literals. The two never converged, an object rename could drift the view, and nothing marked these rows as engine-owned. This PR makes the metadata side-effect engine the **single owner** of the INDEX view and its view fields, on name-free deterministic identifiers, for custom and standard objects alike. ## Core design - **Name-free deterministic identity.** The INDEX view identifier derives from `object identifier + ViewKey.INDEX` (`getSystemViewUniversalIdentifier`); each view-field identifier derives from `view identifier + field identifier` (`getViewFieldUniversalIdentifier`). An object rename (with a pinned object identifier) keeps the same view, losslessly. - **`isSystemSideEffect: true` is provenance.** Every INDEX view / view field the engine emits is flagged system-owned, so manifest deletion inference never drops it. The flag follows the view: a view field inherits its parent view's flag. - **The engine is the sole owner of the INDEX view.** It always emits it; a caller providing one with the same derived identifier is a genuine conflict surfaced by the engine's reserved-identifier collision, not silently deferred. ## Changes ### Shared (`twenty-shared`) - `getIndexViewUniversalIdentifier` → `getSystemViewUniversalIdentifier`, now taking a `viewKey` (generalizes to any singleton engine-owned view). - Standard field identifiers extracted into a new `STANDARD_OBJECT_FIELDS` constant, so both an object's `fields` and its INDEX view read the same field identifiers. - `buildStandardObjectIndexView` derives the standard INDEX view + view-field identifiers from `STANDARD_OBJECT_FIELDS`, replacing the hardcoded literals in `standard-object.constant.ts`. ### Metadata side-effect engine (custom objects) - **`objectSystemFieldsAndIndexViewOnCreate`** (replaces `objectSystemFieldsOnCreate`): on object creation, provisions the 7 reserved system fields **and** the INDEX view with one view field per displayable system field, all `isSystemSideEffect: true`. - **`fieldIndexViewFieldOnCreate`** (new): on field creation, provisions the field's INDEX view field. Object created in the same batch → visible, positioned before the system view fields; pre-existing object → hidden, appended (preserving the historical `createOneField` behavior). Both branches resolve the INDEX view by its derived identifier (single map access, never a scan). - **`fieldSystemViewFieldsOnDelete`** (new): on field deletion, cascade-deletes every engine-owned view field displaying it. - **`objectSystemSideEffectsOnDelete`** (extended): now also cascade-deletes the object's engine-owned views and their view fields (in addition to system fields, indexes, searchFieldMetadata). Every lookup walks a foreign-key aggregator down from the deleted object, so the work is proportional to what the object owns, never to workspace size. - Object-create and field-create positions are derived from the same caller-input field list, so the INDEX view layout is contiguous with no handler-ordering dependency. - `view` / `viewField` added to the side-effect companion metadata names for `fieldMetadata` and `objectMetadata`. ### Reserved-identifier invariant A caller can never define an entity whose identifier collides with one a system side effect produces: caller inputs are forced `isSystemSideEffect: false` at every entry point (API and app-manifest transpilers), and the engine raises `RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`, aborting the operation, when a system emission lands on a caller-claimed identifier. Covered by a new engine-level test. ### Caller-side provisioning removed The imperative INDEX view + view-field provisioning is removed from `ObjectMetadataService.createOneObject`. The record-page `FIELDS_WIDGET` view is intentionally left caller-side and deferred to the follow-up (see below). ### `twenty-standard` convergence Standard INDEX views and their view fields converge on the same derived-identifier + `isSystemSideEffect: true` scheme as the engine. `twenty-standard` syncs through the from/to migration path (which never runs the side-effect engine), so it authors this INDEX surface itself, matching what the engine produces for custom objects. ## Rollout Two `2.26.0` workspace commands, running after the `2.25` messageCampaign commands: - `upgrade:2-26:reconcile-index-view-universal-identifier` re-owns the INDEX views of the **twenty-standard and workspace-custom applications** and all their view fields to the derived identifiers with `isSystemSideEffect: true`, in a single per-workspace transaction. Each view field identifier is keyed on the application of the **displayed field** (an app or user column on a standard INDEX view converges too). Soft-deleted views and view fields are skipped: one can coexist with an active successor on the same derivation inputs and both would derive the same identifier. Children reference the view by primary key, so the re-own is lossless. - `upgrade:2-26:demote-and-backfill-application-index-view` handles **manifest-installed applications**, which never had their INDEX view auto-provisioned: every caller-authored INDEX view of another application is demoted to `key: null` (a plain additional view under its manifest identifier), then every application object gets the engine-owned INDEX view and its full view-field layout backfilled through the migration pipeline's legacy path (no side-effect expansion), views committed before view fields across applications since a view field belongs to the application owning its field. Idempotent and retry-safe: engine-owned INDEX views are neither demoted nor re-backfilled, and view creation and view-field creation are gated independently, so a retry after a partial failure still backfills the missing view fields of an already-committed view. Both support `--dry-run` and invalidate the full flat-maps closure (parents aggregate the re-owned identifiers, children resolve them as universal foreign keys, and page-layout widget universal configurations resolve view PKs at cache-build time). The `2.25` `upgrade:2-25:add-message-campaign-name-field` command is adapted to resolve the campaign INDEX view by its INDEX key on the object instead of by universal identifier: it now runs before the reconcile, on workspaces still holding legacy identifiers. ## ⚠️ Breaking change This PR **mutates 187 previously hardcoded universal identifiers** — the standard objects' INDEX views and their view fields (the literals removed from `standard-object.constant.ts`), now derived. - **Handled by the `2.26` commands above** for all existing workspaces. - **The INDEX key is now engine-reserved.** The flat view validator rejects caller-created INDEX views (API and manifest inputs are forced `isSystemSideEffect: false`) and enforces a single non-deleted INDEX view per object; `view.key` is no longer a comparable/updatable property, so no writer can promote or demote a view after creation. `ViewManifest.key` is deprecated and ignored (manifest views are always additional views, so old apps keep syncing and demoted views are not promoted back); the REST/GraphQL create path now rejects `key: INDEX`. In-repo example apps (`hello-world`, `document-generator`) no longer declare it. - **12 declared-but-never-seeded standard INDEX view field identifiers deleted** (the former `preservedViewFields` on `timelineActivity`, `workflowRun` and `workspaceMember`): after the reconcile, no workspace row references them. - **`computeFlatViewFieldsToCreate` now derives view field identifiers** instead of drawing `v4()` ones, which also changes what the committed `1-23` record-page backfill produces going forward (deliberate, documented in-code). - **Record-page views and view fields are not affected** (identifiers unchanged). - **In-repo apps: `twenty-last-contact` updated.** It was the only app declaring explicit INDEX view fields (10 columns across `allPeople` / `allCompanies` / `allOpportunities`) through manifest `viewFields`. Those target identifiers are now engine-owned and derived, so the manifest inputs no longer resolve and install failed with `View not found`. The app now declares only its fields; the engine's `fieldIndexViewFieldOnCreate` provisions the matching INDEX view field automatically. No other app under `packages/twenty-apps` references any of the 187 mutated identifiers, and apps that target standard views point at record-page views (e.g. `real-estate` → `opportunityRecordPageFields`) or their own objects (`twenty-partners`), all unchanged. ### Loss of granularity for app maintainers The engine now owns the INDEX view field of every field a caller adds to an object, so app maintainers lose direct control over those columns. Previously an app could target the engine-owned INDEX view with an explicit manifest `viewField` and set its `position` and `isVisible`. Now `fieldIndexViewFieldOnCreate` appends a **hidden** view field in caller-input order on field creation, so: - Columns an app previously showed at a **dedicated position** and **visible** (e.g. `twenty-last-contact`'s last-contact columns) become **hidden** and **appended in input order** after install. - There is currently **no manifest way to override** the engine-provisioned INDEX view field's position, visibility, or size. This is a deliberate regression accepted for the sake of single-ownership, and app maintainers should expect their INDEX columns to move/hide after upgrading. A follow-up override API will let maintainers reclaim per-field control over the engine-provisioned INDEX view field. ## Testing - Unit specs for each handler: object create (system fields + INDEX view/view fields, override, position offset), field create (same-batch vs existing-object, non-displayable noop, no-INDEX-view noop), field delete, object delete (fields/indexes/searchFieldMetadata/views/view fields cascade, reverse-relation view field on another object). - Engine-level test for the reserved-identifier collision. - `twenty-standard` guard test that its INDEX views/view fields stay on the derived scheme and stay system-owned. - Integration test: full engine provisioning of the INDEX view/view fields on object creation, same view id preserved across an object rename, and cascade delete on object deletion. ## Follow-up The full record-page stack (record-page view, its view fields, view field groups, page layout / tab / widget) is still built imperatively and moves into the engine in https://github.com/twentyhq/core-team-issues/issues/2721.
This commit is contained in:
+2
-2
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
AggregateOperations,
|
||||
ViewCalendarLayout,
|
||||
ViewKey,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
ViewKey,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
|
||||
@@ -39,7 +39,7 @@ describe('fromViewManifestToUniversalFlatView', () => {
|
||||
expect(result.isCustom).toBe(true);
|
||||
expect(result.visibility).toBe(ViewVisibility.WORKSPACE);
|
||||
expect(result.openRecordIn).toBe(ViewOpenRecordIn.SIDE_PANEL);
|
||||
expect(result.key).toBe(ViewKey.INDEX);
|
||||
expect(result.key).toBeNull();
|
||||
expect(result.createdAt).toBe(now);
|
||||
expect(result.updatedAt).toBe(now);
|
||||
});
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export const fromViewManifestToUniversalFlatView = ({
|
||||
isCustom: true,
|
||||
visibility: viewManifest.visibility ?? ViewVisibility.WORKSPACE,
|
||||
openRecordIn: viewManifest.openRecordIn ?? ViewOpenRecordIn.SIDE_PANEL,
|
||||
key: viewManifest.key ?? null,
|
||||
key: null,
|
||||
kanbanAggregateOperation: viewManifest.kanbanAggregateOperation ?? null,
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier:
|
||||
viewManifest.kanbanAggregateOperationFieldMetadataUniversalIdentifier ??
|
||||
|
||||
-1
@@ -330,7 +330,6 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
},
|
||||
"view": {
|
||||
"propertiesToCompare": [
|
||||
"key",
|
||||
"deletedAt",
|
||||
"createdByUserWorkspaceId",
|
||||
"name",
|
||||
|
||||
+1
-1
@@ -303,7 +303,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
key: { toCompare: true, toStringify: false, universalProperty: undefined },
|
||||
key: { toCompare: false, toStringify: false, universalProperty: undefined },
|
||||
deletedAt: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
|
||||
+8
-2
@@ -1,8 +1,14 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
export const ALL_METADATA_SIDE_EFFECT_COMPANION_METADATA_NAMES = {
|
||||
fieldMetadata: ['index', 'searchFieldMetadata'],
|
||||
objectMetadata: ['fieldMetadata', 'index', 'searchFieldMetadata'],
|
||||
fieldMetadata: ['index', 'searchFieldMetadata', 'view', 'viewField'],
|
||||
objectMetadata: [
|
||||
'fieldMetadata',
|
||||
'index',
|
||||
'searchFieldMetadata',
|
||||
'view',
|
||||
'viewField',
|
||||
],
|
||||
} as const satisfies Partial<
|
||||
Record<AllMetadataName, readonly AllMetadataName[]>
|
||||
>;
|
||||
|
||||
+4
-74
@@ -790,8 +790,8 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('INDEX view propagation', () => {
|
||||
it('should add a hidden, flagged view field to the object INDEX view even without a fields widget', () => {
|
||||
describe('INDEX view non-propagation', () => {
|
||||
it('should not target the object INDEX view without a fields widget', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
@@ -817,81 +817,10 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
viewUniversalIdentifier: 'index-view-uid',
|
||||
fieldMetadataUniversalIdentifier: 'field-uid-1',
|
||||
isVisible: false,
|
||||
isSystemSideEffect: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not target a standalone non-INDEX view without a widget', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
objectMetadataUniversalIdentifier:
|
||||
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: 'field-uid-1',
|
||||
},
|
||||
],
|
||||
flatPageLayoutWidgetMaps:
|
||||
buildEmptyFlatEntityMaps() as unknown as FlatPageLayoutWidgetMaps,
|
||||
flatViewFieldMaps: buildFlatViewFieldMaps(),
|
||||
flatViewMaps: buildFlatViewMaps([
|
||||
{
|
||||
id: 'plain-view-db-id',
|
||||
universalIdentifier: 'plain-view-uid',
|
||||
key: null,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
]),
|
||||
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should skip an inactive or soft-deleted INDEX view', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
objectMetadataUniversalIdentifier:
|
||||
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: 'field-uid-1',
|
||||
},
|
||||
],
|
||||
flatPageLayoutWidgetMaps:
|
||||
buildEmptyFlatEntityMaps() as unknown as FlatPageLayoutWidgetMaps,
|
||||
flatViewFieldMaps: buildFlatViewFieldMaps(),
|
||||
flatViewMaps: buildFlatViewMaps([
|
||||
{
|
||||
id: 'inactive-index-view-db-id',
|
||||
universalIdentifier: 'inactive-index-view-uid',
|
||||
key: ViewKey.INDEX,
|
||||
isActive: false,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
{
|
||||
id: 'deleted-index-view-db-id',
|
||||
universalIdentifier: 'deleted-index-view-uid',
|
||||
key: ViewKey.INDEX,
|
||||
deletedAt: '2024-01-01T00:00:00.000Z',
|
||||
objectMetadataUniversalIdentifier:
|
||||
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
]),
|
||||
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not duplicate a field when a fields widget and the INDEX view share the same view', () => {
|
||||
it('should still honor a fields widget explicitly targeting the INDEX view', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
@@ -919,6 +848,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].isVisible).toBe(true);
|
||||
expect(result[0].viewUniversalIdentifier).toBe('index-view-uid');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-22
@@ -1,4 +1,3 @@
|
||||
import { ViewKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -33,14 +32,13 @@ const isFieldsWidgetConfiguration = (
|
||||
);
|
||||
};
|
||||
|
||||
// TODO remove when implementing https://github.com/twentyhq/core-team-issues/issues/2721
|
||||
const getFieldViewTargets = ({
|
||||
objectMetadataUniversalIdentifier,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewMaps,
|
||||
}: {
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
}): FieldViewTarget[] => {
|
||||
const targets: FieldViewTarget[] = [];
|
||||
const seenViewIds = new Set<string>();
|
||||
@@ -72,24 +70,6 @@ const getFieldViewTargets = ({
|
||||
targets.push({ viewId, isVisible: newFieldDefaultVisibility });
|
||||
}
|
||||
|
||||
for (const view of Object.values(flatViewMaps.byUniversalIdentifier).filter(
|
||||
isDefined,
|
||||
)) {
|
||||
if (
|
||||
view.key !== ViewKey.INDEX ||
|
||||
!view.isActive ||
|
||||
isDefined(view.deletedAt) ||
|
||||
view.objectMetadataUniversalIdentifier !==
|
||||
objectMetadataUniversalIdentifier ||
|
||||
seenViewIds.has(view.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenViewIds.add(view.id);
|
||||
targets.push({ viewId: view.id, isVisible: false });
|
||||
}
|
||||
|
||||
return targets;
|
||||
};
|
||||
|
||||
@@ -181,7 +161,6 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
|
||||
const targets = getFieldViewTargets({
|
||||
objectMetadataUniversalIdentifier,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewMaps,
|
||||
});
|
||||
|
||||
const fieldsForObject = fieldsToCreate.filter(
|
||||
|
||||
+680
@@ -0,0 +1,680 @@
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
getSystemViewUniversalIdentifier,
|
||||
getViewFieldUniversalIdentifier,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { FieldIndexViewFieldOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
const computeFieldUniversalIdentifier = (name: string) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
});
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = computeFieldUniversalIdentifier('name');
|
||||
const PRIORITY_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
computeFieldUniversalIdentifier('priority');
|
||||
|
||||
const DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
viewKey: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
const computeViewFieldUniversalIdentifier = ({
|
||||
viewUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
}: {
|
||||
viewUniversalIdentifier: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
}) =>
|
||||
getViewFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
});
|
||||
|
||||
type PendingFieldMetadata = {
|
||||
universalIdentifier: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
isSystemSideEffect: boolean;
|
||||
};
|
||||
|
||||
const buildPendingFieldMetadata = (
|
||||
name: string,
|
||||
type: FieldMetadataType = FieldMetadataType.TEXT,
|
||||
isSystemSideEffect = false,
|
||||
): PendingFieldMetadata => ({
|
||||
universalIdentifier: computeFieldUniversalIdentifier(name),
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
type,
|
||||
isSystemSideEffect,
|
||||
});
|
||||
|
||||
const NAME_FIELD = buildPendingFieldMetadata('name');
|
||||
const PRIORITY_FIELD = buildPendingFieldMetadata(
|
||||
'priority',
|
||||
FieldMetadataType.NUMBER,
|
||||
);
|
||||
|
||||
const OBJECT_METADATA = {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'ticket',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
};
|
||||
|
||||
type WorkspaceView = {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
key: ViewKey | null;
|
||||
isActive?: boolean;
|
||||
deletedAt?: string | null;
|
||||
isSystemSideEffect?: boolean;
|
||||
objectMetadataUniversalIdentifier?: string;
|
||||
};
|
||||
|
||||
type WorkspaceViewField = {
|
||||
universalIdentifier: string;
|
||||
viewId: string;
|
||||
viewUniversalIdentifier?: string;
|
||||
fieldMetadataUniversalIdentifier?: string;
|
||||
position: number;
|
||||
isActive: boolean;
|
||||
deletedAt?: string | null;
|
||||
};
|
||||
|
||||
const buildArgs = ({
|
||||
triggerFieldMetadata,
|
||||
pendingFieldMetadatas = [],
|
||||
objectMetadataCreatedInBatch = false,
|
||||
pendingViews = [],
|
||||
pendingViewFields = [],
|
||||
objectMetadataInWorkspace = false,
|
||||
viewsInWorkspace = [],
|
||||
viewFieldsInWorkspace = [],
|
||||
}: {
|
||||
triggerFieldMetadata: PendingFieldMetadata;
|
||||
pendingFieldMetadatas?: PendingFieldMetadata[];
|
||||
objectMetadataCreatedInBatch?: boolean;
|
||||
pendingViews?: { universalIdentifier: string; isSystemSideEffect: boolean }[];
|
||||
pendingViewFields?: {
|
||||
universalIdentifier: string;
|
||||
viewUniversalIdentifier: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
}[];
|
||||
objectMetadataInWorkspace?: boolean;
|
||||
viewsInWorkspace?: WorkspaceView[];
|
||||
viewFieldsInWorkspace?: WorkspaceViewField[];
|
||||
}): BuildSideEffectsArgs<'fieldMetadata'> =>
|
||||
({
|
||||
flatEntity: triggerFieldMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingFieldMetadatas.map((pendingFieldMetadata) => [
|
||||
pendingFieldMetadata.universalIdentifier,
|
||||
pendingFieldMetadata,
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
objectMetadata: {
|
||||
flatEntityToCreate: objectMetadataCreatedInBatch
|
||||
? { [OBJECT_UNIVERSAL_IDENTIFIER]: OBJECT_METADATA }
|
||||
: {},
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingViews.map((pendingView) => [
|
||||
pendingView.universalIdentifier,
|
||||
pendingView,
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingViewFields.map((pendingViewField) => [
|
||||
pendingViewField.universalIdentifier,
|
||||
pendingViewField,
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
} as unknown as AllFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps: {
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: objectMetadataInWorkspace
|
||||
? { [OBJECT_UNIVERSAL_IDENTIFIER]: OBJECT_METADATA }
|
||||
: {},
|
||||
},
|
||||
flatViewMaps: {
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
viewsInWorkspace.map((view) => [
|
||||
view.universalIdentifier,
|
||||
{
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
isSystemSideEffect: false,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
...view,
|
||||
// Mirrors fromViewEntityToFlatView: the view aggregates the
|
||||
// view fields pointing at it.
|
||||
viewFieldUniversalIdentifiers: viewFieldsInWorkspace
|
||||
.filter((viewField) => viewField.viewId === view.id)
|
||||
.map((viewField) => viewField.universalIdentifier),
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
viewFieldsInWorkspace.map((viewField) => [
|
||||
viewField.universalIdentifier,
|
||||
viewField,
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'fieldMetadata'>;
|
||||
|
||||
// Synced INDEX view: the 2-26 reconcile command re-owns every INDEX view to the
|
||||
// derived identifier, so the handler resolves it by that identifier alone.
|
||||
const SYNCED_INDEX_VIEW: WorkspaceView = {
|
||||
id: 'view-db-id-1',
|
||||
universalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
};
|
||||
|
||||
describe('FieldIndexViewFieldOnCreateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (FieldIndexViewFieldOnCreateSideEffectHandlerService as unknown as new () => FieldIndexViewFieldOnCreateSideEffectHandlerService)();
|
||||
|
||||
describe('object created in the same batch (default view assembly)', () => {
|
||||
it('should emit a visible view field at position 0 for the label identifier', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: NAME_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD, PRIORITY_FIELD],
|
||||
objectMetadataCreatedInBatch: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].universalIdentifier).toBe(
|
||||
computeViewFieldUniversalIdentifier({
|
||||
viewUniversalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
}),
|
||||
);
|
||||
expect(viewFields[0].viewUniversalIdentifier).toBe(
|
||||
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(viewFields[0].position).toBe(0);
|
||||
expect(viewFields[0].isVisible).toBe(true);
|
||||
expect(viewFields[0].isSystemSideEffect).toBe(true);
|
||||
});
|
||||
|
||||
it('should position a non-label caller field after the label identifier', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD, PRIORITY_FIELD],
|
||||
objectMetadataCreatedInBatch: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].position).toBe(1);
|
||||
});
|
||||
|
||||
it('should still emit when the pending INDEX view is a system side effect (object handler emission)', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: NAME_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD],
|
||||
objectMetadataCreatedInBatch: true,
|
||||
pendingViews: [
|
||||
{
|
||||
universalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
|
||||
// The engine owns the INDEX view, so the handler no longer defers to a
|
||||
// caller-provided one: it emits and lets the engine collision on the view
|
||||
// surface the conflict.
|
||||
it('should still emit when the caller provides the pending INDEX view (no override)', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: NAME_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD],
|
||||
objectMetadataCreatedInBatch: true,
|
||||
pendingViews: [
|
||||
{
|
||||
universalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
|
||||
it('should noop for a non-displayable field (relation) at object creation', () => {
|
||||
const relationField = buildPendingFieldMetadata(
|
||||
'assignee',
|
||||
FieldMetadataType.RELATION,
|
||||
);
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: relationField,
|
||||
pendingFieldMetadatas: [NAME_FIELD, relationField],
|
||||
objectMetadataCreatedInBatch: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
|
||||
describe('field created on an existing object (historical createOneField behavior)', () => {
|
||||
it('should append a hidden view field to the INDEX view resolved by its derived identifier', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
viewFieldsInWorkspace: [
|
||||
{
|
||||
universalIdentifier: 'existing-vf-1',
|
||||
viewId: SYNCED_INDEX_VIEW.id,
|
||||
position: 0,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'existing-vf-2',
|
||||
viewId: SYNCED_INDEX_VIEW.id,
|
||||
position: 4,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].universalIdentifier).toBe(
|
||||
computeViewFieldUniversalIdentifier({
|
||||
viewUniversalIdentifier: SYNCED_INDEX_VIEW.universalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: PRIORITY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
}),
|
||||
);
|
||||
expect(viewFields[0].viewUniversalIdentifier).toBe(
|
||||
SYNCED_INDEX_VIEW.universalIdentifier,
|
||||
);
|
||||
expect(viewFields[0].position).toBe(5);
|
||||
expect(viewFields[0].isVisible).toBe(false);
|
||||
expect(viewFields[0].isSystemSideEffect).toBe(true);
|
||||
});
|
||||
|
||||
it('should ignore soft-deleted view fields when computing the append position', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
viewFieldsInWorkspace: [
|
||||
{
|
||||
universalIdentifier: 'existing-vf-1',
|
||||
viewId: SYNCED_INDEX_VIEW.id,
|
||||
position: 0,
|
||||
isActive: true,
|
||||
},
|
||||
// A removed column: still in the flat maps but soft-deleted, its
|
||||
// position must not inflate the append position.
|
||||
{
|
||||
universalIdentifier: 'soft-deleted-vf',
|
||||
viewId: SYNCED_INDEX_VIEW.id,
|
||||
position: 9,
|
||||
isActive: true,
|
||||
deletedAt: '2024-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].position).toBe(1);
|
||||
});
|
||||
|
||||
it('should place the label identifier view field strictly below every existing one', () => {
|
||||
// Relabeling onto a field created in the same sync: existing positions
|
||||
// start above zero, the label lands one below the lowest.
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: NAME_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
viewFieldsInWorkspace: [
|
||||
{
|
||||
universalIdentifier: 'existing-vf-1',
|
||||
viewId: SYNCED_INDEX_VIEW.id,
|
||||
position: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'existing-vf-2',
|
||||
viewId: SYNCED_INDEX_VIEW.id,
|
||||
position: 5,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].isVisible).toBe(true);
|
||||
expect(viewFields[0].position).toBe(1);
|
||||
});
|
||||
|
||||
it('should place the label identifier view field at position 0 on an empty INDEX view', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: NAME_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].isVisible).toBe(true);
|
||||
expect(viewFields[0].position).toBe(0);
|
||||
});
|
||||
|
||||
it('should emit a hidden view field for a relation field', () => {
|
||||
const relationField = buildPendingFieldMetadata(
|
||||
'assignee',
|
||||
FieldMetadataType.RELATION,
|
||||
);
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: relationField,
|
||||
pendingFieldMetadatas: [relationField],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields).toHaveLength(1);
|
||||
expect(viewFields[0].isVisible).toBe(false);
|
||||
});
|
||||
|
||||
// The emitted view field is engine-owned whatever the view provenance, so
|
||||
// manifest sync deletion inference never drops it.
|
||||
it('should emit an isSystemSideEffect view field on a manifest-owned INDEX view', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [
|
||||
{ ...SYNCED_INDEX_VIEW, isSystemSideEffect: false },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewFields[0].isSystemSideEffect).toBe(true);
|
||||
});
|
||||
|
||||
// View field groups only ever exist on record-page views, never on an INDEX
|
||||
// view, so the appended view field is always ungrouped.
|
||||
it('should emit an ungrouped view field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})[0]
|
||||
.viewFieldGroupUniversalIdentifier,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should noop when an INDEX view exists but not under its derived identifier (unreconciled workspace)', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [
|
||||
{
|
||||
...SYNCED_INDEX_VIEW,
|
||||
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000042',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should noop when the object has no active INDEX view', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [
|
||||
{ ...SYNCED_INDEX_VIEW, isActive: false },
|
||||
{
|
||||
id: 'other-view-db-id',
|
||||
universalIdentifier: 'other-view-uid',
|
||||
key: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not count engine-owned fields when positioning caller view fields', () => {
|
||||
const searchVectorField = buildPendingFieldMetadata(
|
||||
'searchVector',
|
||||
FieldMetadataType.TS_VECTOR,
|
||||
true,
|
||||
);
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
// searchVector sits in the expanded matrix (emitted by a peer handler)
|
||||
// but must not shift the caller field positions.
|
||||
pendingFieldMetadatas: [NAME_FIELD, searchVectorField, PRIORITY_FIELD],
|
||||
objectMetadataCreatedInBatch: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})[0]
|
||||
.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
// A second writer claiming the same (view, field) pair is not deduped here:
|
||||
// it is a genuine conflict left to surface downstream (engine collision, then
|
||||
// the flat view field validator on the pair).
|
||||
it('should still emit when a pending view field already covers the same (view, field) pair', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: PRIORITY_FIELD,
|
||||
pendingFieldMetadatas: [PRIORITY_FIELD],
|
||||
objectMetadataInWorkspace: true,
|
||||
viewsInWorkspace: [SYNCED_INDEX_VIEW],
|
||||
pendingViewFields: [
|
||||
{
|
||||
// e.g. emitted caller-side for a FIELDS widget targeting the view
|
||||
universalIdentifier: 'random-caller-vf-uid',
|
||||
viewUniversalIdentifier: SYNCED_INDEX_VIEW.universalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PRIORITY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})[0]
|
||||
.fieldMetadataUniversalIdentifier,
|
||||
).toBe(PRIORITY_FIELD_UNIVERSAL_IDENTIFIER);
|
||||
});
|
||||
|
||||
it('should fail when the parent object cannot be resolved', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
triggerFieldMetadata: NAME_FIELD,
|
||||
pendingFieldMetadatas: [NAME_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('fail');
|
||||
});
|
||||
});
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { FieldSystemViewFieldsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-system-view-fields-on-delete-side-effect-handler.service';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
|
||||
const DELETED_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000001';
|
||||
const SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a1a2a3a4-a5a6-4000-8000-000000000002';
|
||||
|
||||
const buildArgs = ({
|
||||
viewFieldUniversalIdentifiers,
|
||||
viewFieldsInWorkspace = [],
|
||||
}: {
|
||||
viewFieldUniversalIdentifiers: string[];
|
||||
viewFieldsInWorkspace?: {
|
||||
universalIdentifier: string;
|
||||
isSystemSideEffect: boolean;
|
||||
}[];
|
||||
}): BuildSideEffectsArgs<'fieldMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
universalIdentifier: DELETED_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
viewFieldUniversalIdentifiers,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {},
|
||||
relatedFlatEntityMaps: {
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
viewFieldsInWorkspace.map((viewField) => [
|
||||
viewField.universalIdentifier,
|
||||
{
|
||||
...viewField,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
DELETED_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'fieldMetadata'>;
|
||||
|
||||
describe('FieldSystemViewFieldsOnDeleteSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (FieldSystemViewFieldsOnDeleteSideEffectHandlerService as unknown as new () => FieldSystemViewFieldsOnDeleteSideEffectHandlerService)();
|
||||
|
||||
it('should cascade-delete every engine-owned view field displaying the deleted field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
viewFieldUniversalIdentifiers: [
|
||||
SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
viewFieldsInWorkspace: [
|
||||
{
|
||||
universalIdentifier: SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.viewField?.flatEntityToDelete ?? {}),
|
||||
).toEqual([SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER]);
|
||||
});
|
||||
|
||||
it('should noop when the field only has caller-owned view fields', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
viewFieldUniversalIdentifiers: [CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER],
|
||||
viewFieldsInWorkspace: [
|
||||
{
|
||||
universalIdentifier: CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should noop when the field has no view field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({ viewFieldUniversalIdentifiers: [] }),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
getSystemViewUniversalIdentifier,
|
||||
getViewFieldUniversalIdentifier,
|
||||
} from 'twenty-shared/application';
|
||||
import { ViewKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
|
||||
import { buildFieldSideEffectParentNotFoundFailure } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/utils/build-field-side-effect-parent-not-found-failure.util';
|
||||
import { resolveParentFlatObjectMetadataAfterStateForFieldSideEffect } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/utils/resolve-parent-flat-object-metadata-after-state-for-field-side-effect.util';
|
||||
import { computeCallerFlatFieldMetadatasForObject } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util';
|
||||
import { computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
|
||||
@Injectable()
|
||||
export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'create',
|
||||
metadataName: 'fieldMetadata',
|
||||
name: 'fieldIndexViewFieldOnCreate',
|
||||
description:
|
||||
'When a field is created, provision its view field on the parent object INDEX view. This handler owns the view fields of every caller-provided field. When the parent object is created in the same batch, the default view assembly applies: only displayable fields (no relations) get a visible view field, positioned before the system-field view fields emitted by objectSystemFieldsAndIndexViewOnCreate; both handlers derive positions from the same caller-input list so no ordering dependency exists. When the parent object pre-exists, every created field (relations included) gets a HIDDEN view field appended to the INDEX view, preserving the historical createOneField behavior previously implemented caller-side. Emitted view fields are always isSystemSideEffect, whatever the provenance of the view they land on: the engine authored them, so manifest sync deletion inference must never drop them. Both branches resolve the INDEX view through its name-free deterministic identifier (object identifier + INDEX key), so the lookup is a single map access and never a scan; workspaces synced before the 2-26 reconcile command hold underived INDEX view identifiers and are simply skipped until it runs. Engine-owned fields (the reserved system fields, the searchVector) never reach this handler: they are emitted as side effects and the engine only triggers handlers on the original input, so their view fields belong to the handler that emits them. Noop when the created field is not displayable in the default view (object created in the same batch) or when the object has no active INDEX view (pre-existing object). The engine owns the INDEX view, so this handler never defers to a caller-provided one: the flat view validator rejects caller-created INDEX views outright, and a second writer claiming the same INDEX view field is a genuine conflict left to surface downstream (engine universal-identifier collision, then the flat view field validator on the (view, field) pair).',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatFieldMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'fieldMetadata'>): MetadataSideEffectResult {
|
||||
const sourceFlatFieldMetadata =
|
||||
flatFieldMetadata as UniversalFlatFieldMetadata;
|
||||
const { objectMetadataUniversalIdentifier } = sourceFlatFieldMetadata;
|
||||
|
||||
const parentFlatObjectMetadata =
|
||||
resolveParentFlatObjectMetadataAfterStateForFieldSideEffect({
|
||||
objectMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(parentFlatObjectMetadata)) {
|
||||
return buildFieldSideEffectParentNotFoundFailure({
|
||||
flatFieldMetadata: sourceFlatFieldMetadata,
|
||||
operation: 'create',
|
||||
});
|
||||
}
|
||||
|
||||
const indexViewUniversalIdentifier = getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
parentFlatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
viewKey: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
const parentObjectCreatedInSameBatch = isDefined(
|
||||
allFlatEntityOperationRecordByMetadataName.objectMetadata
|
||||
?.flatEntityToCreate[objectMetadataUniversalIdentifier],
|
||||
);
|
||||
|
||||
const flatViewFieldToCreate = parentObjectCreatedInSameBatch
|
||||
? this.buildViewFieldForObjectCreatedInSameBatch({
|
||||
sourceFlatFieldMetadata,
|
||||
parentFlatObjectMetadata,
|
||||
indexViewUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
})
|
||||
: this.buildViewFieldForExistingObject({
|
||||
sourceFlatFieldMetadata,
|
||||
parentFlatObjectMetadata,
|
||||
indexViewUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatViewFieldToCreate)) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
viewField: {
|
||||
flatEntityToCreate: {
|
||||
[flatViewFieldToCreate.universalIdentifier]: flatViewFieldToCreate,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private buildViewFieldForObjectCreatedInSameBatch({
|
||||
sourceFlatFieldMetadata,
|
||||
parentFlatObjectMetadata,
|
||||
indexViewUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
}: {
|
||||
sourceFlatFieldMetadata: UniversalFlatFieldMetadata;
|
||||
parentFlatObjectMetadata: {
|
||||
applicationUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
};
|
||||
indexViewUniversalIdentifier: string;
|
||||
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'fieldMetadata'>['allFlatEntityOperationRecordByMetadataName'];
|
||||
}): UniversalFlatViewField | undefined {
|
||||
const { labelIdentifierFieldMetadataUniversalIdentifier } =
|
||||
parentFlatObjectMetadata;
|
||||
|
||||
if (
|
||||
!isFlatFieldMetadataDisplayableInDefaultView({
|
||||
flatFieldMetadata: sourceFlatFieldMetadata,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const displayableCallerFlatFieldMetadatas =
|
||||
computeCallerFlatFieldMetadatasForObject({
|
||||
objectMetadataUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
displayableOnly: true,
|
||||
});
|
||||
|
||||
const positionByFieldUniversalIdentifier =
|
||||
computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
parentFlatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
displayableCallerFlatFieldMetadatas,
|
||||
});
|
||||
|
||||
const position =
|
||||
positionByFieldUniversalIdentifier.get(
|
||||
sourceFlatFieldMetadata.universalIdentifier,
|
||||
) ?? 0;
|
||||
|
||||
const [flatViewFieldToCreate] = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: [sourceFlatFieldMetadata],
|
||||
viewUniversalIdentifier: indexViewUniversalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
startPosition: position,
|
||||
});
|
||||
|
||||
return flatViewFieldToCreate;
|
||||
}
|
||||
|
||||
private buildViewFieldForExistingObject({
|
||||
sourceFlatFieldMetadata,
|
||||
parentFlatObjectMetadata,
|
||||
indexViewUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
sourceFlatFieldMetadata: UniversalFlatFieldMetadata;
|
||||
parentFlatObjectMetadata: {
|
||||
applicationUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
};
|
||||
indexViewUniversalIdentifier: string;
|
||||
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'fieldMetadata'>['allFlatEntityOperationRecordByMetadataName'];
|
||||
relatedFlatEntityMaps: BuildSideEffectsArgs<'fieldMetadata'>['relatedFlatEntityMaps'];
|
||||
}): UniversalFlatViewField | undefined {
|
||||
const existingIndexFlatView =
|
||||
relatedFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
|
||||
indexViewUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(existingIndexFlatView) ||
|
||||
!existingIndexFlatView.isActive ||
|
||||
isDefined(existingIndexFlatView.deletedAt)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const existingActivePositions =
|
||||
existingIndexFlatView.viewFieldUniversalIdentifiers
|
||||
.map(
|
||||
(viewFieldUniversalIdentifier) =>
|
||||
relatedFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier[
|
||||
viewFieldUniversalIdentifier
|
||||
],
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(existingFlatViewField) =>
|
||||
existingFlatViewField.isActive &&
|
||||
!isDefined(existingFlatViewField.deletedAt),
|
||||
)
|
||||
.map((existingFlatViewField) => existingFlatViewField.position);
|
||||
|
||||
const isLabelIdentifierField =
|
||||
sourceFlatFieldMetadata.universalIdentifier ===
|
||||
parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (isLabelIdentifierField) {
|
||||
const lowestExistingPosition =
|
||||
existingActivePositions.length > 0
|
||||
? Math.min(...existingActivePositions)
|
||||
: 1;
|
||||
const [flatLabelViewFieldToCreate] = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: [sourceFlatFieldMetadata],
|
||||
viewUniversalIdentifier: indexViewUniversalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
startPosition: lowestExistingPosition - 1,
|
||||
});
|
||||
|
||||
return flatLabelViewFieldToCreate;
|
||||
}
|
||||
|
||||
const appendBasePosition =
|
||||
existingActivePositions.reduce(
|
||||
(maxPosition, position) => Math.max(maxPosition, position),
|
||||
-1,
|
||||
) + 1;
|
||||
|
||||
const callerFlatFieldMetadatas = computeCallerFlatFieldMetadatasForObject({
|
||||
objectMetadataUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
displayableOnly: false,
|
||||
});
|
||||
|
||||
const indexAmongCallerFlatFieldMetadatas = Math.max(
|
||||
callerFlatFieldMetadatas.findIndex(
|
||||
(callerFlatFieldMetadata) =>
|
||||
callerFlatFieldMetadata.universalIdentifier ===
|
||||
sourceFlatFieldMetadata.universalIdentifier,
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const { applicationUniversalIdentifier } = sourceFlatFieldMetadata;
|
||||
|
||||
return {
|
||||
universalIdentifier: getViewFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
viewUniversalIdentifier: indexViewUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
sourceFlatFieldMetadata.universalIdentifier,
|
||||
viewUniversalIdentifier: indexViewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier: null,
|
||||
isVisible: false,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: appendBasePosition + indexAmongCallerFlatFieldMetadatas,
|
||||
aggregateOperation: null,
|
||||
isActive: true,
|
||||
isSystemSideEffect: true,
|
||||
universalOverrides: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { filterSystemSideEffectFlatViewFieldsToDelete } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/filter-system-side-effect-flat-view-fields-to-delete.util';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
|
||||
@Injectable()
|
||||
export class FieldSystemViewFieldsOnDeleteSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
name: 'fieldSystemViewFieldsOnDelete',
|
||||
description:
|
||||
'When a field is deleted, cascade-delete every engine-owned view field displaying it, wherever it lives: the INDEX view fields emitted by fieldIndexViewFieldOnCreate, and the record-page ones. Counterpart of fieldIndexViewFieldOnCreate: the engine authored those view fields, so it owns their deletion. Manifest deletion inference excludes isSystemSideEffect entities, so without this cascade they would only ever disappear through the viewField -> fieldMetadata foreign key, behind the engine back. Caller-authored view fields are NOT touched: they are deleted through normal deletion inference / the field delete transpiler.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatFieldMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'fieldMetadata'>): MetadataSideEffectResult {
|
||||
const viewFieldToDelete = filterSystemSideEffectFlatViewFieldsToDelete({
|
||||
viewFieldUniversalIdentifiers:
|
||||
flatFieldMetadata.viewFieldUniversalIdentifiers,
|
||||
flatViewFieldMaps: relatedFlatEntityMaps.flatViewFieldMaps,
|
||||
});
|
||||
|
||||
if (Object.keys(viewFieldToDelete).length === 0) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
viewField: {
|
||||
flatEntityToDelete: viewFieldToDelete,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -1,12 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { FieldIndexViewFieldOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service';
|
||||
import { FieldSearchFieldMetadataOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-search-field-metadata-on-delete-side-effect-handler.service';
|
||||
import { FieldSystemViewFieldsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-system-view-fields-on-delete-side-effect-handler.service';
|
||||
import { FieldUniqueBackingIndexOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-create-side-effect-handler.service';
|
||||
import { FieldUniqueBackingIndexOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-delete-side-effect-handler.service';
|
||||
import { FieldUniqueBackingIndexOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-update-side-effect-handler.service';
|
||||
import { ObjectSearchVectorOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-create-side-effect-handler.service';
|
||||
import { ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service';
|
||||
import { ObjectSearchVectorOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-update-side-effect-handler.service';
|
||||
import { ObjectSystemFieldsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-on-create-side-effect-handler.service';
|
||||
import { ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service';
|
||||
import { ObjectSystemRelationsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-relations-on-create-side-effect-handler.service';
|
||||
import { ObjectSystemRelationsOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-relations-on-update-side-effect-handler.service';
|
||||
import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-side-effects-on-delete-side-effect-handler.service';
|
||||
@@ -17,11 +20,14 @@ import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/eng
|
||||
FieldUniqueBackingIndexOnUpdateSideEffectHandlerService,
|
||||
FieldUniqueBackingIndexOnDeleteSideEffectHandlerService,
|
||||
FieldSearchFieldMetadataOnDeleteSideEffectHandlerService,
|
||||
ObjectSystemFieldsOnCreateSideEffectHandlerService,
|
||||
FieldIndexViewFieldOnCreateSideEffectHandlerService,
|
||||
FieldSystemViewFieldsOnDeleteSideEffectHandlerService,
|
||||
ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService,
|
||||
ObjectSystemRelationsOnCreateSideEffectHandlerService,
|
||||
ObjectSystemRelationsOnUpdateSideEffectHandlerService,
|
||||
ObjectSearchVectorOnCreateSideEffectHandlerService,
|
||||
ObjectSearchVectorOnUpdateSideEffectHandlerService,
|
||||
ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService,
|
||||
ObjectSystemSideEffectsOnDeleteSideEffectHandlerService,
|
||||
],
|
||||
})
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { ViewKey } from 'twenty-shared/types';
|
||||
|
||||
import { ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = 'd1d2d3d4-d5d6-4000-8000-000000000001';
|
||||
const CODE_FIELD_UNIVERSAL_IDENTIFIER = 'd1d2d3d4-d5d6-4000-8000-000000000002';
|
||||
|
||||
const INDEX_VIEW_UNIVERSAL_IDENTIFIER = getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
viewKey: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
const NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000001';
|
||||
const CODE_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000002';
|
||||
|
||||
const buildArgs = ({
|
||||
fromLabelIdentifier,
|
||||
toLabelIdentifier,
|
||||
viewFieldsInWorkspace = [],
|
||||
}: {
|
||||
fromLabelIdentifier: string | null;
|
||||
toLabelIdentifier: string | null;
|
||||
viewFieldsInWorkspace?: {
|
||||
universalIdentifier: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
}[];
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: toLabelIdentifier,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {},
|
||||
relatedFlatEntityMaps: {
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[OBJECT_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
fromLabelIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
flatViewMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[INDEX_VIEW_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
viewFieldUniversalIdentifiers: viewFieldsInWorkspace.map(
|
||||
(viewField) => viewField.universalIdentifier,
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
viewFieldsInWorkspace.map((viewField) => [
|
||||
viewField.universalIdentifier,
|
||||
{
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
isSystemSideEffect: true,
|
||||
...viewField,
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
const SYNCED_VIEW_FIELDS = [
|
||||
{
|
||||
universalIdentifier: NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CODE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
},
|
||||
];
|
||||
|
||||
describe('ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService as unknown as new () => ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService)();
|
||||
|
||||
it('should move the new label identifier view field below the lowest and make it visible', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
viewFieldsInWorkspace: SYNCED_VIEW_FIELDS,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const updatedCodeViewField = (result.operations.viewField
|
||||
?.flatEntityToUpdate ?? {})[CODE_VIEW_FIELD_UNIVERSAL_IDENTIFIER];
|
||||
|
||||
expect(updatedCodeViewField?.position).toBe(-1);
|
||||
expect(updatedCodeViewField?.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should noop when the label identifier is unchanged', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
viewFieldsInWorkspace: SYNCED_VIEW_FIELDS,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should noop when the new label identifier view field is already visible and strictly lowest', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
viewFieldsInWorkspace: SYNCED_VIEW_FIELDS,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should noop when the new label identifier has no synced INDEX view field (created in the same sync)', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
viewFieldsInWorkspace: [SYNCED_VIEW_FIELDS[0]],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
getSystemViewUniversalIdentifier,
|
||||
getViewFieldUniversalIdentifier,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
const OTHER_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'b1b2b3b4-b5b6-4000-8000-000000000002';
|
||||
|
||||
const computeFieldUniversalIdentifier = (name: string) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
});
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = computeFieldUniversalIdentifier('name');
|
||||
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
computeFieldUniversalIdentifier('searchVector');
|
||||
|
||||
const ALL_SYSTEM_FIELD_NAMES = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
] as const;
|
||||
|
||||
// The subset of system fields the INDEX view displays: id/deletedAt/position are
|
||||
// filtered out by the view-field builder.
|
||||
const DISPLAYABLE_SYSTEM_FIELD_NAMES = [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
] as const;
|
||||
|
||||
const DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
viewKey: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
type PendingFieldMetadata = {
|
||||
universalIdentifier: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
};
|
||||
|
||||
const NAME_FIELD: PendingFieldMetadata = {
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const buildArgs = ({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier = NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas = [],
|
||||
pendingViewUniversalIdentifiers = [],
|
||||
}: {
|
||||
labelIdentifierFieldMetadataUniversalIdentifier?: string | null;
|
||||
pendingFieldMetadatas?: PendingFieldMetadata[];
|
||||
pendingViewUniversalIdentifiers?: string[];
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'ticket',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingFieldMetadatas.map((pendingFieldMetadata) => [
|
||||
pendingFieldMetadata.universalIdentifier,
|
||||
pendingFieldMetadata,
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
...(pendingViewUniversalIdentifiers.length > 0 && {
|
||||
view: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingViewUniversalIdentifiers.map((universalIdentifier) => [
|
||||
universalIdentifier,
|
||||
{ universalIdentifier },
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
}),
|
||||
} as unknown as AllFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps: {},
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
describe('ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService as unknown as new () => ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService)();
|
||||
|
||||
it('should synthesize exactly the 7 reserved system fields, never the caller name nor searchVector', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({ pendingFieldMetadatas: [NAME_FIELD] }),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const createdUniversalIdentifiers = Object.keys(
|
||||
result.operations.fieldMetadata?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(createdUniversalIdentifiers).toHaveLength(7);
|
||||
expect(createdUniversalIdentifiers).not.toContain(
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(createdUniversalIdentifiers).not.toContain(
|
||||
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
for (const name of ALL_SYSTEM_FIELD_NAMES) {
|
||||
expect(createdUniversalIdentifiers).toContain(
|
||||
computeFieldUniversalIdentifier(name),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should provision the INDEX view and the system-field view fields only, positioned after the displayable caller fields', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({ pendingFieldMetadatas: [NAME_FIELD] }),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewUniversalIdentifiers = Object.keys(
|
||||
result.operations.view?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(viewUniversalIdentifiers).toEqual([
|
||||
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
]);
|
||||
expect(
|
||||
(result.operations.view?.flatEntityToCreate ?? {})[
|
||||
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER
|
||||
]?.isSystemSideEffect,
|
||||
).toBe(true);
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
// Only the 4 displayable system fields (id, deletedAt and position are
|
||||
// filtered out); the caller name view field is owned by the field creation
|
||||
// side effect handler.
|
||||
const expectedFieldUniversalIdentifiers =
|
||||
DISPLAYABLE_SYSTEM_FIELD_NAMES.map(computeFieldUniversalIdentifier);
|
||||
|
||||
expect(
|
||||
viewFields
|
||||
.map((viewField) => viewField.fieldMetadataUniversalIdentifier)
|
||||
.sort(),
|
||||
).toEqual([...expectedFieldUniversalIdentifiers].sort());
|
||||
|
||||
// Positions are offset past the single displayable caller field (name),
|
||||
// whose view field the field handler emits at position 0.
|
||||
expect(
|
||||
viewFields.map((viewField) => viewField.position).sort((a, b) => a - b),
|
||||
).toEqual([1, 2, 3, 4]);
|
||||
|
||||
for (const viewField of viewFields) {
|
||||
expect(viewField.isSystemSideEffect).toBe(true);
|
||||
expect(viewField.universalIdentifier).toBe(
|
||||
getViewFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
viewField.fieldMetadataUniversalIdentifier,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should start system view field positions at 0 when there is no displayable caller field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
pendingFieldMetadatas: [],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})
|
||||
.map((viewField) => viewField.position)
|
||||
.sort((a, b) => a - b),
|
||||
).toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
// The engine is the sole owner of the INDEX view and always emits it; a
|
||||
// caller providing one with the same identifier is a conflict left to the
|
||||
// engine collision downstream, not silently deferred here.
|
||||
it('should still emit its own INDEX view even when the caller provides one with the same identifier', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
pendingFieldMetadatas: [NAME_FIELD],
|
||||
pendingViewUniversalIdentifiers: [
|
||||
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.fieldMetadata?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(7);
|
||||
expect(
|
||||
Object.keys(result.operations.view?.flatEntityToCreate ?? {}),
|
||||
).toEqual([DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER]);
|
||||
expect(
|
||||
Object.values(result.operations.viewField?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should ignore caller fields belonging to other objects when computing the position offset', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
pendingFieldMetadatas: [
|
||||
NAME_FIELD,
|
||||
{
|
||||
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000001',
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'otherObjectField',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(
|
||||
viewFields.map((viewField) => viewField.fieldMetadataUniversalIdentifier),
|
||||
).not.toContain('c1c2c3c4-c5c6-4000-8000-000000000001');
|
||||
|
||||
// Offset is 1 (only the name field belongs to the created object), not 2.
|
||||
expect(
|
||||
viewFields.map((viewField) => viewField.position).sort((a, b) => a - b),
|
||||
).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
// An object with no name field (e.g. a manifest object) has its label
|
||||
// identifier backed by the id system field; its view field must still be
|
||||
// strictly lowest, ahead of the displayable caller fields.
|
||||
it('should place the id label-identifier system view field at the strictly lowest position', () => {
|
||||
const idFieldUniversalIdentifier = computeFieldUniversalIdentifier('id');
|
||||
const callerField: PendingFieldMetadata = {
|
||||
universalIdentifier: computeFieldUniversalIdentifier('firstKey'),
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'firstKey',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
idFieldUniversalIdentifier,
|
||||
pendingFieldMetadatas: [callerField],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const viewFields = Object.values(
|
||||
result.operations.viewField?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
const idViewField = viewFields.find(
|
||||
(viewField) =>
|
||||
viewField.fieldMetadataUniversalIdentifier ===
|
||||
idFieldUniversalIdentifier,
|
||||
);
|
||||
|
||||
expect(idViewField?.position).toBe(0);
|
||||
for (const viewField of viewFields) {
|
||||
if (
|
||||
viewField.fieldMetadataUniversalIdentifier !==
|
||||
idFieldUniversalIdentifier
|
||||
) {
|
||||
expect(viewField.position).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { ObjectSystemFieldsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-on-create-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
const SYSTEM_FIELD_NAMES = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
] as const;
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'searchVector',
|
||||
});
|
||||
|
||||
const buildArgs = ({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName = {} as unknown as AllFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps = {},
|
||||
}: {
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string;
|
||||
allFlatEntityOperationRecordByMetadataName?: AllFlatEntityOperationRecordByMetadataName;
|
||||
relatedFlatEntityMaps?: object;
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
describe('ObjectSystemFieldsOnCreateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectSystemFieldsOnCreateSideEffectHandlerService as unknown as new () => ObjectSystemFieldsOnCreateSideEffectHandlerService)();
|
||||
|
||||
it('should synthesize exactly the 7 reserved system fields and never the caller-provided name field nor the searchVector field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const createdUniversalIdentifiers = Object.keys(
|
||||
result.operations.fieldMetadata?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(createdUniversalIdentifiers).toHaveLength(7);
|
||||
expect(createdUniversalIdentifiers).not.toContain(
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(createdUniversalIdentifiers).not.toContain(
|
||||
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
for (const name of SYSTEM_FIELD_NAMES) {
|
||||
expect(createdUniversalIdentifiers).toContain(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should still emit all system fields even when they already exist in the from-state (dedup delegated to the engine merge)', () => {
|
||||
const alreadyPresentByUniversalIdentifier: Record<string, unknown> = {};
|
||||
|
||||
for (const name of SYSTEM_FIELD_NAMES) {
|
||||
const universalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
});
|
||||
|
||||
alreadyPresentByUniversalIdentifier[universalIdentifier] = {
|
||||
universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: alreadyPresentByUniversalIdentifier,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.fieldMetadata?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(7);
|
||||
});
|
||||
});
|
||||
+220
-56
@@ -1,5 +1,5 @@
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-side-effects-on-delete-side-effect-handler.service';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
const OTHER_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
@@ -9,21 +9,81 @@ const SYSTEM_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000001';
|
||||
const AUTHOR_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000002';
|
||||
const FORWARD_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000003';
|
||||
const REVERSE_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000004';
|
||||
const GIN_INDEX_UNIVERSAL_IDENTIFIER = 'e1e2e3e4-e5e6-4000-8000-000000000001';
|
||||
const REVERSE_INDEX_UNIVERSAL_IDENTIFIER =
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000002';
|
||||
const SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER =
|
||||
'f1f2f3f4-f5f6-4000-8000-000000000001';
|
||||
const INDEX_VIEW_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a1a2a3a4-a5a6-4000-8000-000000000002';
|
||||
const CALLER_VIEW_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000003';
|
||||
const CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a1a2a3a4-a5a6-4000-8000-000000000004';
|
||||
|
||||
type FieldFixture = {
|
||||
universalIdentifier: string;
|
||||
isSystemSideEffect: boolean;
|
||||
objectMetadataUniversalIdentifier?: string;
|
||||
relationTargetFieldMetadataUniversalIdentifier?: string | null;
|
||||
viewFieldUniversalIdentifiers?: string[];
|
||||
};
|
||||
|
||||
type ViewFixture = {
|
||||
universalIdentifier: string;
|
||||
isSystemSideEffect: boolean;
|
||||
viewFieldUniversalIdentifiers?: string[];
|
||||
};
|
||||
|
||||
const buildFieldMetadataMaps = (fields: FieldFixture[]) => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
fields.map((field) => [
|
||||
field.universalIdentifier,
|
||||
{
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
...field,
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
const buildArgs = ({
|
||||
relatedFlatEntityMaps,
|
||||
fieldUniversalIdentifiers = [],
|
||||
indexMetadataUniversalIdentifiers = [],
|
||||
searchFieldMetadataUniversalIdentifiers = [],
|
||||
viewUniversalIdentifiers = [],
|
||||
relatedFlatEntityMaps = {},
|
||||
}: {
|
||||
relatedFlatEntityMaps: object;
|
||||
fieldUniversalIdentifiers?: string[];
|
||||
indexMetadataUniversalIdentifiers?: string[];
|
||||
searchFieldMetadataUniversalIdentifiers?: string[];
|
||||
viewUniversalIdentifiers?: string[];
|
||||
relatedFlatEntityMaps?: object;
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
fieldUniversalIdentifiers,
|
||||
indexMetadataUniversalIdentifiers,
|
||||
searchFieldMetadataUniversalIdentifiers,
|
||||
viewUniversalIdentifiers,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {},
|
||||
relatedFlatEntityMaps,
|
||||
relatedFlatEntityMaps: {
|
||||
flatObjectMetadataMaps: { byUniversalIdentifier: {} },
|
||||
flatFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
flatIndexMaps: { byUniversalIdentifier: {} },
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
flatViewMaps: { byUniversalIdentifier: {} },
|
||||
flatViewFieldMaps: { byUniversalIdentifier: {} },
|
||||
...relatedFlatEntityMaps,
|
||||
},
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
@@ -34,23 +94,25 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
|
||||
it('should cascade-delete engine-owned fields, indexes and searchFieldMetadata while leaving author fields untouched', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fieldUniversalIdentifiers: [
|
||||
SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
indexMetadataUniversalIdentifiers: [GIN_INDEX_UNIVERSAL_IDENTIFIER],
|
||||
searchFieldMetadataUniversalIdentifiers: [
|
||||
SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[SYSTEM_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
},
|
||||
[AUTHOR_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
},
|
||||
flatFieldMetadataMaps: buildFieldMetadataMaps([
|
||||
{
|
||||
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
},
|
||||
]),
|
||||
flatIndexMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[GIN_INDEX_UNIVERSAL_IDENTIFIER]: {
|
||||
@@ -65,7 +127,6 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
|
||||
byUniversalIdentifier: {
|
||||
[SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -100,35 +161,34 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
|
||||
});
|
||||
|
||||
it('should cascade-delete the default-relation reverse morph field on a standard object and its join-column index', () => {
|
||||
const FORWARD_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000003';
|
||||
const REVERSE_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000004';
|
||||
const REVERSE_INDEX_UNIVERSAL_IDENTIFIER =
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000002';
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fieldUniversalIdentifiers: [FORWARD_FIELD_UNIVERSAL_IDENTIFIER],
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[FORWARD_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: FORWARD_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
[REVERSE_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
[OTHER_OBJECT_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
indexMetadataUniversalIdentifiers: [
|
||||
REVERSE_INDEX_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
flatFieldMetadataMaps: buildFieldMetadataMaps([
|
||||
{
|
||||
universalIdentifier: FORWARD_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
{
|
||||
universalIdentifier: REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
]),
|
||||
flatIndexMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[REVERSE_INDEX_UNIVERSAL_IDENTIFIER]: {
|
||||
@@ -145,7 +205,6 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -167,27 +226,132 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
|
||||
).toEqual([REVERSE_INDEX_UNIVERSAL_IDENTIFIER]);
|
||||
});
|
||||
|
||||
it('should not delete entities belonging to a different object', () => {
|
||||
it('should not delete entities the object aggregators do not reference', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[SYSTEM_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
},
|
||||
flatFieldMetadataMaps: buildFieldMetadataMaps([
|
||||
{
|
||||
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
flatIndexMaps: { byUniversalIdentifier: {} },
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
]),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should cascade-delete the engine-owned INDEX view and its view fields while leaving a caller-owned view untouched', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
viewUniversalIdentifiers: [
|
||||
INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
CALLER_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
relatedFlatEntityMaps: {
|
||||
flatViewMaps: {
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
(
|
||||
[
|
||||
{
|
||||
universalIdentifier: INDEX_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
viewFieldUniversalIdentifiers: [
|
||||
INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: CALLER_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
viewFieldUniversalIdentifiers: [
|
||||
CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
},
|
||||
] satisfies ViewFixture[]
|
||||
).map((view) => [view.universalIdentifier, view]),
|
||||
),
|
||||
},
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
},
|
||||
[CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.view?.flatEntityToDelete ?? {}),
|
||||
).toEqual([INDEX_VIEW_UNIVERSAL_IDENTIFIER]);
|
||||
expect(
|
||||
Object.keys(result.operations.viewField?.flatEntityToDelete ?? {}),
|
||||
).toEqual([INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER]);
|
||||
});
|
||||
|
||||
it('should cascade-delete a system view field of a deleted field even when it lives on another object view', () => {
|
||||
const OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a1a2a3a4-a5a6-4000-8000-000000000006';
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fieldUniversalIdentifiers: [FORWARD_FIELD_UNIVERSAL_IDENTIFIER],
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: buildFieldMetadataMaps([
|
||||
{
|
||||
universalIdentifier: FORWARD_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
{
|
||||
universalIdentifier: REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
viewFieldUniversalIdentifiers: [
|
||||
OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
},
|
||||
]),
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier:
|
||||
OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(result.operations.view).toBeUndefined();
|
||||
expect(
|
||||
Object.keys(result.operations.viewField?.flatEntityToDelete ?? {}),
|
||||
).toEqual([OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER]);
|
||||
});
|
||||
});
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { ViewKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { MetadataSideEffectExceptionCode } from 'src/engine/metadata-modules/metadata-side-effect/exceptions/metadata-side-effect-exception-code';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'update',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectIndexViewLabelIdentifierOnUpdate',
|
||||
description:
|
||||
'When an object label identifier changes onto a pre-existing field, keep that field INDEX view field strictly lowest and visible, as the flat view field validator requires. The engine owns the INDEX view, so this is a system side effect on both the API and manifest-sync paths, and only the engine-owned view field is touched. Relabeling onto a field created in the same operation is handled by fieldIndexViewFieldOnCreate instead (its view field does not exist yet to be updated), so this handler considers only synced view fields. Noop when the label identifier is unchanged, when the object has no engine-owned INDEX view, when the new label identifier has no synced INDEX view field, or when that view field is already visible and strictly lowest.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const updatedFlatObjectMetadata = flatEntity as UniversalFlatObjectMetadata;
|
||||
|
||||
const existingFlatObjectMetadata =
|
||||
relatedFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
updatedFlatObjectMetadata.universalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(existingFlatObjectMetadata)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
type: 'update',
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier: updatedFlatObjectMetadata.universalIdentifier,
|
||||
} as Partial<MetadataFlatEntity<'objectMetadata'>>,
|
||||
errors: [
|
||||
{
|
||||
code: MetadataSideEffectExceptionCode.SIDE_EFFECT_PARENT_METADATA_NOT_FOUND,
|
||||
message: t`Could not resolve the existing object to reconcile its INDEX label identifier view field`,
|
||||
userFriendlyMessage: msg`The object to update could not be found to reconcile its INDEX view`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const newLabelIdentifierFieldMetadataUniversalIdentifier =
|
||||
updatedFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (
|
||||
existingFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
newLabelIdentifierFieldMetadataUniversalIdentifier ||
|
||||
!isDefined(newLabelIdentifierFieldMetadataUniversalIdentifier)
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const indexViewUniversalIdentifier = getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
updatedFlatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: updatedFlatObjectMetadata.universalIdentifier,
|
||||
viewKey: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
const indexFlatView =
|
||||
relatedFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
|
||||
indexViewUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(indexFlatView) ||
|
||||
indexFlatView.isSystemSideEffect !== true ||
|
||||
!indexFlatView.isActive ||
|
||||
isDefined(indexFlatView.deletedAt)
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const indexFlatViewFields = indexFlatView.viewFieldUniversalIdentifiers
|
||||
.map(
|
||||
(viewFieldUniversalIdentifier) =>
|
||||
relatedFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier[
|
||||
viewFieldUniversalIdentifier
|
||||
],
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(flatViewField) =>
|
||||
flatViewField.isActive && !isDefined(flatViewField.deletedAt),
|
||||
) as UniversalFlatViewField[];
|
||||
|
||||
const labelIdentifierFlatViewField = indexFlatViewFields.find(
|
||||
(flatViewField) =>
|
||||
flatViewField.fieldMetadataUniversalIdentifier ===
|
||||
newLabelIdentifierFieldMetadataUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (!isDefined(labelIdentifierFlatViewField)) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const otherPositions = indexFlatViewFields
|
||||
.filter(
|
||||
(flatViewField) =>
|
||||
flatViewField.universalIdentifier !==
|
||||
labelIdentifierFlatViewField.universalIdentifier,
|
||||
)
|
||||
.map((flatViewField) => flatViewField.position);
|
||||
|
||||
const isAlreadyLowestAndVisible =
|
||||
labelIdentifierFlatViewField.isVisible &&
|
||||
(otherPositions.length === 0 ||
|
||||
labelIdentifierFlatViewField.position < Math.min(...otherPositions));
|
||||
|
||||
if (isAlreadyLowestAndVisible) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const targetPosition =
|
||||
otherPositions.length === 0 ? 0 : Math.min(...otherPositions) - 1;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
viewField: {
|
||||
flatEntityToUpdate: {
|
||||
[labelIdentifierFlatViewField.universalIdentifier]: {
|
||||
...labelIdentifierFlatViewField,
|
||||
position: targetPosition,
|
||||
isVisible: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { fromArrayToUniqueKeyRecord } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import { computeCallerFlatFieldMetadatasForObject } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util';
|
||||
import { computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
|
||||
import { computeFlatIndexViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-index-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSystemFieldsAndIndexViewOnCreate',
|
||||
description:
|
||||
'When an object is created, provision (1) its 7 reserved system fields (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position; searchVector is handled by objectSearchVectorOnCreate, and the name field is caller-provided) and (2) its default INDEX table view ("All {objectLabelPlural}") with one view field per displayable SYSTEM field only, all isSystemSideEffect so the engine owns their lifecycle. View fields for caller-provided fields are owned by fieldIndexViewFieldOnCreate (the field creation side effect), which positions them before the system view fields; both handlers derive positions from the same caller-input list so the layout is contiguous without any ordering dependency. The view identifier is name-free (object identifier + INDEX view key), so an object rename keeps the same view. The engine is the sole owner of the INDEX view and always emits it: the flat view validator rejects caller-created INDEX views (caller inputs are forced isSystemSideEffect: false) and enforces a single non-deleted INDEX view per object. twenty-standard is not concerned: it synchronizes through the from/to migration path, which never runs the side-effect engine, and authors its own curated INDEX view/fields.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatObjectMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const sourceFlatObjectMetadata =
|
||||
flatObjectMetadata as UniversalFlatObjectMetadata;
|
||||
const { applicationUniversalIdentifier, universalIdentifier } =
|
||||
sourceFlatObjectMetadata;
|
||||
|
||||
const systemFlatFieldMetadatas = Object.values(
|
||||
buildReservedSystemFlatFieldMetadatasForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const systemFieldMetadataToCreate: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'fieldMetadata'>
|
||||
> = {};
|
||||
|
||||
for (const systemFlatFieldMetadata of systemFlatFieldMetadatas) {
|
||||
systemFieldMetadataToCreate[systemFlatFieldMetadata.universalIdentifier] =
|
||||
systemFlatFieldMetadata;
|
||||
}
|
||||
|
||||
const flatIndexViewToCreate = computeFlatIndexViewToCreate({
|
||||
objectMetadata: sourceFlatObjectMetadata,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const displayableCallerFlatFieldMetadatas =
|
||||
computeCallerFlatFieldMetadatasForObject({
|
||||
objectMetadataUniversalIdentifier: universalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
displayableOnly: true,
|
||||
});
|
||||
|
||||
const positionByFieldUniversalIdentifier =
|
||||
computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier: universalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
displayableCallerFlatFieldMetadatas,
|
||||
});
|
||||
|
||||
const flatViewFieldsToCreate = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: systemFlatFieldMetadatas,
|
||||
viewUniversalIdentifier: flatIndexViewToCreate.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}).map((flatViewFieldToCreate) => ({
|
||||
...flatViewFieldToCreate,
|
||||
position:
|
||||
positionByFieldUniversalIdentifier.get(
|
||||
flatViewFieldToCreate.fieldMetadataUniversalIdentifier,
|
||||
) ?? flatViewFieldToCreate.position,
|
||||
}));
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
fieldMetadata: { flatEntityToCreate: systemFieldMetadataToCreate },
|
||||
view: {
|
||||
flatEntityToCreate: {
|
||||
[flatIndexViewToCreate.universalIdentifier]: flatIndexViewToCreate,
|
||||
},
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: fromArrayToUniqueKeyRecord({
|
||||
array: flatViewFieldsToCreate,
|
||||
uniqueKey: 'universalIdentifier',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSystemFieldsOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSystemFieldsOnCreate',
|
||||
description:
|
||||
'When an object is created, generate its 7 reserved system fields (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position). The searchVector field is provisioned by the self-contained objectSearchVectorOnCreate handler alongside its GIN index and searchFieldMetadata. The default name field is NOT synthesized here: it is a caller-provided default field (SDK auto-complete on the manifest path, input transpiler on the API path).',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatObjectMetadata,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const { applicationUniversalIdentifier, universalIdentifier } =
|
||||
flatObjectMetadata;
|
||||
|
||||
const reservedSystemFlatFieldMetadatas =
|
||||
buildReservedSystemFlatFieldMetadatasForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
const flatEntityToCreate: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'fieldMetadata'>
|
||||
> = {};
|
||||
|
||||
for (const flatFieldMetadata of Object.values(
|
||||
reservedSystemFlatFieldMetadatas,
|
||||
)) {
|
||||
flatEntityToCreate[flatFieldMetadata.universalIdentifier] =
|
||||
flatFieldMetadata;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+234
-99
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import { filterSystemSideEffectFlatViewFieldsToDelete } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/filter-system-side-effect-flat-view-fields-to-delete.util';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
@@ -10,6 +11,18 @@ import {
|
||||
import { type MetadataSideEffectOperationsByMetadataName } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-operations-by-metadata-name.type';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
|
||||
type RelatedFlatEntityMaps =
|
||||
BuildSideEffectsArgs<'objectMetadata'>['relatedFlatEntityMaps'];
|
||||
|
||||
type FlatEntityToDelete<
|
||||
TMetadataName extends
|
||||
| 'fieldMetadata'
|
||||
| 'index'
|
||||
| 'searchFieldMetadata'
|
||||
| 'view'
|
||||
| 'viewField',
|
||||
> = Record<string, MetadataUniversalFlatEntity<TMetadataName>>;
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSystemSideEffectsOnDeleteSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
@@ -17,139 +30,261 @@ export class ObjectSystemSideEffectsOnDeleteSideEffectHandlerService extends Met
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSystemSideEffectsOnDelete',
|
||||
description:
|
||||
'When an object is deleted, cascade-delete its engine-owned side effects: the reserved system fields, the default relation fields (forward field on the deleted object and reverse morph field on the standard object), every system index (reverse join-column indexes, the GIN searchVector index), and its searchFieldMetadata rows. The engine is the sole authority for isSystemSideEffect entities on delete: the API object delete transpiler cascades only user-authored fields and indexes, and manifest deletion inference excludes these entities entirely. Caller-provided defaults (e.g. the name field) are NOT engine-owned and are deleted through normal deletion inference / the object delete transpiler.',
|
||||
'When an object is deleted, cascade-delete its engine-owned side effects: the reserved system fields, the default relation fields (forward field on the deleted object and reverse morph field on the standard object), every system index (reverse join-column indexes, the GIN searchVector index), its searchFieldMetadata rows, and its engine-owned views (the INDEX table view provisioned by objectSystemFieldsAndIndexViewOnCreate) with their view fields. View fields of the deleted system fields are cascaded too even when they live on another object view, which happens for the reverse relation fields. Every lookup walks a foreign key aggregator down from the deleted object (its fields, indexes, searchFieldMetadatas, views, then their view fields) and indexes into the flat entity maps, so the work is proportional to what the object owns and never to the size of the workspace. The engine is the sole authority for isSystemSideEffect entities on delete: the API object delete transpiler cascades only user-authored fields and indexes, and manifest deletion inference excludes these entities entirely. Caller-provided defaults (e.g. the name field) are NOT engine-owned and are deleted through normal deletion inference / the object delete transpiler.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const objectMetadataUniversalIdentifier =
|
||||
flatObjectMetadata.universalIdentifier;
|
||||
const fieldMetadataToDelete = this.computeFieldMetadataToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
const flatFieldMetadatasToDelete = Object.values(fieldMetadataToDelete);
|
||||
|
||||
const fieldMetadataToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'fieldMetadata'>
|
||||
> = {};
|
||||
const deletedFieldUniversalIdentifiers = new Set<string>();
|
||||
const viewToDelete = this.computeViewToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
|
||||
for (const flatFieldMetadata of Object.values(
|
||||
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
const flatEntityToDeleteByMetadataName = {
|
||||
fieldMetadata: fieldMetadataToDelete,
|
||||
index: this.computeIndexToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
flatFieldMetadatasToDelete,
|
||||
}),
|
||||
searchFieldMetadata: this.computeSearchFieldMetadataToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}),
|
||||
view: viewToDelete,
|
||||
viewField: this.computeViewFieldToDelete({
|
||||
relatedFlatEntityMaps,
|
||||
flatViewsToDelete: Object.values(viewToDelete),
|
||||
flatFieldMetadatasToDelete,
|
||||
}),
|
||||
};
|
||||
|
||||
if (flatFieldMetadata.isSystemSideEffect !== true) {
|
||||
continue;
|
||||
}
|
||||
const operations = Object.fromEntries(
|
||||
Object.entries(flatEntityToDeleteByMetadataName)
|
||||
.filter(
|
||||
([, flatEntityToDelete]) =>
|
||||
Object.keys(flatEntityToDelete).length > 0,
|
||||
)
|
||||
.map(([metadataName, flatEntityToDelete]) => [
|
||||
metadataName,
|
||||
{ flatEntityToDelete },
|
||||
]),
|
||||
) as MetadataSideEffectOperationsByMetadataName;
|
||||
|
||||
const belongsToObject =
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier;
|
||||
const targetsObject =
|
||||
flatFieldMetadata.relationTargetObjectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier;
|
||||
if (Object.keys(operations).length === 0) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
if (!belongsToObject && !targetsObject) {
|
||||
return {
|
||||
status: 'success',
|
||||
operations,
|
||||
};
|
||||
}
|
||||
|
||||
private computeFieldMetadataToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
|
||||
relatedFlatEntityMaps: RelatedFlatEntityMaps;
|
||||
}): FlatEntityToDelete<'fieldMetadata'> {
|
||||
const fieldMetadataToDelete: FlatEntityToDelete<'fieldMetadata'> = {};
|
||||
|
||||
for (const fieldUniversalIdentifier of flatObjectMetadata.fieldUniversalIdentifiers) {
|
||||
const flatFieldMetadata =
|
||||
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
fieldUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(flatFieldMetadata) ||
|
||||
flatFieldMetadata.isSystemSideEffect !== true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldMetadataToDelete[flatFieldMetadata.universalIdentifier] =
|
||||
flatFieldMetadata;
|
||||
deletedFieldUniversalIdentifiers.add(
|
||||
flatFieldMetadata.universalIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
const indexToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'index'>
|
||||
> = {};
|
||||
const { relationTargetFieldMetadataUniversalIdentifier } =
|
||||
flatFieldMetadata;
|
||||
|
||||
for (const flatIndexMetadata of Object.values(
|
||||
relatedFlatEntityMaps.flatIndexMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatIndexMetadata)) {
|
||||
if (!isDefined(relationTargetFieldMetadataUniversalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flatIndexMetadata.isSystemSideEffect !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const belongsToObject =
|
||||
flatIndexMetadata.objectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier;
|
||||
const referencesDeletedField =
|
||||
flatIndexMetadata.universalFlatIndexFieldMetadatas.some(
|
||||
(universalFlatIndexFieldMetadata) =>
|
||||
deletedFieldUniversalIdentifiers.has(
|
||||
universalFlatIndexFieldMetadata.fieldMetadataUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
if (!belongsToObject && !referencesDeletedField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
indexToDelete[flatIndexMetadata.universalIdentifier] = flatIndexMetadata;
|
||||
}
|
||||
|
||||
const searchFieldMetadataToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'searchFieldMetadata'>
|
||||
> = {};
|
||||
|
||||
for (const flatSearchFieldMetadata of Object.values(
|
||||
relatedFlatEntityMaps.flatSearchFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatSearchFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
const reverseFlatFieldMetadata =
|
||||
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
relationTargetFieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
flatSearchFieldMetadata.objectMetadataUniversalIdentifier !==
|
||||
objectMetadataUniversalIdentifier
|
||||
!isDefined(reverseFlatFieldMetadata) ||
|
||||
reverseFlatFieldMetadata.isSystemSideEffect !== true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldMetadataToDelete[reverseFlatFieldMetadata.universalIdentifier] =
|
||||
reverseFlatFieldMetadata;
|
||||
}
|
||||
|
||||
return fieldMetadataToDelete;
|
||||
}
|
||||
|
||||
private computeIndexToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
flatFieldMetadatasToDelete,
|
||||
}: {
|
||||
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
|
||||
relatedFlatEntityMaps: RelatedFlatEntityMaps;
|
||||
flatFieldMetadatasToDelete: MetadataUniversalFlatEntity<'fieldMetadata'>[];
|
||||
}): FlatEntityToDelete<'index'> {
|
||||
const deletedFieldUniversalIdentifiers = new Set(
|
||||
flatFieldMetadatasToDelete.map(
|
||||
(flatFieldMetadata) => flatFieldMetadata.universalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const indexOwnerFlatObjectMetadataByUniversalIdentifier = new Map([
|
||||
[flatObjectMetadata.universalIdentifier, flatObjectMetadata],
|
||||
]);
|
||||
|
||||
for (const flatFieldMetadata of flatFieldMetadatasToDelete) {
|
||||
const ownerFlatObjectMetadata =
|
||||
relatedFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(ownerFlatObjectMetadata)) {
|
||||
indexOwnerFlatObjectMetadataByUniversalIdentifier.set(
|
||||
ownerFlatObjectMetadata.universalIdentifier,
|
||||
ownerFlatObjectMetadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const indexToDelete: FlatEntityToDelete<'index'> = {};
|
||||
|
||||
for (const indexOwnerFlatObjectMetadata of indexOwnerFlatObjectMetadataByUniversalIdentifier.values()) {
|
||||
for (const indexUniversalIdentifier of indexOwnerFlatObjectMetadata.indexMetadataUniversalIdentifiers) {
|
||||
const flatIndexMetadata =
|
||||
relatedFlatEntityMaps.flatIndexMaps.byUniversalIdentifier[
|
||||
indexUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(flatIndexMetadata) ||
|
||||
flatIndexMetadata.isSystemSideEffect !== true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const belongsToObject =
|
||||
flatIndexMetadata.objectMetadataUniversalIdentifier ===
|
||||
flatObjectMetadata.universalIdentifier;
|
||||
const referencesDeletedField =
|
||||
flatIndexMetadata.universalFlatIndexFieldMetadatas.some(
|
||||
(universalFlatIndexFieldMetadata) =>
|
||||
deletedFieldUniversalIdentifiers.has(
|
||||
universalFlatIndexFieldMetadata.fieldMetadataUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
if (!belongsToObject && !referencesDeletedField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
indexToDelete[flatIndexMetadata.universalIdentifier] =
|
||||
flatIndexMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return indexToDelete;
|
||||
}
|
||||
|
||||
private computeSearchFieldMetadataToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
|
||||
relatedFlatEntityMaps: RelatedFlatEntityMaps;
|
||||
}): FlatEntityToDelete<'searchFieldMetadata'> {
|
||||
const searchFieldMetadataToDelete: FlatEntityToDelete<'searchFieldMetadata'> =
|
||||
{};
|
||||
|
||||
for (const searchFieldMetadataUniversalIdentifier of flatObjectMetadata.searchFieldMetadataUniversalIdentifiers) {
|
||||
const flatSearchFieldMetadata =
|
||||
relatedFlatEntityMaps.flatSearchFieldMetadataMaps.byUniversalIdentifier[
|
||||
searchFieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(flatSearchFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchFieldMetadataToDelete[flatSearchFieldMetadata.universalIdentifier] =
|
||||
flatSearchFieldMetadata;
|
||||
}
|
||||
|
||||
const hasFieldMetadataToDelete =
|
||||
Object.keys(fieldMetadataToDelete).length > 0;
|
||||
const hasIndexToDelete = Object.keys(indexToDelete).length > 0;
|
||||
const hasSearchFieldMetadataToDelete =
|
||||
Object.keys(searchFieldMetadataToDelete).length > 0;
|
||||
return searchFieldMetadataToDelete;
|
||||
}
|
||||
|
||||
if (
|
||||
!hasFieldMetadataToDelete &&
|
||||
!hasIndexToDelete &&
|
||||
!hasSearchFieldMetadataToDelete
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
private computeViewToDelete({
|
||||
flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
|
||||
relatedFlatEntityMaps: RelatedFlatEntityMaps;
|
||||
}): FlatEntityToDelete<'view'> {
|
||||
const viewToDelete: FlatEntityToDelete<'view'> = {};
|
||||
|
||||
for (const viewUniversalIdentifier of flatObjectMetadata.viewUniversalIdentifiers) {
|
||||
const flatView =
|
||||
relatedFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
|
||||
viewUniversalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(flatView) || flatView.isSystemSideEffect !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
viewToDelete[flatView.universalIdentifier] = flatView;
|
||||
}
|
||||
|
||||
const operations: MetadataSideEffectOperationsByMetadataName = {
|
||||
...(hasFieldMetadataToDelete && {
|
||||
fieldMetadata: { flatEntityToDelete: fieldMetadataToDelete },
|
||||
}),
|
||||
...(hasIndexToDelete && {
|
||||
index: { flatEntityToDelete: indexToDelete },
|
||||
}),
|
||||
...(hasSearchFieldMetadataToDelete && {
|
||||
searchFieldMetadata: {
|
||||
flatEntityToDelete: searchFieldMetadataToDelete,
|
||||
},
|
||||
}),
|
||||
};
|
||||
return viewToDelete;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations,
|
||||
};
|
||||
private computeViewFieldToDelete({
|
||||
relatedFlatEntityMaps,
|
||||
flatViewsToDelete,
|
||||
flatFieldMetadatasToDelete,
|
||||
}: {
|
||||
relatedFlatEntityMaps: RelatedFlatEntityMaps;
|
||||
flatViewsToDelete: MetadataUniversalFlatEntity<'view'>[];
|
||||
flatFieldMetadatasToDelete: MetadataUniversalFlatEntity<'fieldMetadata'>[];
|
||||
}): FlatEntityToDelete<'viewField'> {
|
||||
const viewFieldUniversalIdentifiers = [
|
||||
...flatViewsToDelete.flatMap(
|
||||
(flatView) => flatView.viewFieldUniversalIdentifiers,
|
||||
),
|
||||
...flatFieldMetadatasToDelete.flatMap(
|
||||
(flatFieldMetadata) => flatFieldMetadata.viewFieldUniversalIdentifiers,
|
||||
),
|
||||
];
|
||||
|
||||
return filterSystemSideEffectFlatViewFieldsToDelete({
|
||||
viewFieldUniversalIdentifiers,
|
||||
flatViewFieldMaps: relatedFlatEntityMaps.flatViewFieldMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
|
||||
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
||||
export const computeCallerFlatFieldMetadatasForObject = ({
|
||||
objectMetadataUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
displayableOnly,
|
||||
}: {
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
allFlatEntityOperationRecordByMetadataName: AllFlatEntityOperationRecordByMetadataName;
|
||||
displayableOnly: boolean;
|
||||
}): UniversalFlatFieldMetadata[] =>
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
flatFieldMetadatas: (
|
||||
Object.values(
|
||||
allFlatEntityOperationRecordByMetadataName.fieldMetadata
|
||||
?.flatEntityToCreate ?? {},
|
||||
) as UniversalFlatFieldMetadata[]
|
||||
).filter(
|
||||
(flatFieldMetadata) =>
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier &&
|
||||
!flatFieldMetadata.isSystemSideEffect &&
|
||||
(!displayableOnly ||
|
||||
isFlatFieldMetadataDisplayableInDefaultView({
|
||||
flatFieldMetadata,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
})),
|
||||
),
|
||||
});
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
|
||||
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
|
||||
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
||||
export const computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier = ({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
displayableCallerFlatFieldMetadatas,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
displayableCallerFlatFieldMetadatas: UniversalFlatFieldMetadata[];
|
||||
}): Map<string, number> => {
|
||||
const displayableSystemFlatFieldMetadatas = Object.values(
|
||||
buildReservedSystemFlatFieldMetadatasForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier: objectMetadataUniversalIdentifier,
|
||||
},
|
||||
}),
|
||||
).filter((flatFieldMetadata) =>
|
||||
isFlatFieldMetadataDisplayableInDefaultView({
|
||||
flatFieldMetadata,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}),
|
||||
);
|
||||
|
||||
const orderedDisplayableFlatFieldMetadatas =
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
flatFieldMetadatas: [
|
||||
...displayableCallerFlatFieldMetadatas,
|
||||
...displayableSystemFlatFieldMetadatas,
|
||||
],
|
||||
});
|
||||
|
||||
return new Map(
|
||||
orderedDisplayableFlatFieldMetadatas.map((flatFieldMetadata, position) => [
|
||||
flatFieldMetadata.universalIdentifier,
|
||||
position,
|
||||
]),
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
|
||||
|
||||
export const filterSystemSideEffectFlatViewFieldsToDelete = ({
|
||||
viewFieldUniversalIdentifiers,
|
||||
flatViewFieldMaps,
|
||||
}: {
|
||||
viewFieldUniversalIdentifiers: string[];
|
||||
flatViewFieldMaps: UniversalFlatEntityMaps<
|
||||
MetadataUniversalFlatEntity<'viewField'>
|
||||
>;
|
||||
}): Record<string, MetadataUniversalFlatEntity<'viewField'>> => {
|
||||
const viewFieldToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'viewField'>
|
||||
> = {};
|
||||
|
||||
for (const viewFieldUniversalIdentifier of viewFieldUniversalIdentifiers) {
|
||||
const flatViewField =
|
||||
flatViewFieldMaps.byUniversalIdentifier[viewFieldUniversalIdentifier];
|
||||
|
||||
if (
|
||||
!isDefined(flatViewField) ||
|
||||
flatViewField.isSystemSideEffect !== true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
viewFieldToDelete[flatViewField.universalIdentifier] = flatViewField;
|
||||
}
|
||||
|
||||
return viewFieldToDelete;
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { MetadataSideEffectExceptionCode } from 'src/engine/metadata-modules/metadata-side-effect/exceptions/metadata-side-effect-exception-code';
|
||||
import { MetadataSideEffectEngineService } from 'src/engine/metadata-modules/metadata-side-effect/services/metadata-side-effect-engine.service';
|
||||
|
||||
const RESERVED_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
const systemViewEmittingHandler = {
|
||||
operation: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'fakeSystemViewEmitter',
|
||||
buildSideEffects: () => ({
|
||||
status: 'success',
|
||||
operations: {
|
||||
view: {
|
||||
flatEntityToCreate: {
|
||||
[RESERVED_VIEW_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: RESERVED_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const buildEngine = () => {
|
||||
const registry = {
|
||||
getRegisteredHandlerKeys: () => [
|
||||
{ operation: 'create', metadataName: 'objectMetadata' },
|
||||
],
|
||||
getHandlers: (operation: string, metadataName: string) =>
|
||||
operation === 'create' && metadataName === 'objectMetadata'
|
||||
? [systemViewEmittingHandler]
|
||||
: [],
|
||||
};
|
||||
|
||||
return new MetadataSideEffectEngineService(registry as never);
|
||||
};
|
||||
|
||||
const buildInput = ({
|
||||
callerViewUniversalIdentifier,
|
||||
}: {
|
||||
callerViewUniversalIdentifier?: string;
|
||||
}) =>
|
||||
({
|
||||
objectMetadata: {
|
||||
flatEntityToCreate: {
|
||||
[OBJECT_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate:
|
||||
callerViewUniversalIdentifier === undefined
|
||||
? {}
|
||||
: {
|
||||
[callerViewUniversalIdentifier]: {
|
||||
universalIdentifier: callerViewUniversalIdentifier,
|
||||
isSystemSideEffect: false,
|
||||
},
|
||||
},
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
}) as never;
|
||||
|
||||
describe('MetadataSideEffectEngineService reserved identifier collision', () => {
|
||||
it('should reject a caller-defined entity whose universal identifier collides with a system side effect one', () => {
|
||||
const engine = buildEngine();
|
||||
|
||||
const result = engine.expandWithSideEffects({
|
||||
allFlatEntityOperationRecordByMetadataName: buildInput({
|
||||
callerViewUniversalIdentifier: RESERVED_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
}),
|
||||
sideEffectRelatedFlatEntityMaps: {},
|
||||
context: {} as never,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('fail');
|
||||
|
||||
if (result.status !== 'fail') {
|
||||
throw new Error('expected fail');
|
||||
}
|
||||
|
||||
const viewFailures = result.report.view;
|
||||
|
||||
expect(viewFailures).toHaveLength(1);
|
||||
expect(viewFailures[0].errors[0].code).toBe(
|
||||
MetadataSideEffectExceptionCode.RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should let a caller-defined entity through when its universal identifier does not collide', () => {
|
||||
const engine = buildEngine();
|
||||
|
||||
const result = engine.expandWithSideEffects({
|
||||
allFlatEntityOperationRecordByMetadataName: buildInput({
|
||||
callerViewUniversalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000001',
|
||||
}),
|
||||
sideEffectRelatedFlatEntityMaps: {},
|
||||
context: {} as never,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
});
|
||||
+10
-80
@@ -2,12 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import {
|
||||
ViewKey,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
import { v4, v5 } from 'uuid';
|
||||
@@ -489,10 +483,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
});
|
||||
|
||||
// Default view fields reference the caller-provided fields (reused as-is from
|
||||
// the transpiler output) plus the engine-owned reserved system fields, whose
|
||||
// deterministic identifiers we re-derive here (searchVector is never shown).
|
||||
// TODO: remove once default view fields move to the metadata side effect engine.
|
||||
// The INDEX table view and its view fields are provisioned by the metadata
|
||||
// side-effect engine (objectSystemFieldsAndIndexViewOnCreate). The FIELDS_WIDGET
|
||||
// record-page view below still references the caller-provided fields plus
|
||||
// the engine-owned reserved system fields, whose deterministic identifiers
|
||||
// we re-derive here (searchVector is never shown).
|
||||
// TODO: remove once the record-page view fields move to the side-effect engine.
|
||||
const defaultFlatFieldMetadatasForViewFields: UniversalFlatFieldMetadata[] =
|
||||
[
|
||||
...flatFieldMetadataToCreateOnObject,
|
||||
@@ -508,19 +504,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
),
|
||||
];
|
||||
|
||||
const flatDefaultViewToCreate = this.computeFlatViewToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
});
|
||||
|
||||
const flatDefaultViewFieldsToCreate = computeFlatViewFieldsToCreate({
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
objectFlatFieldMetadatas: defaultFlatFieldMetadatasForViewFields,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
|
||||
});
|
||||
|
||||
const flatNavigationMenuItemToCreate =
|
||||
await this.computeFlatNavigationMenuItemToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
@@ -557,7 +540,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
const flatRecordPageFieldsViewFieldsToCreate =
|
||||
computeFlatViewFieldsToCreate({
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
applicationUniversalIdentifier:
|
||||
resolvedOwnerFlatApplication.universalIdentifier,
|
||||
objectFlatFieldMetadatas: defaultFlatFieldMetadatasForViewFields,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
@@ -594,18 +578,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: [
|
||||
flatDefaultViewToCreate,
|
||||
flatRecordPageFieldsViewToCreate,
|
||||
],
|
||||
flatEntityToCreate: [flatRecordPageFieldsViewToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: [
|
||||
...flatDefaultViewFieldsToCreate,
|
||||
...flatRecordPageFieldsViewFieldsToCreate,
|
||||
],
|
||||
flatEntityToCreate: [...flatRecordPageFieldsViewFieldsToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
@@ -679,54 +657,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
return createdFlatObjectMetadata;
|
||||
}
|
||||
|
||||
private computeFlatViewToCreate({
|
||||
objectMetadata,
|
||||
flatApplication,
|
||||
}: {
|
||||
flatApplication: FlatApplication;
|
||||
objectMetadata: UniversalFlatObjectMetadata & { id: string };
|
||||
}): UniversalFlatView & { id: string } {
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier,
|
||||
name: `All {objectLabelPlural}`,
|
||||
key: ViewKey.INDEX,
|
||||
icon: 'IconList',
|
||||
type: ViewType.TABLE,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
isCustom: true,
|
||||
anyFieldFilterValue: null,
|
||||
calendarFieldMetadataUniversalIdentifier: null,
|
||||
calendarEndFieldMetadataUniversalIdentifier: null,
|
||||
calendarLayout: null,
|
||||
isCompact: false,
|
||||
shouldHideEmptyGroups: false,
|
||||
kanbanColumnWidth: null,
|
||||
kanbanAggregateOperation: null,
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: null,
|
||||
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
|
||||
position: 0,
|
||||
universalIdentifier: v4(),
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
createdByUserWorkspaceId: null,
|
||||
isActive: true,
|
||||
isSystemSideEffect: true,
|
||||
universalOverrides: null,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
viewFieldGroupUniversalIdentifiers: [],
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewGroupUniversalIdentifiers: [],
|
||||
viewFilterGroupUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
private computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata,
|
||||
flatApplication,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { ViewKey, ViewType } from 'twenty-shared/types';
|
||||
|
||||
import { computeFlatIndexViewToCreate } from '../compute-flat-index-view-to-create.util';
|
||||
|
||||
const applicationUniversalIdentifier = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const objectUniversalIdentifier = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
describe('computeFlatIndexViewToCreate', () => {
|
||||
it('should derive the INDEX view universal identifier from the object', () => {
|
||||
const result = computeFlatIndexViewToCreate({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
|
||||
});
|
||||
|
||||
expect(result.universalIdentifier).toBe(
|
||||
getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
viewKey: ViewKey.INDEX,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should build a system-owned INDEX table view keyed on ViewKey.INDEX', () => {
|
||||
const result = computeFlatIndexViewToCreate({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
|
||||
});
|
||||
|
||||
expect(result.key).toBe(ViewKey.INDEX);
|
||||
expect(result.type).toBe(ViewType.TABLE);
|
||||
expect(result.name).toBe('All {objectLabelPlural}');
|
||||
expect(result.isSystemSideEffect).toBe(true);
|
||||
expect(result.objectMetadataUniversalIdentifier).toBe(
|
||||
objectUniversalIdentifier,
|
||||
);
|
||||
expect(result.applicationUniversalIdentifier).toBe(
|
||||
applicationUniversalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be deterministic and independent from the view primary key', () => {
|
||||
const first = computeFlatIndexViewToCreate({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
|
||||
});
|
||||
const second = computeFlatIndexViewToCreate({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
|
||||
});
|
||||
|
||||
expect(first.universalIdentifier).toBe(second.universalIdentifier);
|
||||
expect(first.id).not.toBe(second.id);
|
||||
});
|
||||
});
|
||||
+31
-10
@@ -1,3 +1,4 @@
|
||||
import { getViewFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -39,10 +40,7 @@ const makeFieldMetadata = (
|
||||
} as unknown as UniversalFlatFieldMetadata;
|
||||
};
|
||||
|
||||
const flatApplication = {
|
||||
universalIdentifier: 'app-uid',
|
||||
id: 'app-id',
|
||||
} as never;
|
||||
const applicationUniversalIdentifier = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
|
||||
const viewUniversalIdentifier = 'view-uid';
|
||||
|
||||
@@ -62,7 +60,7 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
@@ -87,7 +85,7 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
@@ -116,7 +114,7 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
@@ -141,7 +139,7 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
@@ -165,7 +163,7 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelField.universalIdentifier,
|
||||
});
|
||||
@@ -194,7 +192,7 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: [labelField, otherField],
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelField.universalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
@@ -205,4 +203,27 @@ describe('computeFlatViewFieldsToCreate', () => {
|
||||
otherField.universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should derive the view field universal identifier deterministically', () => {
|
||||
const field = makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: [field],
|
||||
viewUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result[0].universalIdentifier).toBe(
|
||||
getViewFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: field.universalIdentifier,
|
||||
}),
|
||||
);
|
||||
expect(result[0].isSystemSideEffect).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
|
||||
|
||||
const NAME = { universalIdentifier: 'name-uid' };
|
||||
const CREATED_AT = { universalIdentifier: 'created-at-uid' };
|
||||
const CREATED_BY = { universalIdentifier: 'created-by-uid' };
|
||||
|
||||
describe('orderFlatFieldMetadatasForSystemIndexView', () => {
|
||||
it('should move the label identifier first when it is not already', () => {
|
||||
expect(
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
flatFieldMetadatas: [CREATED_AT, NAME, CREATED_BY],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
|
||||
}),
|
||||
).toEqual([NAME, CREATED_AT, CREATED_BY]);
|
||||
});
|
||||
|
||||
it('should preserve the input order of every other field', () => {
|
||||
expect(
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
flatFieldMetadatas: [NAME, CREATED_BY, CREATED_AT],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
|
||||
}),
|
||||
).toEqual([NAME, CREATED_BY, CREATED_AT]);
|
||||
});
|
||||
|
||||
it('should keep the input order when the object has no label identifier', () => {
|
||||
expect(
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
flatFieldMetadatas: [CREATED_AT, NAME],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
}),
|
||||
).toEqual([CREATED_AT, NAME]);
|
||||
});
|
||||
|
||||
it('should keep the input order when the label identifier is not in the list', () => {
|
||||
expect(
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
flatFieldMetadatas: [CREATED_AT, CREATED_BY],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
|
||||
}),
|
||||
).toEqual([CREATED_AT, CREATED_BY]);
|
||||
});
|
||||
|
||||
it('should not mutate the input array', () => {
|
||||
const flatFieldMetadatas = [CREATED_AT, NAME];
|
||||
|
||||
orderFlatFieldMetadatasForSystemIndexView({
|
||||
flatFieldMetadatas,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
|
||||
});
|
||||
|
||||
expect(flatFieldMetadatas).toEqual([CREATED_AT, NAME]);
|
||||
});
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
|
||||
import {
|
||||
ViewKey,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
|
||||
export const computeFlatIndexViewToCreate = ({
|
||||
objectMetadata,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
objectMetadata: Pick<UniversalFlatObjectMetadata, 'universalIdentifier'>;
|
||||
}): UniversalFlatView & { id: string } => {
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier,
|
||||
name: `All {objectLabelPlural}`,
|
||||
key: ViewKey.INDEX,
|
||||
icon: 'IconList',
|
||||
type: ViewType.TABLE,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
isCustom: true,
|
||||
anyFieldFilterValue: null,
|
||||
calendarFieldMetadataUniversalIdentifier: null,
|
||||
calendarEndFieldMetadataUniversalIdentifier: null,
|
||||
calendarLayout: null,
|
||||
isCompact: false,
|
||||
shouldHideEmptyGroups: false,
|
||||
kanbanColumnWidth: null,
|
||||
kanbanAggregateOperation: null,
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: null,
|
||||
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
|
||||
position: 0,
|
||||
universalIdentifier: getSystemViewUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadata.universalIdentifier,
|
||||
viewKey: ViewKey.INDEX,
|
||||
}),
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
createdByUserWorkspaceId: null,
|
||||
isActive: true,
|
||||
isSystemSideEffect: true,
|
||||
universalOverrides: null,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
viewFieldGroupUniversalIdentifiers: [],
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewGroupUniversalIdentifiers: [],
|
||||
viewFilterGroupUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
applicationUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
+35
-48
@@ -1,74 +1,61 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { getViewFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
|
||||
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
|
||||
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
export const computeFlatViewFieldsToCreate = ({
|
||||
objectFlatFieldMetadatas,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
excludeLabelIdentifier = false,
|
||||
startPosition = 0,
|
||||
}: {
|
||||
flatApplication: FlatApplication;
|
||||
applicationUniversalIdentifier: string;
|
||||
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
|
||||
viewUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
excludeLabelIdentifier?: boolean;
|
||||
startPosition?: number;
|
||||
}): UniversalFlatViewField[] => {
|
||||
const createdAt = new Date().toISOString();
|
||||
const defaultViewFields = objectFlatFieldMetadatas
|
||||
.filter(
|
||||
const defaultViewFields = orderFlatFieldMetadatasForSystemIndexView({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
flatFieldMetadatas: objectFlatFieldMetadatas.filter(
|
||||
(field) =>
|
||||
field.name !== 'deletedAt' &&
|
||||
field.type !== FieldMetadataType.TS_VECTOR &&
|
||||
field.type !== FieldMetadataType.POSITION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION &&
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
// Include 'id' only if it's the label identifier (e.g., for junction tables)
|
||||
(field.name !== 'id' ||
|
||||
field.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier) &&
|
||||
isFlatFieldMetadataDisplayableInDefaultView({
|
||||
flatFieldMetadata: field,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}) &&
|
||||
// Exclude label identifier field when requested (e.g., for FIELDS_WIDGET views)
|
||||
(!excludeLabelIdentifier ||
|
||||
field.universalIdentifier !==
|
||||
labelIdentifierFieldMetadataUniversalIdentifier),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aIsLabelIdentifierFieldMetadata =
|
||||
a.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
const bIsLabelIdentifierFieldMetadata =
|
||||
b.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (aIsLabelIdentifierFieldMetadata && !bIsLabelIdentifierFieldMetadata)
|
||||
return -1;
|
||||
if (!aIsLabelIdentifierFieldMetadata && bIsLabelIdentifierFieldMetadata)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
})
|
||||
.map<UniversalFlatViewField>((field, index) => ({
|
||||
fieldMetadataUniversalIdentifier: field.universalIdentifier,
|
||||
),
|
||||
}).map<UniversalFlatViewField>((field, index) => ({
|
||||
fieldMetadataUniversalIdentifier: field.universalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: getViewFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: v4(),
|
||||
isVisible: true,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: index,
|
||||
aggregateOperation: null,
|
||||
isActive: true,
|
||||
isSystemSideEffect: true,
|
||||
universalOverrides: null,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
}));
|
||||
fieldMetadataUniversalIdentifier: field.universalIdentifier,
|
||||
}),
|
||||
isVisible: true,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: startPosition + index,
|
||||
aggregateOperation: null,
|
||||
isActive: true,
|
||||
isSystemSideEffect: true,
|
||||
universalOverrides: null,
|
||||
applicationUniversalIdentifier,
|
||||
}));
|
||||
|
||||
return defaultViewFields;
|
||||
};
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
||||
export const isFlatFieldMetadataDisplayableInDefaultView = ({
|
||||
flatFieldMetadata,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}: {
|
||||
flatFieldMetadata: Pick<
|
||||
UniversalFlatFieldMetadata,
|
||||
'name' | 'type' | 'universalIdentifier'
|
||||
>;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
}): boolean =>
|
||||
flatFieldMetadata.name !== 'deletedAt' &&
|
||||
flatFieldMetadata.type !== FieldMetadataType.TS_VECTOR &&
|
||||
flatFieldMetadata.type !== FieldMetadataType.POSITION &&
|
||||
flatFieldMetadata.type !== FieldMetadataType.MORPH_RELATION &&
|
||||
flatFieldMetadata.type !== FieldMetadataType.RELATION &&
|
||||
(flatFieldMetadata.name !== 'id' ||
|
||||
flatFieldMetadata.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier);
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
export const orderFlatFieldMetadatasForSystemIndexView = <
|
||||
TFlatFieldMetadata extends { universalIdentifier: string },
|
||||
>({
|
||||
flatFieldMetadatas,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}: {
|
||||
flatFieldMetadatas: TFlatFieldMetadata[];
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
}): TFlatFieldMetadata[] => [
|
||||
...flatFieldMetadatas.filter(
|
||||
(flatFieldMetadata) =>
|
||||
flatFieldMetadata.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
),
|
||||
...flatFieldMetadatas.filter(
|
||||
(flatFieldMetadata) =>
|
||||
flatFieldMetadata.universalIdentifier !==
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
),
|
||||
];
|
||||
+356
-392
@@ -2493,54 +2493,48 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
||||
"id": "00000000-0000-0000-0000-000000000730",
|
||||
},
|
||||
},
|
||||
"id": "00000000-0000-0000-0000-000000000760",
|
||||
"id": "00000000-0000-0000-0000-000000000758",
|
||||
"views": {
|
||||
"allTimelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000759",
|
||||
"id": "00000000-0000-0000-0000-000000000757",
|
||||
"viewFieldGroups": {},
|
||||
"viewFields": {
|
||||
"happensAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000746",
|
||||
},
|
||||
"linkedRecordCachedName": {
|
||||
"id": "00000000-0000-0000-0000-000000000749",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000745",
|
||||
},
|
||||
"properties": {
|
||||
"id": "00000000-0000-0000-0000-000000000747",
|
||||
},
|
||||
"targetCompany": {
|
||||
"id": "00000000-0000-0000-0000-000000000751",
|
||||
"id": "00000000-0000-0000-0000-000000000749",
|
||||
},
|
||||
"targetDashboard": {
|
||||
"id": "00000000-0000-0000-0000-000000000758",
|
||||
},
|
||||
"targetNote": {
|
||||
"id": "00000000-0000-0000-0000-000000000754",
|
||||
},
|
||||
"targetOpportunity": {
|
||||
"id": "00000000-0000-0000-0000-000000000752",
|
||||
},
|
||||
"targetPerson": {
|
||||
"id": "00000000-0000-0000-0000-000000000750",
|
||||
},
|
||||
"targetTask": {
|
||||
"id": "00000000-0000-0000-0000-000000000753",
|
||||
},
|
||||
"targetWorkflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000755",
|
||||
},
|
||||
"targetWorkflowRun": {
|
||||
"id": "00000000-0000-0000-0000-000000000757",
|
||||
},
|
||||
"targetWorkflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000756",
|
||||
},
|
||||
"workspaceMember": {
|
||||
"targetNote": {
|
||||
"id": "00000000-0000-0000-0000-000000000752",
|
||||
},
|
||||
"targetOpportunity": {
|
||||
"id": "00000000-0000-0000-0000-000000000750",
|
||||
},
|
||||
"targetPerson": {
|
||||
"id": "00000000-0000-0000-0000-000000000748",
|
||||
},
|
||||
"targetTask": {
|
||||
"id": "00000000-0000-0000-0000-000000000751",
|
||||
},
|
||||
"targetWorkflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000753",
|
||||
},
|
||||
"targetWorkflowRun": {
|
||||
"id": "00000000-0000-0000-0000-000000000755",
|
||||
},
|
||||
"targetWorkflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000754",
|
||||
},
|
||||
"workspaceMember": {
|
||||
"id": "00000000-0000-0000-0000-000000000747",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
},
|
||||
@@ -2549,80 +2543,80 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
||||
"workflow": {
|
||||
"fields": {
|
||||
"attachments": {
|
||||
"id": "00000000-0000-0000-0000-000000000777",
|
||||
},
|
||||
"automatedTriggers": {
|
||||
"id": "00000000-0000-0000-0000-000000000775",
|
||||
},
|
||||
"coreWorkflowId": {
|
||||
"id": "00000000-0000-0000-0000-000000000771",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000762",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000765",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000764",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000761",
|
||||
},
|
||||
"lastPublishedVersionId": {
|
||||
"id": "00000000-0000-0000-0000-000000000770",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000769",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000767",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000774",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000768",
|
||||
},
|
||||
"statuses": {
|
||||
"id": "00000000-0000-0000-0000-000000000772",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000776",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000763",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000766",
|
||||
},
|
||||
"versions": {
|
||||
"automatedTriggers": {
|
||||
"id": "00000000-0000-0000-0000-000000000773",
|
||||
},
|
||||
"coreWorkflowId": {
|
||||
"id": "00000000-0000-0000-0000-000000000769",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000760",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000763",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000762",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000759",
|
||||
},
|
||||
"lastPublishedVersionId": {
|
||||
"id": "00000000-0000-0000-0000-000000000768",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000767",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000765",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000772",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000766",
|
||||
},
|
||||
"statuses": {
|
||||
"id": "00000000-0000-0000-0000-000000000770",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000774",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000761",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000764",
|
||||
},
|
||||
"versions": {
|
||||
"id": "00000000-0000-0000-0000-000000000771",
|
||||
},
|
||||
},
|
||||
"id": "00000000-0000-0000-0000-000000000785",
|
||||
"id": "00000000-0000-0000-0000-000000000783",
|
||||
"views": {
|
||||
"allWorkflows": {
|
||||
"id": "00000000-0000-0000-0000-000000000784",
|
||||
"id": "00000000-0000-0000-0000-000000000782",
|
||||
"viewFieldGroups": {},
|
||||
"viewFields": {
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000781",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000778",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000783",
|
||||
},
|
||||
"statuses": {
|
||||
"id": "00000000-0000-0000-0000-000000000779",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000776",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000781",
|
||||
},
|
||||
"statuses": {
|
||||
"id": "00000000-0000-0000-0000-000000000777",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000780",
|
||||
"id": "00000000-0000-0000-0000-000000000778",
|
||||
},
|
||||
"versions": {
|
||||
"id": "00000000-0000-0000-0000-000000000782",
|
||||
"id": "00000000-0000-0000-0000-000000000780",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
@@ -2632,80 +2626,80 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
||||
"workflowAutomatedTrigger": {
|
||||
"fields": {
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000787",
|
||||
"id": "00000000-0000-0000-0000-000000000785",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000790",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000789",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000786",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000792",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000793",
|
||||
},
|
||||
"settings": {
|
||||
"id": "00000000-0000-0000-0000-000000000795",
|
||||
},
|
||||
"type": {
|
||||
"id": "00000000-0000-0000-0000-000000000794",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000788",
|
||||
},
|
||||
"updatedBy": {
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000787",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000784",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000790",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000791",
|
||||
},
|
||||
"settings": {
|
||||
"id": "00000000-0000-0000-0000-000000000793",
|
||||
},
|
||||
"type": {
|
||||
"id": "00000000-0000-0000-0000-000000000792",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000786",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000789",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000796",
|
||||
"id": "00000000-0000-0000-0000-000000000794",
|
||||
},
|
||||
},
|
||||
"id": "00000000-0000-0000-0000-000000000808",
|
||||
"id": "00000000-0000-0000-0000-000000000806",
|
||||
"views": {
|
||||
"allWorkflowAutomatedTriggers": {
|
||||
"id": "00000000-0000-0000-0000-000000000800",
|
||||
"id": "00000000-0000-0000-0000-000000000798",
|
||||
"viewFieldGroups": {},
|
||||
"viewFields": {
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000799",
|
||||
},
|
||||
"type": {
|
||||
"id": "00000000-0000-0000-0000-000000000797",
|
||||
},
|
||||
"type": {
|
||||
"id": "00000000-0000-0000-0000-000000000795",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000798",
|
||||
"id": "00000000-0000-0000-0000-000000000796",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
},
|
||||
"workflowAutomatedTriggerRecordPageFields": {
|
||||
"id": "00000000-0000-0000-0000-000000000807",
|
||||
"id": "00000000-0000-0000-0000-000000000805",
|
||||
"viewFieldGroups": {
|
||||
"general": {
|
||||
"id": "00000000-0000-0000-0000-000000000805",
|
||||
"id": "00000000-0000-0000-0000-000000000803",
|
||||
},
|
||||
"system": {
|
||||
"id": "00000000-0000-0000-0000-000000000806",
|
||||
"id": "00000000-0000-0000-0000-000000000804",
|
||||
},
|
||||
},
|
||||
"viewFields": {
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000803",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000804",
|
||||
},
|
||||
"type": {
|
||||
"id": "00000000-0000-0000-0000-000000000801",
|
||||
},
|
||||
"workflow": {
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000802",
|
||||
},
|
||||
"type": {
|
||||
"id": "00000000-0000-0000-0000-000000000799",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000800",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
},
|
||||
@@ -2714,134 +2708,125 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
||||
"workflowRun": {
|
||||
"fields": {
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000810",
|
||||
"id": "00000000-0000-0000-0000-000000000808",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000813",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000812",
|
||||
},
|
||||
"endedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000822",
|
||||
},
|
||||
"enqueuedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000820",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000809",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000817",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000815",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000816",
|
||||
},
|
||||
"startedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000821",
|
||||
},
|
||||
"state": {
|
||||
"id": "00000000-0000-0000-0000-000000000824",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000823",
|
||||
},
|
||||
"stepLogs": {
|
||||
"id": "00000000-0000-0000-0000-000000000825",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000826",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000811",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000814",
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000810",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000819",
|
||||
"endedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000820",
|
||||
},
|
||||
"workflowVersion": {
|
||||
"enqueuedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000818",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000807",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000815",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000813",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000814",
|
||||
},
|
||||
"startedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000819",
|
||||
},
|
||||
"state": {
|
||||
"id": "00000000-0000-0000-0000-000000000822",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000821",
|
||||
},
|
||||
"stepLogs": {
|
||||
"id": "00000000-0000-0000-0000-000000000823",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000824",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000809",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000812",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000817",
|
||||
},
|
||||
"workflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000816",
|
||||
},
|
||||
},
|
||||
"id": "00000000-0000-0000-0000-000000000849",
|
||||
"id": "00000000-0000-0000-0000-000000000844",
|
||||
"views": {
|
||||
"allWorkflowRuns": {
|
||||
"id": "00000000-0000-0000-0000-000000000833",
|
||||
"id": "00000000-0000-0000-0000-000000000828",
|
||||
"viewFieldGroups": {},
|
||||
"viewFields": {
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000831",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000827",
|
||||
},
|
||||
"startedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000830",
|
||||
"id": "00000000-0000-0000-0000-000000000825",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000829",
|
||||
"id": "00000000-0000-0000-0000-000000000827",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000828",
|
||||
},
|
||||
"workflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000832",
|
||||
"id": "00000000-0000-0000-0000-000000000826",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
},
|
||||
"workflowRunRecordPageFields": {
|
||||
"id": "00000000-0000-0000-0000-000000000848",
|
||||
"id": "00000000-0000-0000-0000-000000000843",
|
||||
"viewFieldGroups": {
|
||||
"general": {
|
||||
"id": "00000000-0000-0000-0000-000000000846",
|
||||
"id": "00000000-0000-0000-0000-000000000841",
|
||||
},
|
||||
"system": {
|
||||
"id": "00000000-0000-0000-0000-000000000847",
|
||||
"id": "00000000-0000-0000-0000-000000000842",
|
||||
},
|
||||
},
|
||||
"viewFields": {
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000839",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000840",
|
||||
},
|
||||
"endedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000838",
|
||||
},
|
||||
"enqueuedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000841",
|
||||
},
|
||||
"startedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000837",
|
||||
},
|
||||
"state": {
|
||||
"id": "00000000-0000-0000-0000-000000000842",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000834",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000845",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000843",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000844",
|
||||
},
|
||||
"workflow": {
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000835",
|
||||
},
|
||||
"workflowVersion": {
|
||||
"endedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000833",
|
||||
},
|
||||
"enqueuedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000836",
|
||||
},
|
||||
"startedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000832",
|
||||
},
|
||||
"state": {
|
||||
"id": "00000000-0000-0000-0000-000000000837",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000829",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000840",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000838",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000839",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000830",
|
||||
},
|
||||
"workflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000831",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
},
|
||||
@@ -2850,118 +2835,118 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
||||
"workflowVersion": {
|
||||
"fields": {
|
||||
"coreWorkflowVersionId": {
|
||||
"id": "00000000-0000-0000-0000-000000000864",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000851",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000854",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000853",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000850",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000858",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000856",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000862",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000857",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000861",
|
||||
},
|
||||
"steps": {
|
||||
"id": "00000000-0000-0000-0000-000000000863",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000865",
|
||||
},
|
||||
"trigger": {
|
||||
"id": "00000000-0000-0000-0000-000000000860",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000852",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000855",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000859",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000846",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000849",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000848",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000845",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000853",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000851",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000857",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000852",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000856",
|
||||
},
|
||||
"steps": {
|
||||
"id": "00000000-0000-0000-0000-000000000858",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000860",
|
||||
},
|
||||
"trigger": {
|
||||
"id": "00000000-0000-0000-0000-000000000855",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000847",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000850",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000854",
|
||||
},
|
||||
},
|
||||
"id": "00000000-0000-0000-0000-000000000885",
|
||||
"id": "00000000-0000-0000-0000-000000000880",
|
||||
"views": {
|
||||
"allWorkflowVersions": {
|
||||
"id": "00000000-0000-0000-0000-000000000871",
|
||||
"id": "00000000-0000-0000-0000-000000000866",
|
||||
"viewFieldGroups": {},
|
||||
"viewFields": {
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000866",
|
||||
"id": "00000000-0000-0000-0000-000000000861",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000870",
|
||||
"id": "00000000-0000-0000-0000-000000000865",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000868",
|
||||
"id": "00000000-0000-0000-0000-000000000863",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000869",
|
||||
"id": "00000000-0000-0000-0000-000000000864",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000867",
|
||||
"id": "00000000-0000-0000-0000-000000000862",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
},
|
||||
"workflowVersionRecordPageFields": {
|
||||
"id": "00000000-0000-0000-0000-000000000884",
|
||||
"id": "00000000-0000-0000-0000-000000000879",
|
||||
"viewFieldGroups": {
|
||||
"general": {
|
||||
"id": "00000000-0000-0000-0000-000000000882",
|
||||
"id": "00000000-0000-0000-0000-000000000877",
|
||||
},
|
||||
"system": {
|
||||
"id": "00000000-0000-0000-0000-000000000883",
|
||||
"id": "00000000-0000-0000-0000-000000000878",
|
||||
},
|
||||
},
|
||||
"viewFields": {
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000875",
|
||||
"id": "00000000-0000-0000-0000-000000000870",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000877",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000880",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000872",
|
||||
},
|
||||
"runs": {
|
||||
"id": "00000000-0000-0000-0000-000000000875",
|
||||
},
|
||||
"status": {
|
||||
"id": "00000000-0000-0000-0000-000000000867",
|
||||
},
|
||||
"steps": {
|
||||
"id": "00000000-0000-0000-0000-000000000876",
|
||||
"id": "00000000-0000-0000-0000-000000000871",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000881",
|
||||
"id": "00000000-0000-0000-0000-000000000876",
|
||||
},
|
||||
"trigger": {
|
||||
"id": "00000000-0000-0000-0000-000000000874",
|
||||
"id": "00000000-0000-0000-0000-000000000869",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000878",
|
||||
"id": "00000000-0000-0000-0000-000000000873",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000879",
|
||||
"id": "00000000-0000-0000-0000-000000000874",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000873",
|
||||
"id": "00000000-0000-0000-0000-000000000868",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
@@ -2971,125 +2956,104 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
||||
"workspaceMember": {
|
||||
"fields": {
|
||||
"accountOwnerForCompanies": {
|
||||
"id": "00000000-0000-0000-0000-000000000903",
|
||||
},
|
||||
"assignedTasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000901",
|
||||
},
|
||||
"avatarUrl": {
|
||||
"id": "00000000-0000-0000-0000-000000000897",
|
||||
},
|
||||
"blocklist": {
|
||||
"id": "00000000-0000-0000-0000-000000000905",
|
||||
},
|
||||
"calendarEventParticipants": {
|
||||
"id": "00000000-0000-0000-0000-000000000906",
|
||||
},
|
||||
"calendarStartDay": {
|
||||
"id": "00000000-0000-0000-0000-000000000911",
|
||||
},
|
||||
"colorScheme": {
|
||||
"id": "00000000-0000-0000-0000-000000000895",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000887",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000890",
|
||||
},
|
||||
"dateFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000909",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000889",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000886",
|
||||
},
|
||||
"jobTitle": {
|
||||
"id": "00000000-0000-0000-0000-000000000899",
|
||||
},
|
||||
"locale": {
|
||||
"id": "00000000-0000-0000-0000-000000000896",
|
||||
},
|
||||
"messageParticipants": {
|
||||
"id": "00000000-0000-0000-0000-000000000904",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000894",
|
||||
},
|
||||
"numberFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000912",
|
||||
},
|
||||
"ownedOpportunities": {
|
||||
"id": "00000000-0000-0000-0000-000000000902",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000892",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000893",
|
||||
},
|
||||
"timeFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000910",
|
||||
},
|
||||
"timeZone": {
|
||||
"id": "00000000-0000-0000-0000-000000000908",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000907",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000888",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000891",
|
||||
},
|
||||
"userEmail": {
|
||||
"id": "00000000-0000-0000-0000-000000000898",
|
||||
},
|
||||
"userId": {
|
||||
"assignedTasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000896",
|
||||
},
|
||||
"avatarUrl": {
|
||||
"id": "00000000-0000-0000-0000-000000000892",
|
||||
},
|
||||
"blocklist": {
|
||||
"id": "00000000-0000-0000-0000-000000000900",
|
||||
},
|
||||
"calendarEventParticipants": {
|
||||
"id": "00000000-0000-0000-0000-000000000901",
|
||||
},
|
||||
"calendarStartDay": {
|
||||
"id": "00000000-0000-0000-0000-000000000906",
|
||||
},
|
||||
"colorScheme": {
|
||||
"id": "00000000-0000-0000-0000-000000000890",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000882",
|
||||
},
|
||||
"createdBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000885",
|
||||
},
|
||||
"dateFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000904",
|
||||
},
|
||||
"deletedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000884",
|
||||
},
|
||||
"id": {
|
||||
"id": "00000000-0000-0000-0000-000000000881",
|
||||
},
|
||||
"jobTitle": {
|
||||
"id": "00000000-0000-0000-0000-000000000894",
|
||||
},
|
||||
"locale": {
|
||||
"id": "00000000-0000-0000-0000-000000000891",
|
||||
},
|
||||
"messageParticipants": {
|
||||
"id": "00000000-0000-0000-0000-000000000899",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000889",
|
||||
},
|
||||
"numberFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000907",
|
||||
},
|
||||
"ownedOpportunities": {
|
||||
"id": "00000000-0000-0000-0000-000000000897",
|
||||
},
|
||||
"position": {
|
||||
"id": "00000000-0000-0000-0000-000000000887",
|
||||
},
|
||||
"searchVector": {
|
||||
"id": "00000000-0000-0000-0000-000000000888",
|
||||
},
|
||||
"timeFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000905",
|
||||
},
|
||||
"timeZone": {
|
||||
"id": "00000000-0000-0000-0000-000000000903",
|
||||
},
|
||||
"timelineActivities": {
|
||||
"id": "00000000-0000-0000-0000-000000000902",
|
||||
},
|
||||
"updatedAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000883",
|
||||
},
|
||||
"updatedBy": {
|
||||
"id": "00000000-0000-0000-0000-000000000886",
|
||||
},
|
||||
"userEmail": {
|
||||
"id": "00000000-0000-0000-0000-000000000893",
|
||||
},
|
||||
"userId": {
|
||||
"id": "00000000-0000-0000-0000-000000000895",
|
||||
},
|
||||
},
|
||||
"id": "00000000-0000-0000-0000-000000000925",
|
||||
"id": "00000000-0000-0000-0000-000000000913",
|
||||
"views": {
|
||||
"allWorkspaceMembers": {
|
||||
"id": "00000000-0000-0000-0000-000000000924",
|
||||
"id": "00000000-0000-0000-0000-000000000912",
|
||||
"viewFieldGroups": {},
|
||||
"viewFields": {
|
||||
"assignedTasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000923",
|
||||
},
|
||||
"avatarUrl": {
|
||||
"id": "00000000-0000-0000-0000-000000000915",
|
||||
},
|
||||
"colorScheme": {
|
||||
"id": "00000000-0000-0000-0000-000000000916",
|
||||
"id": "00000000-0000-0000-0000-000000000911",
|
||||
},
|
||||
"createdAt": {
|
||||
"id": "00000000-0000-0000-0000-000000000921",
|
||||
},
|
||||
"dateFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000919",
|
||||
},
|
||||
"locale": {
|
||||
"id": "00000000-0000-0000-0000-000000000917",
|
||||
"id": "00000000-0000-0000-0000-000000000909",
|
||||
},
|
||||
"name": {
|
||||
"id": "00000000-0000-0000-0000-000000000913",
|
||||
"id": "00000000-0000-0000-0000-000000000908",
|
||||
},
|
||||
"ownedOpportunities": {
|
||||
"id": "00000000-0000-0000-0000-000000000922",
|
||||
},
|
||||
"timeFormat": {
|
||||
"id": "00000000-0000-0000-0000-000000000920",
|
||||
},
|
||||
"timeZone": {
|
||||
"id": "00000000-0000-0000-0000-000000000918",
|
||||
},
|
||||
"userEmail": {
|
||||
"id": "00000000-0000-0000-0000-000000000914",
|
||||
"id": "00000000-0000-0000-0000-000000000910",
|
||||
},
|
||||
},
|
||||
"viewGroups": {},
|
||||
|
||||
+9
-5
@@ -1,17 +1,21 @@
|
||||
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
let uuidCounter = 0;
|
||||
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(
|
||||
() => `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
|
||||
),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getStandardObjectMetadataRelatedEntityIds', () => {
|
||||
let uuidCounter = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
uuidCounter = 0;
|
||||
(v4 as jest.Mock).mockImplementation(
|
||||
() =>
|
||||
`00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
+1
-11
@@ -79,8 +79,6 @@ export type BuildStandardFlatViewFieldMetadataMapsArgs = Omit<
|
||||
export const buildStandardFlatViewFieldMetadataMaps = (
|
||||
args: BuildStandardFlatViewFieldMetadataMapsArgs,
|
||||
): FlatEntityMaps<FlatViewField> => {
|
||||
const { flatViewMaps } = args.dependencyFlatEntityMaps;
|
||||
|
||||
const allViewFieldMetadatas: FlatViewField[] = (
|
||||
Object.keys(
|
||||
STANDARD_FLAT_VIEW_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME,
|
||||
@@ -100,16 +98,8 @@ export const buildStandardFlatViewFieldMetadataMaps = (
|
||||
let flatViewFieldMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const viewFieldMetadata of allViewFieldMetadatas) {
|
||||
const parentView =
|
||||
flatViewMaps.byUniversalIdentifier[
|
||||
viewFieldMetadata.viewUniversalIdentifier
|
||||
];
|
||||
|
||||
flatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity: {
|
||||
...viewFieldMetadata,
|
||||
isSystemSideEffect: parentView?.isSystemSideEffect ?? false,
|
||||
},
|
||||
flatEntity: viewFieldMetadata,
|
||||
flatEntityMaps: flatViewFieldMaps,
|
||||
});
|
||||
}
|
||||
|
||||
+14
-2
@@ -1,7 +1,7 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { type AggregateOperations } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { type AggregateOperations } from 'twenty-shared/types';
|
||||
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
@@ -51,6 +51,7 @@ export const createStandardViewFieldFlatMetadata = <
|
||||
viewFieldGroupName = null,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}: CreateStandardViewFieldArgs<O, V>): FlatViewField => {
|
||||
@@ -79,6 +80,17 @@ export const createStandardViewFieldFlatMetadata = <
|
||||
);
|
||||
}
|
||||
|
||||
const parentView =
|
||||
dependencyFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
|
||||
viewDefinition.universalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(parentView)) {
|
||||
throw new Error(
|
||||
`Missing parent view ${objectName} ${viewName.toString()} for view field ${viewFieldName}`,
|
||||
);
|
||||
}
|
||||
|
||||
let viewFieldGroupId: string | null = null;
|
||||
let viewFieldGroupUniversalIdentifier: string | null = null;
|
||||
|
||||
@@ -121,7 +133,7 @@ export const createStandardViewFieldFlatMetadata = <
|
||||
size,
|
||||
aggregateOperation,
|
||||
isActive: true,
|
||||
isSystemSideEffect: false,
|
||||
isSystemSideEffect: parentView.isSystemSideEffect,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
createdAt: now,
|
||||
|
||||
-1
@@ -54,7 +54,6 @@ type Assertions = [
|
||||
| 'deletedAt'
|
||||
| 'type'
|
||||
| 'position'
|
||||
| 'key'
|
||||
| 'isCompact'
|
||||
| 'openRecordIn'
|
||||
| 'kanbanAggregateOperation'
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { ViewKey, ViewType } from 'twenty-shared/types';
|
||||
|
||||
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service';
|
||||
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = '00000000-0000-4000-8000-0000000000b1';
|
||||
const VIEW_UNIVERSAL_IDENTIFIER = '00000000-0000-4000-8000-0000000000c1';
|
||||
const OTHER_VIEW_UNIVERSAL_IDENTIFIER = '00000000-0000-4000-8000-0000000000c2';
|
||||
|
||||
type TestFlatView = {
|
||||
universalIdentifier: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
key: ViewKey | null;
|
||||
type: ViewType;
|
||||
isSystemSideEffect: boolean;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
|
||||
const buildFlatView = (
|
||||
overrides: Partial<TestFlatView> = {},
|
||||
): TestFlatView => ({
|
||||
universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
key: null,
|
||||
type: ViewType.TABLE,
|
||||
isSystemSideEffect: false,
|
||||
deletedAt: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const mapsFrom = (
|
||||
entities: { universalIdentifier: string; [key: string]: unknown }[],
|
||||
) => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
entities.map((entity) => [entity.universalIdentifier, entity]),
|
||||
),
|
||||
});
|
||||
|
||||
const buildCreationArgs = ({
|
||||
flatViewToValidate,
|
||||
existingFlatViews = [],
|
||||
}: {
|
||||
flatViewToValidate: TestFlatView;
|
||||
existingFlatViews?: TestFlatView[];
|
||||
}) =>
|
||||
({
|
||||
flatEntityToValidate: flatViewToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatViewMaps: mapsFrom(existingFlatViews),
|
||||
flatFieldMetadataMaps: mapsFrom([]),
|
||||
flatObjectMetadataMaps: mapsFrom([
|
||||
{
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifiers: existingFlatViews.map(
|
||||
(existingFlatView) => existingFlatView.universalIdentifier,
|
||||
),
|
||||
},
|
||||
]),
|
||||
},
|
||||
additionalCacheDataMaps: { featureFlagsMap: {} },
|
||||
}) as unknown as Parameters<
|
||||
FlatViewValidatorService['validateFlatViewCreation']
|
||||
>[0];
|
||||
|
||||
const buildUpdateArgs = ({
|
||||
flatEntityUpdate,
|
||||
existingFlatView,
|
||||
}: {
|
||||
flatEntityUpdate: Record<string, unknown>;
|
||||
existingFlatView: TestFlatView;
|
||||
}) =>
|
||||
({
|
||||
universalIdentifier: existingFlatView.universalIdentifier,
|
||||
flatEntityUpdate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatViewMaps: mapsFrom([existingFlatView]),
|
||||
flatFieldMetadataMaps: mapsFrom([]),
|
||||
},
|
||||
additionalCacheDataMaps: { featureFlagsMap: {} },
|
||||
}) as unknown as Parameters<
|
||||
FlatViewValidatorService['validateFlatViewUpdate']
|
||||
>[0];
|
||||
|
||||
describe('FlatViewValidatorService INDEX key reservation', () => {
|
||||
let service: FlatViewValidatorService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new FlatViewValidatorService();
|
||||
});
|
||||
|
||||
describe('creation', () => {
|
||||
it('rejects a caller-provided INDEX view', () => {
|
||||
const result = service.validateFlatViewCreation(
|
||||
buildCreationArgs({
|
||||
flatViewToValidate: buildFlatView({
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].message).toContain('reserved');
|
||||
});
|
||||
|
||||
it('accepts an engine-emitted INDEX view', () => {
|
||||
const result = service.validateFlatViewCreation(
|
||||
buildCreationArgs({
|
||||
flatViewToValidate: buildFlatView({
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects a second INDEX view on the same object', () => {
|
||||
const result = service.validateFlatViewCreation(
|
||||
buildCreationArgs({
|
||||
flatViewToValidate: buildFlatView({
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
}),
|
||||
existingFlatViews: [
|
||||
buildFlatView({
|
||||
universalIdentifier: OTHER_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].message).toContain('already has an INDEX view');
|
||||
});
|
||||
|
||||
it('ignores a soft-deleted INDEX view when checking the singleton', () => {
|
||||
const result = service.validateFlatViewCreation(
|
||||
buildCreationArgs({
|
||||
flatViewToValidate: buildFlatView({
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
}),
|
||||
existingFlatViews: [
|
||||
buildFlatView({
|
||||
universalIdentifier: OTHER_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
deletedAt: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('accepts a caller-provided view without a key', () => {
|
||||
const result = service.validateFlatViewCreation(
|
||||
buildCreationArgs({
|
||||
flatViewToValidate: buildFlatView({ key: null }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('accepts an update on an INDEX view', () => {
|
||||
const result = service.validateFlatViewUpdate(
|
||||
buildUpdateArgs({
|
||||
flatEntityUpdate: { name: 'Renamed default view' },
|
||||
existingFlatView: buildFlatView({
|
||||
key: ViewKey.INDEX,
|
||||
isSystemSideEffect: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+36
@@ -5,6 +5,7 @@ import {
|
||||
FieldMetadataType,
|
||||
RelationType,
|
||||
ViewCalendarLayout,
|
||||
ViewKey,
|
||||
ViewType,
|
||||
} from 'twenty-shared/types';
|
||||
import { getViewLayoutFromViewType, isDefined } from 'twenty-shared/utils';
|
||||
@@ -459,6 +460,41 @@ export class FlatViewValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
if (flatViewToValidate.key === ViewKey.INDEX) {
|
||||
if (flatViewToValidate.isSystemSideEffect !== true) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`The INDEX view key is reserved for the engine-owned default view; remove the key from the view definition`,
|
||||
userFriendlyMessage: msg`The INDEX view key is reserved for the default view`,
|
||||
});
|
||||
}
|
||||
|
||||
const objectAlreadyHasIndexFlatView =
|
||||
isDefined(optimisticFlatObjectMetadata) &&
|
||||
optimisticFlatObjectMetadata.viewUniversalIdentifiers.some(
|
||||
(viewUniversalIdentifier) => {
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier: viewUniversalIdentifier,
|
||||
flatEntityMaps: optimisticFlatViewMaps,
|
||||
});
|
||||
|
||||
return (
|
||||
isDefined(flatView) &&
|
||||
flatView.key === ViewKey.INDEX &&
|
||||
!isDefined(flatView.deletedAt)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (objectAlreadyHasIndexFlatView) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`Object already has an INDEX view`,
|
||||
userFriendlyMessage: msg`This object already has a default view`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(
|
||||
flatViewToValidate.kanbanAggregateOperationFieldMetadataUniversalIdentifier,
|
||||
|
||||
Reference in New Issue
Block a user