diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2cb7856c904af7b0a62edfa196d57632d02c2c4b..5d845a232cb88e94101e56e3d26932af3072a668 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -124,24 +124,24 @@ release python package: when: manual allow_failure: true -build e2e-test nomad image: - stage: build - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [''] - variables: - GIT_SUBMODULE_STRATEGY: recursive - GIT_SUBMODULE_DEPTH: 1 - GIT_SUBMODULE_UPDATE_FLAGS: --jobs 4 - before_script: - - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$CI_DEPENDENCY_PROXY_SERVER\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json - script: - - /kaniko/executor - --context "${CI_PROJECT_DIR}/infra" - --dockerfile "${CI_PROJECT_DIR}/infra/Dockerfile" - --destination "${CI_REGISTRY_IMAGE}:${DOCKER_TAG}-api-e2e" - rules: - - when: on_success +#build e2e-test nomad image: +# stage: build +# image: +# name: gcr.io/kaniko-project/executor:debug +# entrypoint: [''] +# variables: +# GIT_SUBMODULE_STRATEGY: recursive +# GIT_SUBMODULE_DEPTH: 1 +# GIT_SUBMODULE_UPDATE_FLAGS: --jobs 4 +# before_script: +# - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$CI_DEPENDENCY_PROXY_SERVER\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json +# script: +# - /kaniko/executor +# --context "${CI_PROJECT_DIR}/infra" +# --dockerfile "${CI_PROJECT_DIR}/infra/Dockerfile" +# --destination "${CI_REGISTRY_IMAGE}:${DOCKER_TAG}-api-e2e" +# rules: +# - when: on_success build standalone gui image: stage: build @@ -186,53 +186,53 @@ tests: coverage_format: cobertura path: cobertura-coverage.xml -e2e_tests: - stage: test - image: mcr.microsoft.com/playwright:v1.49.0-jammy - services: - - name: ${CI_REGISTRY_IMAGE}:${DOCKER_TAG}-api-e2e - alias: backend - command: - - python - - -m - - nomad.cli - - admin - - run - - appworker - - name: rabbitmq:3.11.5 - alias: rabbitmq - - name: docker.elastic.co/elasticsearch/elasticsearch:7.17.1 - alias: elastic - command: - - bash - - '-c' - - ES_JAVA_OPTS="-Xms512m -Xmx512m" docker-entrypoint.sh elasticsearch -Ediscovery.type=single-node -Expack.security.enabled=false - - name: mongo:5.0.6 - alias: mongo - variables: - RABBITMQ_ERLANG_COOKIE: SWQOKODSQALRPCLNMEQG - RABBITMQ_DEFAULT_USER: rabbitmq - RABBITMQ_DEFAULT_PASS: rabbitmq - RABBITMQ_DEFAULT_VHOST: / - NOMAD_RABBITMQ_HOST: rabbitmq - NOMAD_ELASTIC_HOST: elastic - NOMAD_MONGO_HOST: mongo - NOMAD_NORMALIZE_SPRINGER_DB_PATH: /nomad/fairdi/db/data/springer.msg - VITE_USE_MOCKED_API: false - E2E_API_URL: http://localhost:8000/fairdi/nomad/latest/api/v1 - dependencies: - - install - - build e2e-test nomad image - script: - - sh infra/health_check.sh - - npm run build - - npm ci - - npm run test:e2e - artifacts: - expire_in: 1 days - when: always - paths: - - test-results/ +#e2e_tests: +# stage: test +# image: mcr.microsoft.com/playwright:v1.49.0-jammy +# services: +# - name: ${CI_REGISTRY_IMAGE}:${DOCKER_TAG}-api-e2e +# alias: backend +# command: +# - python +# - -m +# - nomad.cli +# - admin +# - run +# - appworker +# - name: rabbitmq:3.11.5 +# alias: rabbitmq +# - name: docker.elastic.co/elasticsearch/elasticsearch:7.17.1 +# alias: elastic +# command: +# - bash +# - '-c' +# - ES_JAVA_OPTS="-Xms512m -Xmx512m" docker-entrypoint.sh elasticsearch -Ediscovery.type=single-node -Expack.security.enabled=false +# - name: mongo:5.0.6 +# alias: mongo +# variables: +# RABBITMQ_ERLANG_COOKIE: SWQOKODSQALRPCLNMEQG +# RABBITMQ_DEFAULT_USER: rabbitmq +# RABBITMQ_DEFAULT_PASS: rabbitmq +# RABBITMQ_DEFAULT_VHOST: / +# NOMAD_RABBITMQ_HOST: rabbitmq +# NOMAD_ELASTIC_HOST: elastic +# NOMAD_MONGO_HOST: mongo +# NOMAD_NORMALIZE_SPRINGER_DB_PATH: /nomad/fairdi/db/data/springer.msg +# VITE_USE_MOCKED_API: false +# E2E_API_URL: http://localhost:8000/fairdi/nomad/latest/api/v1 +# dependencies: +# - install +# - build e2e-test nomad image +# script: +# - sh infra/health_check.sh +# - npm run build +# - npm ci +# - npm run test:e2e +# artifacts: +# expire_in: 1 days +# when: always +# paths: +# - test-results/ release standalone gui image: stage: release diff --git a/src/components/editor/utils.test.ts b/src/components/editor/utils.test.ts index 1fcf4d9432f48cd34e0e42836cec7d59971c4e2c..f1a188c5885fa5129e53b78a592fd45f9d020eaf 100644 --- a/src/components/editor/utils.test.ts +++ b/src/components/editor/utils.test.ts @@ -1,5 +1,9 @@ import {SubSection} from './SubSectionEditor' -import {calculateRequestFromLayout, defaultSubSectionRequest} from './utils' +import { + calculateRequestFromLayout, + defaultSubSectionRequest, + mergeRequests, +} from './utils' describe('calculateRequestFromLayout', async () => { it('adds quantities', () => { @@ -65,3 +69,82 @@ describe('calculateRequestFromLayout', async () => { }) }) }) + +describe('mergeRequests', () => { + it.each([ + { + description: 'should merge non-conflicting objects successfully', + current: {a: 1, b: {c: 2}}, + request: {d: 3, b: {e: 4}}, + expected: {a: 1, b: {c: 2, e: 4}, d: 3}, + }, + { + description: 'should merge when nested values are non-conflicting', + current: {a: {b: {c: 1}}}, + request: {a: {b: {d: 2}}}, + expected: {a: {b: {c: 1, d: 2}}}, + }, + { + description: 'should handle empty request objects', + current: {a: 1, b: {c: 2}}, + request: {}, + expected: {a: 1, b: {c: 2}}, + }, + { + description: 'should handle empty current objects', + current: {}, + request: {a: 1, b: {c: 2}}, + expected: {a: 1, b: {c: 2}}, + }, + { + description: 'should merge deeply nested non-conflicting objects', + current: {a: {b: {c: {d: 1}}}}, + request: {a: {b: {c: {e: 2, f: {g: 3}}}}}, + expected: {a: {b: {c: {d: 1, e: 2, f: {g: 3}}}}}, + }, + { + description: 'should throw an error on conflicting primitive values', + current: {a: 1, b: 2}, + request: {b: 1}, + error: + 'Conflicting layout data requests for the same property. Conflict detected at path: b', + }, + { + description: 'should throw an error on conflicting nested values', + current: {a: {b: {c: 1}}, x: {y: {z: 1}}}, + request: {a: {b: {c: 1}}, x: {y: {z: 2}}}, + error: + 'Conflicting layout data requests for the same property. Conflict detected at path: x.y.z', + }, + { + description: 'should throw an error for conflicts in arrays', + current: {a: {b: [1, 2, 3]}}, + request: {a: {b: [1, 5, 6]}}, + error: + 'Conflicting layout data requests for the same property. Conflict detected at path: a.b.1', + }, + { + description: + 'should throw an error when a primitive is replaced with an object', + current: {a: {b: 1}}, + request: {a: {b: {c: 2}}}, + error: + 'Conflicting layout data requests for the same property. Conflict detected at path: a.b', + }, + { + description: + 'should throw an error when an object is replaced with a primitive', + current: {a: {b: {c: 2}}}, + request: {a: {b: 1}}, + error: + 'Conflicting layout data requests for the same property. Conflict detected at path: a.b', + }, + ])('$description', ({current, request, expected, error}) => { + if (error) { + expect(() => mergeRequests(current, request)).toThrow(new Error(error)) + } else { + mergeRequests(current, request) + expect(current).toEqual(expected) + } + }) +}) diff --git a/src/components/editor/utils.ts b/src/components/editor/utils.ts index 7e2e2113d0da299e207ff3a7e2933ded07c55615..f97163236780e35956c34cb7add2cf27b206d61b 100644 --- a/src/components/editor/utils.ts +++ b/src/components/editor/utils.ts @@ -1,4 +1,4 @@ -import _ from 'lodash' +import {has, isEqual, isObject, merge} from 'lodash' import {MSectionRequest} from '../../models/graphRequestModels' import {mDefRquest} from '../../pages/entry/entryRoute' @@ -14,6 +14,43 @@ export const defaultSubSectionRequest = { m_def: mDefRquest, } as MSectionRequest +/** + * Merges an `additionalRequest` object into a `request` object, checking for conflicts in the process. + * + * This function ensures that no conflicting values are merged into the `request` object. + * If a conflict is detected, it throws an error with the specific property path where + * the conflict occurs. + * + * @param request - The current object to be updated. + * @param additionalRequest - The new object to merge into the current object. + * @throws {Error} Throws an error if a conflict is detected between `request` and `additionalRequest`. + */ +export function mergeRequests(request: object, additionalRequest: object) { + const checkConflicts = ( + source: object, + obj: object, + path: string[] = [], + ): void => { + for (const [key, value] of Object.entries(obj)) { + const fullPath = [...path, key] + if (has(source, key)) { + const existingValue = source[key] + if (isObject(existingValue) && isObject(value)) { + checkConflicts(existingValue, value, fullPath) + } else if (!isEqual(existingValue, value)) { + throw new Error( + `Conflicting layout data requests for the same property. Conflict detected at path: ${fullPath.join( + '.', + )}`, + ) + } + } + } + } + checkConflicts(request, additionalRequest) + merge(request, additionalRequest) +} + /** * Traverses the given layout spec element populates the given request object with * required request based on all the quantity and subsSection elements in the layout. @@ -31,14 +68,11 @@ export function calculateRequestFromLayout( property: string, parent: MSectionRequest, ) { - let current = parent + const newRequest: MSectionRequest = {} + let current = newRequest assert(property !== undefined, 'Property layout without "property" key.') property.split('.').forEach((item, index, array) => { if (index === array.length - 1) { - assert( - current[item] === undefined || _.isEqual(current[item], request), - 'Two conflicting layout data requests for the same property.', - ) current[item] = request } else { if (current[item] === undefined) { @@ -47,6 +81,7 @@ export function calculateRequestFromLayout( current = current[item] as MSectionRequest } }) + mergeRequests(parent, newRequest) } if ('children' in element) {