refactor(navigation-menu-item): validate universal properties instead of ids (#23566)
Follow-up to the discussion on #23485 and closes https://github.com/twentyhq/twenty/issues/23484. `FlatNavigationMenuItemValidatorService` receives `UniversalFlatEntityValidationArgs<'navigationMenuItem'>`, so the entity it validates is a `UniversalFlatNavigationMenuItem`: `viewId`, `pageLayoutId` and `targetObjectMetadataId` do not exist at that scope. The validator read the right universal keys but passed them through a bag of booleans named after the ids (`hasViewId`, `hasPageLayoutId`, ...) and then reported the id names in its errors. Nothing tied a message to the property it checked, so fixing one message string leaves the other five wrong. ## Changes - Replace the private `validateNavigationMenuItemType` boolean bag with `validateNavigationMenuItemTypeRequiredProperties({ flatNavigationMenuItem })` under `flat-navigation-menu-item/validators/utils/`, in line with `validateAgentRequiredProperties` and `validateNavigationMenuItemPageLayoutReferenceCrossEntity`. It takes the universal entity, so a message can only name a property that exists at that scope. - The util is an explicit `switch` on `NavigationMenuItemType` closed by `assertUnreachable`, so adding a type fails to compile until its contract is declared. - Each case validates its own properties instead of checking presence generically: - `FOLDER`: non blank `name` - `OBJECT`, `VIEW`, `PAGE_LAYOUT`: `targetObjectMetadataUniversalIdentifier` / `viewUniversalIdentifier` / `pageLayoutUniversalIdentifier` must be valid uuids - `RECORD`: `targetRecordId` and `targetObjectMetadataUniversalIdentifier`, both uuids, reported separately - `LINK`: `link` must pass `isValidUrl` - Both call sites spread the result; the update path passes the merged `{ ...from, ...update }` entity, which removes the redundant `name` re-merge. `targetRecordId` stays an id: it points at workspace record data rather than metadata, so it has no universal counterpart. ## Behaviour - Errors name the universal property (`viewUniversalIdentifier`) instead of the id (`viewId`). - Blank strings are now uniformly treated as missing; creation previously accepted `link: " "`. - `RECORD` reports each missing property separately instead of one merged error. - Values that are present but malformed are now rejected: non uuid identifiers and links that are not urls. Standard application identifiers are all v4 uuids and the create/update inputs already carry `@IsUUID`, so this only tightens the app manifest path. ## Verification - Unit tests for the util cover each type valid and invalid, blank names, non url links and non uuid identifiers (23 tests pass alongside the sibling suite) - `nx typecheck twenty-server` clean - oxlint (type-aware) and oxfmt clean on the changed files <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23566?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+184
@@ -0,0 +1,184 @@
|
|||||||
|
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { validateNavigationMenuItemTypeRequiredProperties } from 'src/engine/metadata-modules/flat-navigation-menu-item/validators/utils/validate-navigation-menu-item-type-required-properties.util';
|
||||||
|
import { NavigationMenuItemExceptionCode } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||||
|
import { type UniversalFlatNavigationMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-navigation-menu-item.type';
|
||||||
|
|
||||||
|
const VALID_UUID = '20202020-b001-4b01-8b01-c0aba11c0001';
|
||||||
|
const OTHER_VALID_UUID = '20202020-b002-4b02-8b02-c0aba11c0002';
|
||||||
|
|
||||||
|
const buildFlatNavigationMenuItem = (
|
||||||
|
overrides: Partial<UniversalFlatNavigationMenuItem>,
|
||||||
|
): UniversalFlatNavigationMenuItem =>
|
||||||
|
({
|
||||||
|
universalIdentifier: VALID_UUID,
|
||||||
|
name: null,
|
||||||
|
link: null,
|
||||||
|
icon: null,
|
||||||
|
color: null,
|
||||||
|
position: 0,
|
||||||
|
targetRecordId: null,
|
||||||
|
userWorkspaceId: null,
|
||||||
|
folderUniversalIdentifier: null,
|
||||||
|
viewUniversalIdentifier: null,
|
||||||
|
pageLayoutUniversalIdentifier: null,
|
||||||
|
targetObjectMetadataUniversalIdentifier: null,
|
||||||
|
...overrides,
|
||||||
|
}) as UniversalFlatNavigationMenuItem;
|
||||||
|
|
||||||
|
describe('validateNavigationMenuItemTypeRequiredProperties', () => {
|
||||||
|
it('should return an error when type is not defined', () => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({
|
||||||
|
type: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(1);
|
||||||
|
expect(errors[0]).toMatchObject({
|
||||||
|
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||||
|
message: 'Navigation menu item type is required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.FOLDER,
|
||||||
|
expectedMessages: ['A name is required for FOLDER type'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.OBJECT,
|
||||||
|
expectedMessages: [
|
||||||
|
'A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.VIEW,
|
||||||
|
expectedMessages: [
|
||||||
|
'A valid viewUniversalIdentifier is required for VIEW type',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.RECORD,
|
||||||
|
expectedMessages: [
|
||||||
|
'A valid targetRecordId is required for RECORD type',
|
||||||
|
'A valid targetObjectMetadataUniversalIdentifier is required for RECORD type',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.LINK,
|
||||||
|
expectedMessages: ['A valid link is required for LINK type'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.PAGE_LAYOUT,
|
||||||
|
expectedMessages: [
|
||||||
|
'A valid pageLayoutUniversalIdentifier is required for PAGE_LAYOUT type',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'should report every missing property for $type type',
|
||||||
|
({ type, expectedMessages }) => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({ type }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors.map(({ message }) => message)).toEqual(expectedMessages);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.FOLDER,
|
||||||
|
overrides: { name: 'My folder' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.OBJECT,
|
||||||
|
overrides: { targetObjectMetadataUniversalIdentifier: VALID_UUID },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.VIEW,
|
||||||
|
overrides: { viewUniversalIdentifier: VALID_UUID },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.RECORD,
|
||||||
|
overrides: {
|
||||||
|
targetRecordId: VALID_UUID,
|
||||||
|
targetObjectMetadataUniversalIdentifier: OTHER_VALID_UUID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.LINK,
|
||||||
|
overrides: { link: 'https://twenty.com' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NavigationMenuItemType.PAGE_LAYOUT,
|
||||||
|
overrides: { pageLayoutUniversalIdentifier: VALID_UUID },
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'should not report any error when $type type properties are valid',
|
||||||
|
({ type, overrides }) => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({
|
||||||
|
type,
|
||||||
|
...overrides,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should return an error when type is unknown', () => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({
|
||||||
|
type: 'UNKNOWN_NAVIGATION_MENU_ITEM_TYPE' as NavigationMenuItemType,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(1);
|
||||||
|
expect(errors[0]).toMatchObject({
|
||||||
|
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||||
|
message:
|
||||||
|
'Unknown navigation menu item type UNKNOWN_NAVIGATION_MENU_ITEM_TYPE',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should treat blank folder names as missing', () => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({
|
||||||
|
type: NavigationMenuItemType.FOLDER,
|
||||||
|
name: ' ',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors.map(({ message }) => message)).toEqual([
|
||||||
|
'A name is required for FOLDER type',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report an error when the link is not a valid url', () => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({
|
||||||
|
type: NavigationMenuItemType.LINK,
|
||||||
|
link: 'not a link',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors.map(({ message }) => message)).toEqual([
|
||||||
|
'A valid link is required for LINK type',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report an error when a universal identifier is not a valid uuid', () => {
|
||||||
|
const errors = validateNavigationMenuItemTypeRequiredProperties({
|
||||||
|
flatNavigationMenuItem: buildFlatNavigationMenuItem({
|
||||||
|
type: NavigationMenuItemType.VIEW,
|
||||||
|
viewUniversalIdentifier: 'not-a-uuid',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors.map(({ message }) => message)).toEqual([
|
||||||
|
'A valid viewUniversalIdentifier is required for VIEW type',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
import { type MessageDescriptor } from '@lingui/core';
|
||||||
|
import { msg, t } from '@lingui/core/macro';
|
||||||
|
import {
|
||||||
|
type AssertUnreachable,
|
||||||
|
NavigationMenuItemType,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
|
import { isDefined, isValidUrl, isValidUuid } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { NavigationMenuItemExceptionCode } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||||
|
import { type UniversalFlatNavigationMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-navigation-menu-item.type';
|
||||||
|
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||||
|
|
||||||
|
type NavigationMenuItemValidationError =
|
||||||
|
FlatEntityValidationError<NavigationMenuItemExceptionCode>;
|
||||||
|
|
||||||
|
const buildInvalidInputError = (
|
||||||
|
message: string,
|
||||||
|
userFriendlyMessage: MessageDescriptor,
|
||||||
|
): NavigationMenuItemValidationError => ({
|
||||||
|
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||||
|
message,
|
||||||
|
userFriendlyMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateUuidProperty = ({
|
||||||
|
value,
|
||||||
|
message,
|
||||||
|
userFriendlyMessage,
|
||||||
|
}: {
|
||||||
|
value: string | null | undefined;
|
||||||
|
message: string;
|
||||||
|
userFriendlyMessage: MessageDescriptor;
|
||||||
|
}): NavigationMenuItemValidationError[] =>
|
||||||
|
isDefined(value) && isValidUuid(value)
|
||||||
|
? []
|
||||||
|
: [buildInvalidInputError(message, userFriendlyMessage)];
|
||||||
|
|
||||||
|
export const validateNavigationMenuItemTypeRequiredProperties = ({
|
||||||
|
flatNavigationMenuItem,
|
||||||
|
}: {
|
||||||
|
flatNavigationMenuItem: UniversalFlatNavigationMenuItem;
|
||||||
|
}): NavigationMenuItemValidationError[] => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
targetRecordId,
|
||||||
|
targetObjectMetadataUniversalIdentifier,
|
||||||
|
viewUniversalIdentifier,
|
||||||
|
pageLayoutUniversalIdentifier,
|
||||||
|
} = flatNavigationMenuItem;
|
||||||
|
|
||||||
|
if (!isDefined(type)) {
|
||||||
|
return [
|
||||||
|
buildInvalidInputError(
|
||||||
|
t`Navigation menu item type is required`,
|
||||||
|
msg`Navigation menu item type is required`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case NavigationMenuItemType.FOLDER: {
|
||||||
|
return isDefined(name) && name.trim() !== ''
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
buildInvalidInputError(
|
||||||
|
t`A name is required for FOLDER type`,
|
||||||
|
msg`A name is required for FOLDER type`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case NavigationMenuItemType.OBJECT: {
|
||||||
|
return validateUuidProperty({
|
||||||
|
value: targetObjectMetadataUniversalIdentifier,
|
||||||
|
message: t`A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type`,
|
||||||
|
userFriendlyMessage: msg`A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case NavigationMenuItemType.VIEW: {
|
||||||
|
return validateUuidProperty({
|
||||||
|
value: viewUniversalIdentifier,
|
||||||
|
message: t`A valid viewUniversalIdentifier is required for VIEW type`,
|
||||||
|
userFriendlyMessage: msg`A valid viewUniversalIdentifier is required for VIEW type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case NavigationMenuItemType.RECORD: {
|
||||||
|
return [
|
||||||
|
...validateUuidProperty({
|
||||||
|
value: targetRecordId,
|
||||||
|
message: t`A valid targetRecordId is required for RECORD type`,
|
||||||
|
userFriendlyMessage: msg`A valid targetRecordId is required for RECORD type`,
|
||||||
|
}),
|
||||||
|
...validateUuidProperty({
|
||||||
|
value: targetObjectMetadataUniversalIdentifier,
|
||||||
|
message: t`A valid targetObjectMetadataUniversalIdentifier is required for RECORD type`,
|
||||||
|
userFriendlyMessage: msg`A valid targetObjectMetadataUniversalIdentifier is required for RECORD type`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case NavigationMenuItemType.LINK: {
|
||||||
|
return isDefined(link) && isValidUrl(link)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
buildInvalidInputError(
|
||||||
|
t`A valid link is required for LINK type`,
|
||||||
|
msg`A valid link is required for LINK type`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case NavigationMenuItemType.PAGE_LAYOUT: {
|
||||||
|
return validateUuidProperty({
|
||||||
|
value: pageLayoutUniversalIdentifier,
|
||||||
|
message: t`A valid pageLayoutUniversalIdentifier is required for PAGE_LAYOUT type`,
|
||||||
|
userFriendlyMessage: msg`A valid pageLayoutUniversalIdentifier is required for PAGE_LAYOUT type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// oxlint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
type UnhandledNavigationMenuItemType = AssertUnreachable<typeof type>;
|
||||||
|
|
||||||
|
return [
|
||||||
|
buildInvalidInputError(
|
||||||
|
t`Unknown navigation menu item type ${type}`,
|
||||||
|
msg`Unknown navigation menu item type ${type}`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
+17
-126
@@ -1,12 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { msg, t } from '@lingui/core/macro';
|
import { msg, t } from '@lingui/core/macro';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||||
|
import { validateNavigationMenuItemTypeRequiredProperties } from 'src/engine/metadata-modules/flat-navigation-menu-item/validators/utils/validate-navigation-menu-item-type-required-properties.util';
|
||||||
import { NavigationMenuItemExceptionCode } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
import { NavigationMenuItemExceptionCode } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||||
import { type MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
|
import { type MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
|
||||||
import { validateFlatEntityCircularDependency } from 'src/engine/workspace-manager/workspace-migration/utils/validate-flat-entity-circular-dependency.util';
|
import { validateFlatEntityCircularDependency } from 'src/engine/workspace-manager/workspace-migration/utils/validate-flat-entity-circular-dependency.util';
|
||||||
@@ -22,96 +21,6 @@ const NAVIGATION_MENU_ITEM_MAX_DEPTH = 2;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FlatNavigationMenuItemValidatorService {
|
export class FlatNavigationMenuItemValidatorService {
|
||||||
private validateNavigationMenuItemType({
|
|
||||||
type,
|
|
||||||
hasTargetRecordId,
|
|
||||||
hasTargetObjectMetadataId,
|
|
||||||
hasViewId,
|
|
||||||
hasLink,
|
|
||||||
hasPageLayoutId,
|
|
||||||
name,
|
|
||||||
}: {
|
|
||||||
type: NavigationMenuItemType | null | undefined;
|
|
||||||
hasTargetRecordId: boolean;
|
|
||||||
hasTargetObjectMetadataId: boolean;
|
|
||||||
hasViewId: boolean;
|
|
||||||
hasLink: boolean;
|
|
||||||
hasPageLayoutId: boolean;
|
|
||||||
name: string | null | undefined;
|
|
||||||
}): FlatEntityValidationError<NavigationMenuItemExceptionCode>[] {
|
|
||||||
const errors: FlatEntityValidationError<NavigationMenuItemExceptionCode>[] =
|
|
||||||
[];
|
|
||||||
|
|
||||||
if (!isDefined(type)) {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`Navigation menu item type is required`,
|
|
||||||
userFriendlyMessage: msg`Navigation menu item type is required`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case NavigationMenuItemType.FOLDER:
|
|
||||||
if (!isDefined(name) || name.trim() === '') {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`Folder name is required`,
|
|
||||||
userFriendlyMessage: msg`Folder name is required`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NavigationMenuItemType.OBJECT:
|
|
||||||
if (!hasTargetObjectMetadataId) {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`targetObjectMetadataId is required for OBJECT type`,
|
|
||||||
userFriendlyMessage: msg`targetObjectMetadataId is required for OBJECT type`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NavigationMenuItemType.VIEW:
|
|
||||||
if (!hasViewId) {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`viewId is required for VIEW type`,
|
|
||||||
userFriendlyMessage: msg`viewId is required for VIEW type`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NavigationMenuItemType.RECORD:
|
|
||||||
if (!hasTargetRecordId || !hasTargetObjectMetadataId) {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`targetRecordId and targetObjectMetadataId are required for RECORD type`,
|
|
||||||
userFriendlyMessage: msg`targetRecordId and targetObjectMetadataId are required for RECORD type`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NavigationMenuItemType.LINK:
|
|
||||||
if (!hasLink) {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`link is required for LINK type`,
|
|
||||||
userFriendlyMessage: msg`link is required for LINK type`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NavigationMenuItemType.PAGE_LAYOUT:
|
|
||||||
if (!hasPageLayoutId) {
|
|
||||||
errors.push({
|
|
||||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
|
||||||
message: t`pageLayoutId is required for PAGE_LAYOUT type`,
|
|
||||||
userFriendlyMessage: msg`pageLayoutId is required for PAGE_LAYOUT type`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCircularDependencyValidationErrors({
|
private getCircularDependencyValidationErrors({
|
||||||
navigationMenuItemUniversalIdentifier,
|
navigationMenuItemUniversalIdentifier,
|
||||||
folderUniversalIdentifier,
|
folderUniversalIdentifier,
|
||||||
@@ -173,6 +82,8 @@ export class FlatNavigationMenuItemValidatorService {
|
|||||||
const validationResult = getEmptyFlatEntityValidationError({
|
const validationResult = getEmptyFlatEntityValidationError({
|
||||||
flatEntityMinimalInformation: {
|
flatEntityMinimalInformation: {
|
||||||
universalIdentifier: flatNavigationMenuItem.universalIdentifier,
|
universalIdentifier: flatNavigationMenuItem.universalIdentifier,
|
||||||
|
name: flatNavigationMenuItem.name,
|
||||||
|
type: flatNavigationMenuItem.type,
|
||||||
},
|
},
|
||||||
metadataName: 'navigationMenuItem',
|
metadataName: 'navigationMenuItem',
|
||||||
type: 'create',
|
type: 'create',
|
||||||
@@ -189,23 +100,11 @@ export class FlatNavigationMenuItemValidatorService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeValidationErrors = this.validateNavigationMenuItemType({
|
validationResult.errors.push(
|
||||||
type: flatNavigationMenuItem.type,
|
...validateNavigationMenuItemTypeRequiredProperties({
|
||||||
hasTargetRecordId: isDefined(flatNavigationMenuItem.targetRecordId),
|
flatNavigationMenuItem,
|
||||||
hasTargetObjectMetadataId: isDefined(
|
}),
|
||||||
flatNavigationMenuItem.targetObjectMetadataUniversalIdentifier,
|
);
|
||||||
),
|
|
||||||
hasViewId: isDefined(flatNavigationMenuItem.viewUniversalIdentifier),
|
|
||||||
hasLink:
|
|
||||||
isDefined(flatNavigationMenuItem.link) &&
|
|
||||||
isNonEmptyString(flatNavigationMenuItem.link),
|
|
||||||
hasPageLayoutId: isDefined(
|
|
||||||
flatNavigationMenuItem.pageLayoutUniversalIdentifier,
|
|
||||||
),
|
|
||||||
name: flatNavigationMenuItem.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
validationResult.errors.push(...typeValidationErrors);
|
|
||||||
|
|
||||||
if (isDefined(flatNavigationMenuItem.folderUniversalIdentifier)) {
|
if (isDefined(flatNavigationMenuItem.folderUniversalIdentifier)) {
|
||||||
const circularDependencyErrors =
|
const circularDependencyErrors =
|
||||||
@@ -257,6 +156,8 @@ export class FlatNavigationMenuItemValidatorService {
|
|||||||
const validationResult = getEmptyFlatEntityValidationError({
|
const validationResult = getEmptyFlatEntityValidationError({
|
||||||
flatEntityMinimalInformation: {
|
flatEntityMinimalInformation: {
|
||||||
universalIdentifier: flatEntityToValidate.universalIdentifier,
|
universalIdentifier: flatEntityToValidate.universalIdentifier,
|
||||||
|
name: flatEntityToValidate.name,
|
||||||
|
type: flatEntityToValidate.type,
|
||||||
},
|
},
|
||||||
metadataName: 'navigationMenuItem',
|
metadataName: 'navigationMenuItem',
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
@@ -295,6 +196,8 @@ export class FlatNavigationMenuItemValidatorService {
|
|||||||
const validationResult = getEmptyFlatEntityValidationError({
|
const validationResult = getEmptyFlatEntityValidationError({
|
||||||
flatEntityMinimalInformation: {
|
flatEntityMinimalInformation: {
|
||||||
universalIdentifier,
|
universalIdentifier,
|
||||||
|
name: flatEntityUpdate.name ?? fromFlatNavigationMenuItem?.name,
|
||||||
|
type: flatEntityUpdate.type ?? fromFlatNavigationMenuItem?.type,
|
||||||
},
|
},
|
||||||
metadataName: 'navigationMenuItem',
|
metadataName: 'navigationMenuItem',
|
||||||
type: 'update',
|
type: 'update',
|
||||||
@@ -325,23 +228,11 @@ export class FlatNavigationMenuItemValidatorService {
|
|||||||
...flatEntityUpdate,
|
...flatEntityUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nameUpdate = flatEntityUpdate.name;
|
validationResult.errors.push(
|
||||||
|
...validateNavigationMenuItemTypeRequiredProperties({
|
||||||
const typeValidationErrors = this.validateNavigationMenuItemType({
|
flatNavigationMenuItem: toFlatNavigationMenuItem,
|
||||||
type: toFlatNavigationMenuItem.type,
|
}),
|
||||||
hasTargetRecordId: isDefined(toFlatNavigationMenuItem.targetRecordId),
|
);
|
||||||
hasTargetObjectMetadataId: isDefined(
|
|
||||||
toFlatNavigationMenuItem.targetObjectMetadataUniversalIdentifier,
|
|
||||||
),
|
|
||||||
hasViewId: isDefined(toFlatNavigationMenuItem.viewUniversalIdentifier),
|
|
||||||
hasLink: isNonEmptyString((toFlatNavigationMenuItem.link ?? '').trim()),
|
|
||||||
hasPageLayoutId: isDefined(
|
|
||||||
toFlatNavigationMenuItem.pageLayoutUniversalIdentifier,
|
|
||||||
),
|
|
||||||
name: isDefined(nameUpdate) ? nameUpdate : toFlatNavigationMenuItem.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
validationResult.errors.push(...typeValidationErrors);
|
|
||||||
|
|
||||||
const folderUniversalIdentifierUpdate =
|
const folderUniversalIdentifierUpdate =
|
||||||
flatEntityUpdate.folderUniversalIdentifier;
|
flatEntityUpdate.folderUniversalIdentifier;
|
||||||
|
|||||||
+334
@@ -0,0 +1,334 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing a FOLDER item with a blank name 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A name is required for FOLDER type",
|
||||||
|
"userFriendlyMessage": "A name is required for FOLDER type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": " ",
|
||||||
|
"type": "FOLDER",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A name is required for FOLDER type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing a FOLDER item without name 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A name is required for FOLDER type",
|
||||||
|
"userFriendlyMessage": "A name is required for FOLDER type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": null,
|
||||||
|
"type": "FOLDER",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A name is required for FOLDER type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing a LINK item with a link that is not a valid url 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A valid link is required for LINK type",
|
||||||
|
"userFriendlyMessage": "A valid link is required for LINK type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Link with invalid url",
|
||||||
|
"type": "LINK",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A valid link is required for LINK type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing a LINK item without link 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A valid link is required for LINK type",
|
||||||
|
"userFriendlyMessage": "A valid link is required for LINK type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Link without url",
|
||||||
|
"type": "LINK",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A valid link is required for LINK type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing a PAGE_LAYOUT item with a pageLayoutUniversalIdentifier that is not a uuid 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A valid pageLayoutUniversalIdentifier is required for PAGE_LAYOUT type",
|
||||||
|
"userFriendlyMessage": "A valid pageLayoutUniversalIdentifier is required for PAGE_LAYOUT type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Page layout with malformed identifier",
|
||||||
|
"type": "PAGE_LAYOUT",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A valid pageLayoutUniversalIdentifier is required for PAGE_LAYOUT type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing a VIEW item with a viewUniversalIdentifier that is not a uuid 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A valid viewUniversalIdentifier is required for VIEW type",
|
||||||
|
"userFriendlyMessage": "A valid viewUniversalIdentifier is required for VIEW type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "View with malformed identifier",
|
||||||
|
"type": "VIEW",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A valid viewUniversalIdentifier is required for VIEW type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing an OBJECT item with a targetObjectUniversalIdentifier that is not a uuid 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type",
|
||||||
|
"userFriendlyMessage": "A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Object with malformed target",
|
||||||
|
"type": "OBJECT",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing an OBJECT item without targetObjectUniversalIdentifier 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type",
|
||||||
|
"userFriendlyMessage": "A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Object without target",
|
||||||
|
"type": "OBJECT",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "A valid targetObjectMetadataUniversalIdentifier is required for OBJECT type",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sync application should fail on invalid navigation menu items when syncing an item with an unknown type 1`] = `
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"code": "METADATA_VALIDATION_FAILED",
|
||||||
|
"errors": {
|
||||||
|
"navigationMenuItem": [
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
|
"message": "Unknown navigation menu item type UNKNOWN_NAVIGATION_MENU_ITEM_TYPE",
|
||||||
|
"userFriendlyMessage": "Unknown navigation menu item type UNKNOWN_NAVIGATION_MENU_ITEM_TYPE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Item with unknown type",
|
||||||
|
"type": "UNKNOWN_NAVIGATION_MENU_ITEM_TYPE",
|
||||||
|
"universalIdentifier": Any<String>,
|
||||||
|
},
|
||||||
|
"metadataName": "navigationMenuItem",
|
||||||
|
"status": "fail",
|
||||||
|
"type": "create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"message": "Validation failed for 1 navigationMenuItem",
|
||||||
|
"summary": {
|
||||||
|
"navigationMenuItem": 1,
|
||||||
|
"totalErrors": 1,
|
||||||
|
},
|
||||||
|
"userFriendlyMessage": "Unknown navigation menu item type UNKNOWN_NAVIGATION_MENU_ITEM_TYPE",
|
||||||
|
},
|
||||||
|
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||||
|
"name": "GraphQLError",
|
||||||
|
}
|
||||||
|
`;
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||||
|
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||||
|
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||||
|
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||||
|
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||||
|
import { type NavigationMenuItemManifest } from 'twenty-shared/application';
|
||||||
|
import {
|
||||||
|
eachTestingContextFilter,
|
||||||
|
type EachTestingContext,
|
||||||
|
} from 'twenty-shared/testing';
|
||||||
|
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
const TEST_APP_ID = 'c1b2c3d4-0001-4000-a000-000000000001';
|
||||||
|
const TEST_ROLE_ID = 'c1b2c3d4-0002-4000-a000-000000000002';
|
||||||
|
const TEST_NAVIGATION_MENU_ITEM_ID = 'c1b2c3d4-0003-4000-a000-000000000003';
|
||||||
|
|
||||||
|
type TestContext = {
|
||||||
|
navigationMenuItem: NavigationMenuItemManifest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const failingNavigationMenuItemSyncTestCases: EachTestingContext<TestContext>[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: 'when syncing a FOLDER item without name',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.FOLDER,
|
||||||
|
position: 0,
|
||||||
|
icon: 'IconFolder',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'when syncing a FOLDER item with a blank name',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.FOLDER,
|
||||||
|
position: 0,
|
||||||
|
name: ' ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'when syncing a LINK item without link',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.LINK,
|
||||||
|
position: 0,
|
||||||
|
name: 'Link without url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'when syncing a LINK item with a link that is not a valid url',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.LINK,
|
||||||
|
position: 0,
|
||||||
|
name: 'Link with invalid url',
|
||||||
|
link: 'not a link',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:
|
||||||
|
'when syncing an OBJECT item without targetObjectUniversalIdentifier',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.OBJECT,
|
||||||
|
position: 0,
|
||||||
|
name: 'Object without target',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:
|
||||||
|
'when syncing an OBJECT item with a targetObjectUniversalIdentifier that is not a uuid',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.OBJECT,
|
||||||
|
position: 0,
|
||||||
|
name: 'Object with malformed target',
|
||||||
|
targetObjectUniversalIdentifier: 'not-a-uuid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:
|
||||||
|
'when syncing a VIEW item with a viewUniversalIdentifier that is not a uuid',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.VIEW,
|
||||||
|
position: 0,
|
||||||
|
name: 'View with malformed identifier',
|
||||||
|
viewUniversalIdentifier: 'not-a-uuid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:
|
||||||
|
'when syncing a PAGE_LAYOUT item with a pageLayoutUniversalIdentifier that is not a uuid',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: NavigationMenuItemType.PAGE_LAYOUT,
|
||||||
|
position: 0,
|
||||||
|
name: 'Page layout with malformed identifier',
|
||||||
|
pageLayoutUniversalIdentifier: 'not-a-uuid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'when syncing an item with an unknown type',
|
||||||
|
context: {
|
||||||
|
navigationMenuItem: {
|
||||||
|
universalIdentifier: TEST_NAVIGATION_MENU_ITEM_ID,
|
||||||
|
type: 'UNKNOWN_NAVIGATION_MENU_ITEM_TYPE' as NavigationMenuItemType,
|
||||||
|
position: 0,
|
||||||
|
name: 'Item with unknown type',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('Sync application should fail on invalid navigation menu items', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupApplicationForSync({
|
||||||
|
applicationUniversalIdentifier: TEST_APP_ID,
|
||||||
|
name: 'Test Invalid Navigation Menu Item App',
|
||||||
|
description: 'App for testing navigation menu item manifest validation',
|
||||||
|
sourcePath: 'test-invalid-navigation-menu-item',
|
||||||
|
});
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanupApplicationAndAppRegistration({
|
||||||
|
applicationUniversalIdentifier: TEST_APP_ID,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(eachTestingContextFilter(failingNavigationMenuItemSyncTestCases))(
|
||||||
|
'$title',
|
||||||
|
async ({ context }) => {
|
||||||
|
const { errors } = await syncApplication({
|
||||||
|
manifest: buildBaseManifest({
|
||||||
|
appId: TEST_APP_ID,
|
||||||
|
roleId: TEST_ROLE_ID,
|
||||||
|
overrides: {
|
||||||
|
navigationMenuItems: [context.navigationMenuItem],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectToFail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||||
|
},
|
||||||
|
60000,
|
||||||
|
);
|
||||||
|
});
|
||||||
+6
-4
@@ -1,4 +1,4 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`NavigationMenuItem creation should fail when creating with empty targetObjectMetadataId 1`] = `
|
exports[`NavigationMenuItem creation should fail when creating with empty targetObjectMetadataId 1`] = `
|
||||||
{
|
{
|
||||||
@@ -100,11 +100,13 @@ exports[`NavigationMenuItem creation should fail when creating with missing targ
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||||
"message": "targetRecordId and targetObjectMetadataId are required for RECORD type",
|
"message": "A valid targetObjectMetadataUniversalIdentifier is required for RECORD type",
|
||||||
"userFriendlyMessage": "targetRecordId and targetObjectMetadataId are required for RECORD type",
|
"userFriendlyMessage": "A valid targetObjectMetadataUniversalIdentifier is required for RECORD type",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"flatEntityMinimalInformation": {
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": null,
|
||||||
|
"type": "RECORD",
|
||||||
"universalIdentifier": Any<String>,
|
"universalIdentifier": Any<String>,
|
||||||
},
|
},
|
||||||
"metadataName": "navigationMenuItem",
|
"metadataName": "navigationMenuItem",
|
||||||
@@ -118,7 +120,7 @@ exports[`NavigationMenuItem creation should fail when creating with missing targ
|
|||||||
"navigationMenuItem": 1,
|
"navigationMenuItem": 1,
|
||||||
"totalErrors": 1,
|
"totalErrors": 1,
|
||||||
},
|
},
|
||||||
"userFriendlyMessage": "targetRecordId and targetObjectMetadataId are required for RECORD type",
|
"userFriendlyMessage": "A valid targetObjectMetadataUniversalIdentifier is required for RECORD type",
|
||||||
},
|
},
|
||||||
"message": "Multiple validation errors occurred while creating navigation menu items",
|
"message": "Multiple validation errors occurred while creating navigation menu items",
|
||||||
"name": "GraphQLError",
|
"name": "GraphQLError",
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Navigation Menu Item update should fail with circular dependency when folderId equals id (self-reference) 1`] = `
|
exports[`Navigation Menu Item update should fail with circular dependency when folderId equals id (self-reference) 1`] = `
|
||||||
{
|
{
|
||||||
@@ -15,6 +15,8 @@ exports[`Navigation Menu Item update should fail with circular dependency when f
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"flatEntityMinimalInformation": {
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Standalone Folder",
|
||||||
|
"type": "FOLDER",
|
||||||
"universalIdentifier": Any<String>,
|
"universalIdentifier": Any<String>,
|
||||||
},
|
},
|
||||||
"metadataName": "navigationMenuItem",
|
"metadataName": "navigationMenuItem",
|
||||||
@@ -50,6 +52,8 @@ exports[`Navigation Menu Item update should fail with circular dependency when u
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"flatEntityMinimalInformation": {
|
"flatEntityMinimalInformation": {
|
||||||
|
"name": "Parent Folder",
|
||||||
|
"type": "FOLDER",
|
||||||
"universalIdentifier": Any<String>,
|
"universalIdentifier": Any<String>,
|
||||||
},
|
},
|
||||||
"metadataName": "navigationMenuItem",
|
"metadataName": "navigationMenuItem",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Type level counterpart of assertUnreachable: fails to compile when a switch
|
||||||
|
// statement does not handle every case, without any runtime behavior
|
||||||
|
export type AssertUnreachable<T extends never> = T;
|
||||||
@@ -15,6 +15,7 @@ export { AppBasePath } from './AppBasePath';
|
|||||||
export { AppPath } from './AppPath';
|
export { AppPath } from './AppPath';
|
||||||
export type { Arrayable } from './Arrayable';
|
export type { Arrayable } from './Arrayable';
|
||||||
export type { ArraySortDirection } from './ArraySortDirection';
|
export type { ArraySortDirection } from './ArraySortDirection';
|
||||||
|
export type { AssertUnreachable } from './AssertUnreachable.type';
|
||||||
export { CalendarChannelContactAutoCreationPolicy } from './CalendarChannelContactAutoCreationPolicy';
|
export { CalendarChannelContactAutoCreationPolicy } from './CalendarChannelContactAutoCreationPolicy';
|
||||||
export { CalendarChannelSyncStage } from './CalendarChannelSyncStage';
|
export { CalendarChannelSyncStage } from './CalendarChannelSyncStage';
|
||||||
export { CalendarChannelSyncStatus } from './CalendarChannelSyncStatus';
|
export { CalendarChannelSyncStatus } from './CalendarChannelSyncStatus';
|
||||||
|
|||||||
Reference in New Issue
Block a user