This is an automated email from the ASF dual-hosted git repository.
zehnder pushed a commit to branch
4111-refreshing-fields-removes-existing-field-configurations
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to
refs/heads/4111-refreshing-fields-removes-existing-field-configurations by this
push:
new c35d87e1c6 feat(#4111): Implement event schema diff service to apply
user configuration changes
c35d87e1c6 is described below
commit c35d87e1c6beb71bfccf97ce56bc7df73f165601
Author: Philipp Zehnder <[email protected]>
AuthorDate: Fri Jan 23 22:31:04 2026 +0100
feat(#4111): Implement event schema diff service to apply user
configuration changes
---
.../adapter-configuration-state.service.ts | 10 +
.../connect/services/event-schema-diff.service.ts | 207 +++++++++++++++++++++
2 files changed, 217 insertions(+)
diff --git
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
index 5917dfe4fe..0d94319861 100644
---
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
+++
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
@@ -31,6 +31,7 @@ import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmDialogComponent } from '@streampipes/shared-ui';
+import { EventSchemaDiffService } from
'../../../services/event-schema-diff.service';
@Injectable({
providedIn: 'root',
@@ -40,6 +41,7 @@ export class AdapterConfigurationStateService {
private translateService = inject(TranslateService);
private restService = inject(RestService);
private scriptLanguagesService = inject(ConnectScriptLanguagesService);
+ private eventSchemaDiffService = inject(EventSchemaDiffService);
private initialState: AdapterConfigurationState = {
adapterSettingsChanged: false,
@@ -350,6 +352,14 @@ export class AdapterConfigurationStateService {
this.restService.getEventSchema(adapter).subscribe({
next: schema => {
+ const previousEventProperties =
+ this.state().adapterDescription?.dataStream?.eventSchema
+ ?.eventProperties;
+ this.eventSchemaDiffService.applyUserConfiguration(
+ previousEventProperties,
+ schema.eventProperties,
+ );
+
this.sortEventPropertiesAlphabetically(schema);
const updatedAdapter = { ...adapter };
diff --git a/ui/src/app/connect/services/event-schema-diff.service.ts
b/ui/src/app/connect/services/event-schema-diff.service.ts
new file mode 100644
index 0000000000..0802280f21
--- /dev/null
+++ b/ui/src/app/connect/services/event-schema-diff.service.ts
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Injectable } from '@angular/core';
+import {
+ EventProperty,
+ EventPropertyList,
+ EventPropertyNested,
+ EventPropertyPrimitive,
+ EventPropertyUnion,
+} from '@streampipes/platform-services';
+
+@Injectable({ providedIn: 'root' })
+export class EventSchemaDiffService {
+ // Copies user-configurable fields from old to new for matching properties.
+ public applyUserConfiguration(
+ oldEventProperties: EventPropertyUnion[] | undefined,
+ newEventProperties: EventPropertyUnion[] | undefined,
+ ): void {
+ const pairs = this.getMatchingRuntimeTypePairs(
+ oldEventProperties,
+ newEventProperties,
+ );
+
+ for (const { oldProperty, newProperty } of pairs) {
+ newProperty.semanticType = oldProperty.semanticType;
+ newProperty.additionalMetadata = oldProperty.additionalMetadata;
+ newProperty.description = oldProperty.description;
+ newProperty.label = oldProperty.label;
+ newProperty.propertyScope = oldProperty.propertyScope;
+
+ if (
+ this.isPrimitiveProperty(newProperty) &&
+ this.isPrimitiveProperty(oldProperty)
+ ) {
+ const newPrimitive = newProperty as EventPropertyPrimitive;
+ const oldPrimitive = oldProperty as EventPropertyPrimitive;
+ newPrimitive.measurementUnit = oldPrimitive.measurementUnit;
+ newPrimitive.runtimeType = oldPrimitive.runtimeType;
+ }
+ }
+ }
+
+ // Returns only matching old/new pairs where runtimeType is unchanged.
+ private getMatchingRuntimeTypePairs(
+ oldEventProperties: EventPropertyUnion[] | undefined,
+ newEventProperties: EventPropertyUnion[] | undefined,
+ ): Array<{
+ oldProperty: EventProperty;
+ newProperty: EventProperty;
+ }> {
+ const diff = this.compareEventSchemas(
+ oldEventProperties,
+ newEventProperties,
+ );
+
+ return diff
+ .filter(entry => {
+ if (!entry.oldProperty || !entry.newProperty) {
+ return false;
+ }
+ const oldDataType = this.getDataType(entry.oldProperty);
+ const newDataType = this.getDataType(entry.newProperty);
+ if (oldDataType !== undefined && newDataType !== undefined) {
+ return oldDataType === newDataType;
+ }
+
+ const oldIsPrimitive = this.isPrimitiveProperty(
+ entry.oldProperty,
+ );
+ const newIsPrimitive = this.isPrimitiveProperty(
+ entry.newProperty,
+ );
+ return !oldIsPrimitive && !newIsPrimitive;
+ })
+ .map(entry => ({
+ oldProperty: entry.oldProperty as EventProperty,
+ newProperty: entry.newProperty as EventProperty,
+ }));
+ }
+
+ // Returns a path-based comparison of old vs. new event properties for
diffing or merge logic.
+ private compareEventSchemas(
+ oldEventProperties: EventPropertyUnion[] | undefined,
+ newEventProperties: EventPropertyUnion[] | undefined,
+ ): Array<{
+ path: string;
+ oldProperty?: EventPropertyUnion;
+ newProperty?: EventPropertyUnion;
+ }> {
+ const oldMap = this.buildEventPropertyPathMap(oldEventProperties);
+ const newMap = this.buildEventPropertyPathMap(newEventProperties);
+
+ const paths = new Set<string>();
+ oldMap.forEach((_value, key) => paths.add(key));
+ newMap.forEach((_value, key) => paths.add(key));
+
+ return Array.from(paths)
+ .sort()
+ .map(path => ({
+ path,
+ oldProperty: oldMap.get(path),
+ newProperty: newMap.get(path),
+ }));
+ }
+
+ private buildEventPropertyPathMap(
+ eventProperties: EventPropertyUnion[] | undefined,
+ basePath: string = '',
+ ): Map<string, EventPropertyUnion> {
+ const map = new Map<string, EventPropertyUnion>();
+
+ if (!eventProperties) {
+ return map;
+ }
+
+ for (const eventProperty of eventProperties) {
+ const path = this.buildEventPropertyPath(
+ basePath,
+ eventProperty?.runtimeName,
+ );
+ if (path) {
+ map.set(path, eventProperty);
+ }
+
+ if (this.isNestedProperty(eventProperty)) {
+ const childMap = this.buildEventPropertyPathMap(
+ eventProperty.eventProperties,
+ path,
+ );
+ childMap.forEach((value, key) => map.set(key, value));
+ } else if (this.isListProperty(eventProperty)) {
+ const listPath = path ? `${path}[]` : '[]';
+ const childMap = this.buildEventPropertyPathMap(
+ [eventProperty.eventProperty],
+ listPath,
+ );
+ childMap.forEach((value, key) => map.set(key, value));
+ }
+ }
+
+ return map;
+ }
+
+ private buildEventPropertyPath(
+ basePath: string,
+ runtimeName?: string,
+ ): string {
+ if (!runtimeName) {
+ return basePath;
+ }
+ return basePath ? `${basePath}.${runtimeName}` : runtimeName;
+ }
+
+ private isNestedProperty(
+ eventProperty: EventPropertyUnion,
+ ): eventProperty is EventPropertyNested {
+ return (
+ eventProperty?.['@class'] ===
+ 'org.apache.streampipes.model.schema.EventPropertyNested'
+ );
+ }
+
+ private isListProperty(
+ eventProperty: EventPropertyUnion,
+ ): eventProperty is EventPropertyList {
+ return (
+ eventProperty?.['@class'] ===
+ 'org.apache.streampipes.model.schema.EventPropertyList'
+ );
+ }
+
+ private isPrimitiveProperty(
+ eventProperty: EventProperty,
+ ): eventProperty is EventPropertyPrimitive {
+ return (
+ eventProperty?.['@class'] ===
+ 'org.apache.streampipes.model.schema.EventPropertyPrimitive'
+ );
+ }
+
+ private getDataType(eventProperty: EventProperty): string | undefined {
+ const originType = eventProperty?.additionalMetadata?.originType;
+ if (originType !== undefined) {
+ return originType;
+ }
+ if (this.isPrimitiveProperty(eventProperty)) {
+ return eventProperty.runtimeType;
+ }
+ return undefined;
+ }
+}