From 3f0ff04b20e13a0988edb01ffb4c736da7bccdf2 Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Wed, 30 Oct 2024 15:24:52 +0100 Subject: [PATCH] fix: Check for circular references (#13783) Co-authored-by: Konrad-Simso --- frontend/language/src/nb.json | 1 + .../src/ArrayUtils/ArrayUtils.test.ts | 18 +++ .../src/ArrayUtils/ArrayUtils.ts | 4 + .../hooks/useAddReference.test.ts | 11 ++ .../SchemaEditor/hooks/useAddReference.ts | 28 +++- .../hooks/useMoveProperty.test.ts | 12 ++ .../SchemaEditor/hooks/useMoveProperty.ts | 111 ++++++++++--- .../CircularReferenceDetector.test.ts | 149 ++++++++++++++++++ .../SchemaModel/CircularReferenceDetector.ts | 98 ++++++++++++ .../src/lib/SchemaModel/SchemaModel.ts | 9 ++ 10 files changed, 417 insertions(+), 24 deletions(-) create mode 100644 frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.test.ts create mode 100644 frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.ts diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 56ff9476aed..53b5b25796b 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -855,6 +855,7 @@ "schema_editor.enum_error_duplicate": "Verdiene må være unike.", "schema_editor.enum_legend": "Liste med gyldige verdier", "schema_editor.enum_value": "Gyldig verdi nummer {{index}}", + "schema_editor.error_circular_references": "Du kan ikke utføre denne operasjonen fordi Studio ikke støtter sirkulære referanser.", "schema_editor.error_could_not_detect_taskType": "Kunne ikke hente oppgavetype for {{layout}}", "schema_editor.error_could_not_detect_taskType_description": "Dette gjør at vi ikke kan vise riktig liste over tilgjengelige komponenter. Velg en annen sidegruppe eller prøv å last siden på nytt.", "schema_editor.error_data_type_name_exists": "Modellen kan ikke ha samme navn som datatyper i løsningen.", diff --git a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts index 6b2bb066296..7e28ccd6d35 100644 --- a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts +++ b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts @@ -118,6 +118,24 @@ describe('ArrayUtils', () => { }); }); + describe('hasIntersection', () => { + it('Returns true when arrays have one common element', () => { + expect(ArrayUtils.hasIntersection([1, 2, 3], [3, 4, 5])).toBe(true); + }); + + it('Returns true when arrays have multiple common elements', () => { + expect(ArrayUtils.hasIntersection([1, 2, 3], [3, 2, 5])).toBe(true); + }); + + it('Returns false when arrays have no common elements', () => { + expect(ArrayUtils.hasIntersection([1, 2, 3], [4, 5, 6])).toBe(false); + }); + + it('Returns false when the arrays are empty', () => { + expect(ArrayUtils.hasIntersection([], [])).toBe(false); + }); + }); + describe('replaceLastItem', () => { it('should replace the last item in an array and return the modified array', () => { expect(ArrayUtils.replaceLastItem([1, 2, 3], 99)).toEqual([1, 2, 99]); diff --git a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts index 0056739527d..876021b059f 100644 --- a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts +++ b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts @@ -31,6 +31,10 @@ export class ArrayUtils { /** Returns the last item of the given array */ public static last = (array: T[]): T => array[array.length - 1]; + public static hasIntersection = (arrA: T[], arrB: T[]): boolean => { + return arrA.some((x) => arrB.includes(x)); + }; + /** * Returns an array of which the element of arrA are either present or not present in arrB based on the include param. * @param arrA The first array. diff --git a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.test.ts b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.test.ts index ef6ac3048e6..4e900b1c75d 100644 --- a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.test.ts +++ b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.test.ts @@ -78,4 +78,15 @@ describe('useAddReference', () => { const addedReferenceNode = addedChild as ReferenceNode; expect(addedReferenceNode.reference).toEqual(definitionNodeMock.schemaPointer); }); + + it('Does not add a reference when the reference would result in a circular reference', () => { + const { add, save } = setup(); + const uniquePointerOfDefinition = SchemaModel.getUniquePointer( + definitionNodeMock.schemaPointer, + ); + const target: ItemPosition = { parentId: uniquePointerOfDefinition, index: 0 }; + jest.spyOn(window, 'alert').mockImplementation(jest.fn()); + add(nameOfDefinition, target); + expect(save).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.ts b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.ts index 5b46ca551a9..64fbc51ec58 100644 --- a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.ts +++ b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useAddReference.ts @@ -1,18 +1,26 @@ import type { HandleAdd, ItemPosition } from 'app-shared/types/dndTypes'; import { useCallback } from 'react'; import type { NodePosition } from '@altinn/schema-model'; -import { SchemaModel } from '@altinn/schema-model'; +import { createDefinitionPointer, SchemaModel } from '@altinn/schema-model'; + import { calculatePositionInFullList } from '../utils'; import { useSavableSchemaModel } from '../../../hooks/useSavableSchemaModel'; import { useSchemaEditorAppContext } from '@altinn/schema-editor/hooks/useSchemaEditorAppContext'; +import { useTranslation } from 'react-i18next'; export const useAddReference = (): HandleAdd => { const { setSelectedUniquePointer } = useSchemaEditorAppContext(); const savableModel = useSavableSchemaModel(); + const circularReferenceError = useCircularReferenceError(); + const circularReferenceAlert = useCircularReferenceAlert(); return useCallback( (reference: string, position: ItemPosition) => { const index = calculatePositionInFullList(savableModel, position); const parentPointer = savableModel.getSchemaPointerByUniquePointer(position.parentId); + if (circularReferenceError(reference, parentPointer)) { + circularReferenceAlert(); + return; + } const target: NodePosition = { parentPointer, index }; const { schemaPointer } = savableModel.getFinalNode(target.parentPointer); const refName = savableModel.generateUniqueChildName(schemaPointer, 'ref'); @@ -20,6 +28,22 @@ export const useAddReference = (): HandleAdd => { const uniquePointer = SchemaModel.getUniquePointer(ref.schemaPointer); setSelectedUniquePointer(uniquePointer); }, - [savableModel, setSelectedUniquePointer], + [savableModel, setSelectedUniquePointer, circularReferenceAlert, circularReferenceError], ); }; + +function useCircularReferenceError(): (reference: string, targetSchemaPointer: string) => boolean { + const savableModel = useSavableSchemaModel(); + return useCallback( + (reference: string, targetSchemaPointer: string) => { + const referencePointer = createDefinitionPointer(reference); + return savableModel.willResultInCircularReferences(referencePointer, targetSchemaPointer); + }, + [savableModel], + ); +} + +function useCircularReferenceAlert(): () => void { + const { t } = useTranslation(); + return useCallback(() => alert(t('schema_editor.error_circular_references')), [t]); +} diff --git a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.test.ts b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.test.ts index 4a0501a5384..ca4191119ea 100644 --- a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.test.ts +++ b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.test.ts @@ -18,6 +18,7 @@ import { objectChildMock, objectNodeMock, referenceNodeMock, + referredNodeMock, rootNodeMock, toggableNodeMock, uiSchemaNodesMock, @@ -229,4 +230,15 @@ describe('useMoveProperty', () => { expect(setSelectedUniquePointerMock).toHaveBeenCalledTimes(1); expect(setSelectedUniquePointerMock).toHaveBeenCalledWith(expectedFinalUniquePointer); }); + + it('Does not move the node when it would result in circular references', () => { + const { move, save } = setup(); + const pointerOfNodeToMove = referenceNodeMock.schemaPointer; + const pointerOfNewParent = referredNodeMock.schemaPointer; + const indexInNewParent = 0; + const target: ItemPosition = { parentId: pointerOfNewParent, index: indexInNewParent }; + jest.spyOn(window, 'alert').mockImplementation(jest.fn()); + move(pointerOfNodeToMove, target); + expect(save).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.ts b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.ts index 7b87dc5fd03..99b21978a49 100644 --- a/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.ts +++ b/frontend/packages/schema-editor/src/components/SchemaEditor/hooks/useMoveProperty.ts @@ -7,11 +7,68 @@ import { useSavableSchemaModel } from '../../../hooks/useSavableSchemaModel'; import { useTranslation } from 'react-i18next'; import { useSchemaEditorAppContext } from '@altinn/schema-editor/hooks/useSchemaEditorAppContext'; +type MovementErrorCode = 'circular' | 'colliding_names'; + export const useMoveProperty = (): HandleMove => { const savableModel = useSavableSchemaModel(); const { selectedUniquePointer, setSelectedUniquePointer } = useSchemaEditorAppContext(); - const { t } = useTranslation(); - const areThereCollidingNames = useCallback( + const movementError = useMovementError(); + const movementErrorAlert = useMovementErrorAlert(); + + return useCallback( + (uniquePointer: string, position: ItemPosition) => { + const schemaPointer = savableModel.getSchemaPointerByUniquePointer(uniquePointer); + const schemaParentPointer = savableModel.getSchemaPointerByUniquePointer(position.parentId); + const index = calculatePositionInFullList(savableModel, position); + const target: NodePosition = { parentPointer: schemaParentPointer, index }; + const name = extractNameFromPointer(schemaPointer); + const error = movementError(schemaPointer, schemaParentPointer); + if (error) { + const parent = extractNameFromPointer(schemaParentPointer); + movementErrorAlert(error, { name, parent }); + return; + } + const movedNode = savableModel.moveNode(schemaPointer, target); + if (selectedUniquePointer === uniquePointer) { + const movedUniquePointer = SchemaModel.getUniquePointer( + movedNode.schemaPointer, + position.parentId, + ); + setSelectedUniquePointer(movedUniquePointer); + } + }, + [ + savableModel, + movementError, + selectedUniquePointer, + setSelectedUniquePointer, + movementErrorAlert, + ], + ); +}; + +function useMovementError(): ( + schemaPointer: string, + targetSchemaPointer: string, +) => MovementErrorCode | null { + const savableModel = useSavableSchemaModel(); + const areThereCollidingNames = useCollidingNamesError(); + return useCallback( + (schemaPointer: string, targetSchemaPointer: string): MovementErrorCode | null => { + if (savableModel.willResultInCircularReferences(schemaPointer, targetSchemaPointer)) { + return 'circular'; + } else if (areThereCollidingNames(schemaPointer, targetSchemaPointer)) { + return 'colliding_names'; + } + return null; + }, + [savableModel, areThereCollidingNames], + ); +} + +function useCollidingNamesError(): (schemaPointer: string, targetSchemaPointer: string) => boolean { + const savableModel = useSavableSchemaModel(); + return useCallback( (schemaPointer: string, schemaParentPointer: string): boolean => { const currentParent = savableModel.getParentNode(schemaPointer); const isMovingWithinSameParent = schemaParentPointer === currentParent.schemaPointer; @@ -24,28 +81,38 @@ export const useMoveProperty = (): HandleMove => { }, [savableModel], ); +} +type AlertMessageInfo = { + name: string; + parent: string; +}; + +function useMovementErrorAlert(): (errorCode: MovementErrorCode, info: AlertMessageInfo) => void { + const movementErrorMessage = useMovementErrorMessage(); return useCallback( - (uniquePointer: string, position: ItemPosition) => { - const schemaPointer = savableModel.getSchemaPointerByUniquePointer(uniquePointer); - const schemaParentPointer = savableModel.getSchemaPointerByUniquePointer(position.parentId); - const index = calculatePositionInFullList(savableModel, position); - const target: NodePosition = { parentPointer: schemaParentPointer, index }; - const name = extractNameFromPointer(schemaPointer); - if (areThereCollidingNames(schemaPointer, schemaParentPointer)) { - const parent = extractNameFromPointer(schemaParentPointer); - alert(t('schema_editor.move_node_same_name_error', { name, parent })); - } else { - const movedNode = savableModel.moveNode(schemaPointer, target); - if (selectedUniquePointer === uniquePointer) { - const movedUniquePointer = SchemaModel.getUniquePointer( - movedNode.schemaPointer, - position.parentId, - ); - setSelectedUniquePointer(movedUniquePointer); - } + (errorCode: MovementErrorCode, info: AlertMessageInfo) => { + const message = movementErrorMessage(errorCode, info); + alert(message); + }, + [movementErrorMessage], + ); +} + +function useMovementErrorMessage(): ( + errorCode: MovementErrorCode, + info: AlertMessageInfo, +) => string { + const { t } = useTranslation(); + return useCallback( + (errorCode: MovementErrorCode, info: AlertMessageInfo) => { + switch (errorCode) { + case 'circular': + return t('schema_editor.error_circular_references'); + case 'colliding_names': + return t('schema_editor.move_node_same_name_error', info); } }, - [savableModel, t, areThereCollidingNames, selectedUniquePointer, setSelectedUniquePointer], + [t], ); -}; +} diff --git a/frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.test.ts b/frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.test.ts new file mode 100644 index 00000000000..f87c8c418ad --- /dev/null +++ b/frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.test.ts @@ -0,0 +1,149 @@ +import { ROOT_POINTER } from '../constants'; +import { FieldType, type NodePosition, ObjectKind } from '../../types'; +import { SchemaModel } from './SchemaModel'; +import type { FieldNode } from '../../types/FieldNode'; +import { nodeMockBase } from '../../../test/uiSchemaMock'; + +describe('CircularReferenceDetector', () => { + describe('willResultInCircularReferences', () => { + it('Returns false when there are no types involved', () => { + const model = createCleanModel(); + const field = model.addField('name'); + const childPointer = field.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, ROOT_POINTER); + expect(result).toBe(false); + }); + + it('Returns false when the parent is not referred by the child', () => { + const model = createCleanModel(); + const defName = 'def'; + const def = model.addFieldType(defName); + model.addReference('ref', defName); + const object = model.addField('object', FieldType.Object); + const childPointer = def.schemaPointer; + const parentPointer = object.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(false); + }); + + it('Returns true when the child node and the parent node are the same', () => { + const model = createCleanModel(); + const def = model.addFieldType('def'); + const childPointer = def.schemaPointer; + const parentPointer = def.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + + it('Returns true when the parent node is a reference to the child node', () => { + const model = createCleanModel(); + const defName = 'def'; + const def = model.addFieldType(defName); + const ref = model.addReference('ref', defName); + const childPointer = def.schemaPointer; + const parentPointer = ref.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + + it('Returns true when the parent node is a child of the child node', () => { + const model = createCleanModel(); + const defName = 'def'; + const def = model.addFieldType(defName); + const objectTarget: NodePosition = { parentPointer: def.schemaPointer, index: -1 }; + const object = model.addField('object', FieldType.Object, objectTarget); + const childPointer = def.schemaPointer; + const parentPointer = object.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + + it('Returns true when the child node contains a reference to the parent node', () => { + const model = createCleanModel(); + const defName = 'def'; + model.addFieldType(defName); + const rootRef = model.addReference('ref', defName); + const object = model.addField('object', FieldType.Object); + model.addReference('ref', defName, { parentPointer: object.schemaPointer, index: -1 }); + const parentPointer = rootRef.schemaPointer; + const childPointer = object.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + + it('Returns true when the child node contains an object with a reference to the parent node', () => { + const model = createCleanModel(); + const defName = 'def'; + model.addFieldType(defName); + const rootRef = model.addReference('ref', defName); + const firstLevelObject = model.addField('object', FieldType.Object); + const secondLevelObject = model.addField('object', FieldType.Object, { + parentPointer: firstLevelObject.schemaPointer, + index: -1, + }); + model.addReference('ref', defName, { + parentPointer: secondLevelObject.schemaPointer, + index: -1, + }); + const parentPointer = rootRef.schemaPointer; + const childPointer = firstLevelObject.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + + it('Returns true when the child node contains a reference with a reference to the parent node', () => { + const model = createCleanModel(); + + const object = model.addField('object', FieldType.Object); + + const firstLevelDefName = 'firstLevelDef'; + const firstLevelDef = model.addFieldType(firstLevelDefName); + model.addReference('ref', firstLevelDefName, { + parentPointer: object.schemaPointer, + index: -1, + }); + + const secondLevelDefName = 'secondLevelDef'; + const secondLevelDef = model.addFieldType(secondLevelDefName); + model.addReference('ref', secondLevelDefName, { + parentPointer: firstLevelDef.schemaPointer, + index: -1, + }); + + const parentPointer = secondLevelDef.schemaPointer; + const childPointer = object.schemaPointer; + + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + + it('Returns true when the parent node is an object within the node referred by the child node', () => { + const model = createCleanModel(); + + const defName = 'def'; + const def = model.addFieldType(defName); + const ref = model.addReference('ref', defName); + + const object = model.addField('object', FieldType.Object, { + parentPointer: def.schemaPointer, + index: -1, + }); + + const parentPointer = object.schemaPointer; + const childPointer = ref.schemaPointer; + const result = model.willResultInCircularReferences(childPointer, parentPointer); + expect(result).toBe(true); + }); + }); +}); + +function createCleanModel(): SchemaModel { + const rootNode: FieldNode = { + ...nodeMockBase, + schemaPointer: ROOT_POINTER, + objectKind: ObjectKind.Field, + fieldType: FieldType.Object, + children: [], + }; + return SchemaModel.fromArray([rootNode]); +} diff --git a/frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.ts b/frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.ts new file mode 100644 index 00000000000..1546b9d56b1 --- /dev/null +++ b/frontend/packages/schema-model/src/lib/SchemaModel/CircularReferenceDetector.ts @@ -0,0 +1,98 @@ +import { SchemaModelBase } from './SchemaModelBase'; +import type { NodeMap } from '../../types/NodeMap'; +import type { UiSchemaNode, UiSchemaNodes } from '../../types'; +import { isDefinition, isReference } from '../utils'; +import { ROOT_POINTER } from '../constants'; +import { ArrayUtils } from '@studio/pure-functions'; + +export class CircularReferenceDetector extends SchemaModelBase { + constructor(nodes: NodeMap) { + super(nodes); + } + + public willResultInCircularReferences( + childSchemaPointer: string, + parentSchemaPointer: string, + ): boolean { + const childDefinitions = this.listDefinitionsWithin(childSchemaPointer); + const parentDefinitions = this.listAllDefinitionsAbove(parentSchemaPointer); + return ArrayUtils.hasIntersection(childDefinitions, parentDefinitions); + } + + private listDefinitionsWithin(schemaPointer: string): string[] { + const allNodesWithin = this.listAllNodesWithin(schemaPointer); + return this.filterDefinitions(allNodesWithin); + } + + private listAllNodesWithin(schemaPointer: string): UiSchemaNode[] { + const node = this.getNodeBySchemaPointer(schemaPointer); + const allNodesWithin: UiSchemaNode[] = [node]; + const children = this.getChildNodes(schemaPointer); + children.forEach((child) => { + allNodesWithin.push(...this.listAllNodesWithin(child.schemaPointer)); + }); + return allNodesWithin; + } + + private filterDefinitions(nodes: UiSchemaNodes): string[] { + const directDefinitions = this.filterDirectDefinitions(nodes); + const refererredDefinitions = this.filterReferredDefinitions(nodes); + return [...directDefinitions, ...refererredDefinitions]; + } + + private filterDirectDefinitions(nodes: UiSchemaNodes): string[] { + return nodes.filter((node) => this.isDefinitionRoot(node)).map((node) => node.schemaPointer); + } + + private isDefinitionRoot(node: UiSchemaNode): boolean { + return isDefinition(node) && this.isChildOfRoot(node); + } + + private isChildOfRoot(node: UiSchemaNode): boolean { + const parent = this.getParentNode(node.schemaPointer); + return parent && parent.schemaPointer === ROOT_POINTER; + } + + private filterReferredDefinitions(nodes: UiSchemaNodes): string[] { + return nodes.filter(isReference).map((node) => node.reference); + } + + private listAllDefinitionsAbove(schemaPointer: string): string[] { + const allNodesAbove = this.listAllNodesAbove(schemaPointer); + return this.filterDefinitions(allNodesAbove); + } + + private listAllNodesAbove(schemaPointer: string): UiSchemaNode[] { + const node = this.getNodeBySchemaPointer(schemaPointer); + const equivalentPropertyNodeParents = this.getEquivalentPropertyNodeParents(node); + const allNodesAbove: UiSchemaNode[] = [node]; + equivalentPropertyNodeParents.forEach((parent) => { + const nodesAbove = this.listAllNodesAbove(parent.schemaPointer); + allNodesAbove.push(...nodesAbove); + }); + return allNodesAbove; + } + + private getEquivalentPropertyNodeParents(node: UiSchemaNode): UiSchemaNode[] { + const equivalentPropertyNodes = this.getEquivalentPropertyNodes(node); + return this.getParentsOfNodes(equivalentPropertyNodes); + } + + private getEquivalentPropertyNodes(node: UiSchemaNode): UiSchemaNode[] { + const equivalentPropertyNodes: UiSchemaNodes = [node]; + if (this.isDefinitionRoot(node)) { + const referringNodes = this.getReferringNodes(node.schemaPointer); + equivalentPropertyNodes.push(...referringNodes); + } + return equivalentPropertyNodes; + } + + private getParentsOfNodes(nodes: UiSchemaNodes): UiSchemaNode[] { + const parentNodes: UiSchemaNodes = []; + nodes.forEach((node) => { + const parent = this.getParentNode(node.schemaPointer); + if (parent) parentNodes.push(parent); + }); + return parentNodes; + } +} diff --git a/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts b/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts index 06484fd850c..282afa3bb69 100644 --- a/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts +++ b/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts @@ -33,6 +33,7 @@ import { } from '../../config/default-nodes'; import { convertPropToType } from '../mutations/convert-node'; import { SchemaModelBase } from './SchemaModelBase'; +import { CircularReferenceDetector } from './CircularReferenceDetector'; export class SchemaModel extends SchemaModelBase { constructor(nodes: NodeMap) { @@ -473,6 +474,14 @@ export class SchemaModel extends SchemaModelBase { newModel.getNodeMap().forEach((node) => this.nodeMap.set(node.schemaPointer, node)); return this; } + + public willResultInCircularReferences( + childSchemaPointer: string, + parentSchemaPointer: string, + ): boolean { + const detector = new CircularReferenceDetector(this.nodeMap); + return detector.willResultInCircularReferences(childSchemaPointer, parentSchemaPointer); + } } const defaultNodePosition: NodePosition = { parentPointer: ROOT_POINTER, index: -1 };