Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ object Routes {
const val ENTITY_STATE_AT_SEQUENCE = "entity-state-at-sequence"
}

object Model {
const val REGISTERED_ENTITIES = "model-registered-entities"
const val DOMAIN_EVENTS = "model-domain-events"
const val ENTITY_STATE_AT_SEQUENCE = "model-entity-state-at-sequence"
const val REPLAY_TIMELINE = "model-replay-timeline"
}

object MessageFlow {
const val STATS = "message-flow-stats"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ data class SupportedFeatures(
val clientStatusUpdates: Boolean? = false,
/* Whether the application has the entitlement manager configured, allowing it to receive licenses */
val licenseEntitlement: Boolean? = false,
/* Whether the client supports model inspection (AF5 StateManager-based entity inspection). */
val modelInspection: Boolean? = false,
Comment thread
stefanmirkovic marked this conversation as resolved.
)

data class Versions(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2022-2026. AxonIQ B.V.
*
* Licensed 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.
*/

package io.axoniq.platform.framework.api

data class RegisteredEntitiesResult(
val entities: List<RegisteredEntityInfo>
)

data class RegisteredEntityInfo(
val entityType: String,
val idTypes: List<String>,
/**
* Structural descriptors of the id class's properties. Empty when the id is a
* "simple" type (String, primitives, UUID, etc.) — in that case the frontend should
* render a single text input. When populated, the id is a compound type (record /
* data class / plain object) and the frontend should render one input per descriptor
* and send the entityId as a JSON object keyed by the descriptor names.
*/
val idFields: List<IdFieldDescriptor> = emptyList(),
)

data class IdFieldDescriptor(
val name: String,
/** Normalized form-friendly type: "string", "number", "boolean", "uuid", or "object". */
val type: String,
/** Fully qualified Java type name, useful for diagnostics / future extensions. */
val javaType: String,
)

data class ModelDomainEventsQuery(
val entityType: String,
val entityId: String,
val page: Int = 0,
val pageSize: Int = 10,
)

data class ModelEntityStateAtSequenceQuery(
val entityType: String,
val entityId: String,
val maxSequenceNumber: Long = 0,
)

data class ModelTimelineQuery(
val entityType: String,
val entityId: String,
val offset: Int = 0,
val limit: Int = 100,
)

data class ModelTimelineResult(
val entityType: String,
val entityId: String,
val entries: List<ModelTimelineEntry>,
val offset: Int = 0,
val totalEvents: Int,
val truncated: Boolean,
)

data class ModelTimelineEntry(
val sequenceNumber: Long,
/**
* ISO-8601 formatted timestamp (from [java.time.Instant.toString]).
* String is used here — instead of [java.time.Instant] — to avoid ambiguity in how
* the different serializers (CBOR on the RSocket leg, Jackson on the query-handler leg)
* encode the Instant: some emit an epoch-seconds number, which the frontend would
* then incorrectly treat as milliseconds.
*/
val timestamp: String,
val eventType: String,
val eventPayload: String?,
val stateBefore: String?,
val stateAfter: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class SetupPayloadCreator(
heartbeat = true,
threadDump = true,
clientStatusUpdates = true,
licenseEntitlement = hasEntitlementManager()
licenseEntitlement = hasEntitlementManager(),
modelInspection = hasStateManager(),
)
)
}
Expand Down Expand Up @@ -346,6 +347,18 @@ class SetupPayloadCreator(
}


/**
* Checks whether a StateManager has been registered, indicating AF5 model inspection support.
*/
private fun hasStateManager(): Boolean {
try {
val stateManagerClass = Class.forName("org.axonframework.modelling.StateManager")
return configuration.hasComponent(stateManagerClass)
} catch (_: ClassNotFoundException) {
return false
}
}

/**
* Checks whether the PlatformLicenseSource have been configured, in which case we want updates of licenses from Platform.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2022-2026. AxonIQ B.V.
*
* Licensed 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.
*/

package io.axoniq.platform.framework.eventsourcing;

import io.axoniq.platform.framework.AxoniqPlatformConfigurerEnhancer;
import io.axoniq.platform.framework.client.RSocketHandlerRegistrar;
import org.axonframework.common.configuration.ComponentDefinition;
import org.axonframework.common.configuration.ComponentRegistry;
import org.axonframework.common.configuration.ConfigurationEnhancer;
import org.axonframework.common.lifecycle.Phase;
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
import org.axonframework.modelling.StateManager;

/**
* Enhancer that registers the {@link RSocketModelInspectionResponder} when both
* {@link StateManager} and {@link EventStorageEngine} are available (AF5 applications).
*/
public class AxoniqPlatformModelInspectionEnhancer implements ConfigurationEnhancer {

@Override
public void enhance(ComponentRegistry registry) {
if (!registry.hasComponent(StateManager.class)
|| !registry.hasComponent(EventStorageEngine.class)
|| !registry.hasComponent(RSocketHandlerRegistrar.class)) {
return;
}

registry.registerComponent(ComponentDefinition
.ofType(RSocketModelInspectionResponder.class)
.withBuilder(c -> new RSocketModelInspectionResponder(
c.getComponent(StateManager.class),
c.getComponent(EventStorageEngine.class),
c.getComponent(RSocketHandlerRegistrar.class),
c))
.onStart(Phase.EXTERNAL_CONNECTIONS, RSocketModelInspectionResponder::start));
}

@Override
public int order() {
return AxoniqPlatformConfigurerEnhancer.PLATFORM_ENHANCER_ORDER + 1;
}
}
Loading