feat(ext-data): add unified Data gateway; mcp: derive roles claim from OIDC
CI / Build & Test (push) Canceled after 1m28s
CI / Build & Test (pull_request) Canceled after 16s

Data/RepositoryFactory/HibernateData give applications one cached,
stateless entry point for repositories per entity type instead of
per-request instantiation, with write()/afterCommit() replacing manual
transaction+reload choreography.

McpConfig.rolesClaimPath is removed - MCP now derives the claim path
from OidcMiddleware.rolesClaimPath() so applications never duplicate
the roles-claim config between OIDC and MCP. McpPackageScanner is
rebuilt on the shared PackageScanner.discover(packageName) primitive,
doing only McpTool/McpResource/McpPrompt classification itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-12 18:37:30 +00:00
co-authored by Claude Sonnet 5
parent 891ef99b8e
commit 9e045287c1
12 changed files with 161 additions and 107 deletions
@@ -1,6 +1,7 @@
package dev.relism.flash.ext.data;
import dev.relism.flash.ext.data.core.Tx;
import dev.relism.flash.ext.data.core.Data;
import dev.relism.flash.ext.data.core.TxDefinition;
import dev.relism.flash.ext.data.core.TxManager;
import dev.relism.flash.ext.data.core.TransactionPropagation;
@@ -19,16 +20,23 @@ public final class DataExtension implements FlashExtension {
private static final MiddlewareKey TRANSACTION = MiddlewareKey.of("flash.data.transaction");
private final TxManager txManager;
private final Tx tx;
private final Data data;
public DataExtension(TxManager txManager) {
this(txManager, null);
}
public DataExtension(TxManager txManager, Data data) {
this.txManager = Objects.requireNonNull(txManager);
this.tx = new Tx(txManager);
this.data = data;
this.tx = data != null ? data.tx() : new Tx(txManager);
}
@Override
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
ctx.provide(Tx.class, tx);
ctx.provide(TxManager.class, txManager);
if (data != null) ctx.provide(Data.class, data);
ctx.addAnnotationProcessor(handlerClass -> {
Transactional ann = handlerClass.getAnnotation(Transactional.class);
if (ann == null) {
@@ -0,0 +1,46 @@
package dev.relism.flash.ext.data.core;
import java.util.Objects;
import java.io.Serializable;
import java.util.concurrent.ConcurrentHashMap;
/**
* Application-facing data gateway. Repositories are created once per entity type and are safe to
* share: transaction/session state stays in {@link Tx}, never in the repository instance.
*/
public final class Data {
private final Tx tx;
private final RepositoryFactory repositories;
private final ConcurrentHashMap<Class<?>, Repository<?, ?>> cache = new ConcurrentHashMap<>();
public Data(Tx tx, RepositoryFactory repositories) {
this.tx = Objects.requireNonNull(tx, "tx");
this.repositories = Objects.requireNonNull(repositories, "repositories");
}
public Tx tx() { return tx; }
@SuppressWarnings("unchecked")
public <T, ID extends Serializable> Repository<T, ID> repository(Class<T> type) {
Objects.requireNonNull(type, "type");
return (Repository<T, ID>) cache.computeIfAbsent(type, key -> repositories.create(tx, type));
}
public void read(Tx.TxRunnable work) { tx.run(tx.readOnly(), work); }
public <T> T read(Tx.TxCallable<T> work) { return tx.call(tx.readOnly(), work); }
public void write(Tx.TxRunnable work) { tx.run(work); }
public <T> T write(Tx.TxCallable<T> work) { return tx.call(work); }
/** Transitional low-level access for infrastructure that needs an explicit definition. */
public void run(Tx.TxRunnable work) { tx.run(work); }
public <T> T call(Tx.TxCallable<T> work) { return tx.call(work); }
public <T> T call(TxDefinition definition, Tx.TxCallable<T> work) { return tx.call(definition, work); }
public TxDefinition readOnly() { return tx.readOnly(); }
/** Registers work that runs only after the enclosing write transaction commits. */
public void afterCommit(Runnable work) {
if (!tx.isActive()) throw new IllegalStateException("afterCommit requires an active transaction");
ResourceRegistry.addSynchronization(new TxSynchronization() {
@Override public void afterCommit() { work.run(); }
});
}
}
@@ -0,0 +1,9 @@
package dev.relism.flash.ext.data.core;
import java.io.Serializable;
/** Creates the backend-specific, stateless repository for one entity type. */
@FunctionalInterface
public interface RepositoryFactory {
<T, ID extends Serializable> Repository<T, ID> create(Tx tx, Class<T> type);
}