Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

---

## πŸ“¦ Safe Dependency Bumps (2026-06-19)

**Repo:** EDDI (`chore/bump-safe-deps`)
**What changed:** Bumped two dependencies to their latest stable patch/minor versions. Both are drop-in upgrades with no breaking changes.

- **`quarkus-mcp-server.version`**: `1.12.1` β†’ `1.13.0` β€” adds lazy SSE initialization for Streamable HTTP transport (defers SSE setup until first API call)
- **`swagger-parser`**: `2.1.42` β†’ `2.1.44` β€” bug fix for unsafe Yaml instantiation in ReferenceVisitor

**File:** `pom.xml`
**Verified:** `mvnw compile` passes cleanly.

---

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## πŸ”’ OpenSSF Scorecard β€” SAST on All Commits (2026-06-18)

**Repo:** EDDI (`fix/code-review-bugs`)
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<failsafe-plugin.version>3.5.5</failsafe-plugin.version>
<failsafe.useModulePath>false</failsafe.useModulePath>
<war-plugin.version>3.4.0</war-plugin.version>
<quarkus-mcp-server.version>1.12.1</quarkus-mcp-server.version>
<quarkus-mcp-server.version>1.13.0</quarkus-mcp-server.version>
<langchain4j-community.version>1.16.0-beta26</langchain4j-community.version>
<skipITs>true</skipITs>
</properties>
Expand Down Expand Up @@ -446,7 +446,7 @@
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.42</version>
<version>2.1.44</version>
</dependency>

</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.bson.Document;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import java.time.Instant;
Expand Down Expand Up @@ -52,14 +53,50 @@ public class MongoTenantQuotaStore implements ITenantQuotaStore {
private final MongoCollection<Document> usage;

@Inject
public MongoTenantQuotaStore(MongoDatabase database) {
public MongoTenantQuotaStore(MongoDatabase database,
@ConfigProperty(name = "eddi.tenant.default-id", defaultValue = "default") String defaultTenantId,
@ConfigProperty(name = "eddi.tenant.quota.enabled", defaultValue = "false") boolean enabled,
@ConfigProperty(name = "eddi.tenant.quota.max-conversations-per-day", defaultValue = "-1") int maxConvPerDay,
@ConfigProperty(name = "eddi.tenant.quota.max-agents-per-tenant", defaultValue = "-1") int maxAgents,
@ConfigProperty(name = "eddi.tenant.quota.max-api-calls-per-minute", defaultValue = "-1") int maxApiCalls,
@ConfigProperty(name = "eddi.tenant.quota.max-monthly-cost-usd", defaultValue = "-1") double maxCost) {

this.quotas = database.getCollection(QUOTAS_COLLECTION);
this.usage = database.getCollection(USAGE_COLLECTION);

// Ensure unique index on tenantId to prevent duplicate rows from upsert races
var indexOptions = new com.mongodb.client.model.IndexOptions().unique(true);
quotas.createIndex(new Document("tenantId", 1), indexOptions);
usage.createIndex(new Document("tenantId", 1), indexOptions);

// Bootstrap default tenant quota if none exists (parity with
// InMemoryTenantQuotaStore).
// Uses $setOnInsert so an existing quota is never overwritten, even under
// races.
quotas.findOneAndUpdate(
Filters.eq("tenantId", defaultTenantId),
Updates.combine(
Updates.setOnInsert("tenantId", defaultTenantId),
Updates.setOnInsert("maxConversationsPerDay", maxConvPerDay),
Updates.setOnInsert("maxAgentsPerTenant", maxAgents),
Updates.setOnInsert("maxApiCallsPerMinute", maxApiCalls),
Updates.setOnInsert("maxMonthlyCostUsd", maxCost),
Updates.setOnInsert("enabled", enabled)),
new FindOneAndUpdateOptions().upsert(true));
LOGGER.infof("Ensured default tenant quota exists: tenantId=%s, enabled=%s, maxConv=%d, maxAgents=%d, maxApi=%d, maxCost=%.2f",
defaultTenantId, enabled, maxConvPerDay, maxAgents, maxApiCalls, maxCost);
}

/**
* Test-only constructor β€” no CDI injection, no bootstrap.
*/
MongoTenantQuotaStore(MongoDatabase database) {
this.quotas = database.getCollection(QUOTAS_COLLECTION);
this.usage = database.getCollection(USAGE_COLLECTION);

var indexOptions = new com.mongodb.client.model.IndexOptions().unique(true);
quotas.createIndex(new Document("tenantId", 1), indexOptions);
usage.createIndex(new Document("tenantId", 1), indexOptions);
}

// ─── Quota Configuration ───
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import javax.sql.DataSource;
Expand Down Expand Up @@ -72,9 +73,31 @@ cost_month VARCHAR(10)
private final Instance<DataSource> dataSourceInstance;
private volatile boolean schemaInitialized = false;

// Bootstrap config β€” stored as fields for lazy initialization in ensureSchema()
private final String defaultTenantId;
private final TenantQuota defaultQuota;

@Inject
public PostgresTenantQuotaStore(Instance<DataSource> dataSourceInstance) {
public PostgresTenantQuotaStore(Instance<DataSource> dataSourceInstance,
@ConfigProperty(name = "eddi.tenant.default-id", defaultValue = "default") String defaultTenantId,
@ConfigProperty(name = "eddi.tenant.quota.enabled", defaultValue = "false") boolean enabled,
@ConfigProperty(name = "eddi.tenant.quota.max-conversations-per-day", defaultValue = "-1") int maxConvPerDay,
@ConfigProperty(name = "eddi.tenant.quota.max-agents-per-tenant", defaultValue = "-1") int maxAgents,
@ConfigProperty(name = "eddi.tenant.quota.max-api-calls-per-minute", defaultValue = "-1") int maxApiCalls,
@ConfigProperty(name = "eddi.tenant.quota.max-monthly-cost-usd", defaultValue = "-1") double maxCost) {

this.dataSourceInstance = dataSourceInstance;
this.defaultTenantId = defaultTenantId;
this.defaultQuota = new TenantQuota(defaultTenantId, maxConvPerDay, maxAgents, maxApiCalls, maxCost, enabled);
}

/**
* Test-only constructor β€” no CDI injection, no bootstrap.
*/
PostgresTenantQuotaStore(Instance<DataSource> dataSourceInstance) {
this.dataSourceInstance = dataSourceInstance;
this.defaultTenantId = null;
this.defaultQuota = null;
}

private synchronized void ensureSchema() {
Expand All @@ -84,56 +107,109 @@ private synchronized void ensureSchema() {
Statement stmt = conn.createStatement()) {
stmt.execute(CREATE_QUOTAS_TABLE);
stmt.execute(CREATE_USAGE_TABLE);
schemaInitialized = true;
LOGGER.info("PostgresTenantQuotaStore initialized (tables=tenant_quotas, tenant_usage)");

// Bootstrap default tenant quota if none exists (parity with
// InMemoryTenantQuotaStore).
// Uses INSERT ... ON CONFLICT DO NOTHING so an existing quota is never
// overwritten.
if (defaultTenantId != null && defaultQuota != null) {
bootstrapDefaultQuota(conn, defaultQuota);
}

schemaInitialized = true;
} catch (SQLException e) {
throw new RuntimeException("Failed to initialize tenant quota tables", e);
}
}

// ─── Quota Configuration ───

@Override
public TenantQuota getQuota(String tenantId) {
ensureSchema();
try (Connection conn = dataSourceInstance.get().getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM tenant_quotas WHERE tenant_id = ?")) {
/**
* Bootstrap-only insert using ON CONFLICT DO NOTHING β€” never overwrites an
* existing quota.
*/
private void bootstrapDefaultQuota(Connection conn, TenantQuota quota) throws SQLException {
try (PreparedStatement ps = conn.prepareStatement(
"""
INSERT INTO tenant_quotas (tenant_id, max_conversations_per_day, max_agents_per_tenant,
max_api_calls_per_minute, max_monthly_cost_usd, enabled)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (tenant_id) DO NOTHING
""")) {
ps.setString(1, quota.tenantId());
ps.setInt(2, quota.maxConversationsPerDay());
ps.setInt(3, quota.maxAgentsPerTenant());
ps.setInt(4, quota.maxApiCallsPerMinute());
ps.setDouble(5, quota.maxMonthlyCostUsd());
ps.setBoolean(6, quota.enabled());
int inserted = ps.executeUpdate();
if (inserted > 0) {
LOGGER.infof("Bootstrapped default tenant quota: tenantId=%s, enabled=%s, maxConv=%d, maxAgents=%d, maxApi=%d, maxCost=%.2f",
quota.tenantId(), quota.enabled(), quota.maxConversationsPerDay(),
quota.maxAgentsPerTenant(), quota.maxApiCallsPerMinute(), quota.maxMonthlyCostUsd());
}
}
}

/**
* Internal quota lookup reusing an existing connection (used during bootstrap).
*/
private TenantQuota getQuotaInternal(Connection conn, String tenantId) throws SQLException {
try (PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM tenant_quotas WHERE tenant_id = ?")) {
ps.setString(1, tenantId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return toQuota(rs);
}
}
} catch (SQLException e) {
LOGGER.warnf("Failed to read quota for tenant '%s': %s", sanitize(tenantId), sanitize(e.getMessage()));
}
return null;
}

@Override
public void setQuota(TenantQuota quota) {
ensureSchema();
try (Connection conn = dataSourceInstance.get().getConnection();
PreparedStatement ps = conn.prepareStatement(
"""
INSERT INTO tenant_quotas (tenant_id, max_conversations_per_day, max_agents_per_tenant,
max_api_calls_per_minute, max_monthly_cost_usd, enabled)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (tenant_id) DO UPDATE SET
max_conversations_per_day = EXCLUDED.max_conversations_per_day,
max_agents_per_tenant = EXCLUDED.max_agents_per_tenant,
max_api_calls_per_minute = EXCLUDED.max_api_calls_per_minute,
max_monthly_cost_usd = EXCLUDED.max_monthly_cost_usd,
enabled = EXCLUDED.enabled
""")) {
/**
* Internal quota upsert reusing an existing connection (used during bootstrap).
*/
private void setQuotaInternal(Connection conn, TenantQuota quota) throws SQLException {
try (PreparedStatement ps = conn.prepareStatement(
"""
INSERT INTO tenant_quotas (tenant_id, max_conversations_per_day, max_agents_per_tenant,
max_api_calls_per_minute, max_monthly_cost_usd, enabled)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (tenant_id) DO UPDATE SET
max_conversations_per_day = EXCLUDED.max_conversations_per_day,
max_agents_per_tenant = EXCLUDED.max_agents_per_tenant,
max_api_calls_per_minute = EXCLUDED.max_api_calls_per_minute,
max_monthly_cost_usd = EXCLUDED.max_monthly_cost_usd,
enabled = EXCLUDED.enabled
""")) {
ps.setString(1, quota.tenantId());
ps.setInt(2, quota.maxConversationsPerDay());
ps.setInt(3, quota.maxAgentsPerTenant());
ps.setInt(4, quota.maxApiCallsPerMinute());
ps.setDouble(5, quota.maxMonthlyCostUsd());
ps.setBoolean(6, quota.enabled());
ps.executeUpdate();
}
}

@Override
public TenantQuota getQuota(String tenantId) {
ensureSchema();
try (Connection conn = dataSourceInstance.get().getConnection()) {
return getQuotaInternal(conn, tenantId);
} catch (SQLException e) {
LOGGER.warnf("Failed to read quota for tenant '%s': %s", sanitize(tenantId), sanitize(e.getMessage()));
}
return null;
}

@Override
public void setQuota(TenantQuota quota) {
ensureSchema();
try (Connection conn = dataSourceInstance.get().getConnection()) {
setQuotaInternal(conn, quota);
} catch (SQLException e) {
LOGGER.errorf("Failed to set quota for tenant '%s': %s", sanitize(quota.tenantId()), sanitize(e.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.IndexOptions;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.mockito.ArgumentCaptor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -527,4 +529,28 @@ void success() {
verify(usageCollection).deleteOne(any(Bson.class));
}
}

// ─── Bootstrap ──────────────────────────────────────────────────────────────

@Nested
@DisplayName("Bootstrap (CDI constructor)")
class Bootstrap {

@Test
@DisplayName("should bootstrap default quota via atomic setOnInsert upsert")
void bootstrapsAtomically() {
// CDI constructor uses $setOnInsert with upsert β€” always calls findOneAndUpdate
lenient().when(quotasCollection.findOneAndUpdate(
any(Bson.class), any(Bson.class), any(FindOneAndUpdateOptions.class))).thenReturn(null);

new MongoTenantQuotaStore(
database, "default", false, -1, -1, -1, -1.0);

// Verify findOneAndUpdate was called with upsert(true) for atomic bootstrap
ArgumentCaptor<FindOneAndUpdateOptions> optionsCaptor = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class);
verify(quotasCollection, atLeastOnce()).findOneAndUpdate(
any(Bson.class), any(Bson.class), optionsCaptor.capture());
assertTrue(optionsCaptor.getValue().isUpsert());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -680,4 +680,37 @@ void resetUsage_sqlException() throws Exception {
assertDoesNotThrow(() -> sut.resetUsage(TENANT_ID));
}
}

// ─── Bootstrap ──────────────────────────────────────────────────────────────

@Nested
@DisplayName("Bootstrap (CDI constructor)")
class Bootstrap {

@Test
@DisplayName("should bootstrap default quota via atomic INSERT ON CONFLICT DO NOTHING")
void bootstrapsAtomically() throws Exception {
// executeUpdate returns 1 (row was inserted β€” no prior quota existed)
when(preparedStatement.executeUpdate()).thenReturn(1);
// getQuota after bootstrap returns no rows (the bootstrap INSERT used a
// different PS)
when(resultSet.next()).thenReturn(false);

var bootstrapStore = new PostgresTenantQuotaStore(
dataSourceInstance, "default", false, -1, -1, -1, -1.0);

// Trigger ensureSchema + bootstrap
bootstrapStore.getQuota("any");

// CREATE TABLE x2 + bootstrap INSERT + getQuota SELECT
verify(statement, times(2)).execute(anyString());
verify(preparedStatement, atLeastOnce()).executeUpdate();

// Assert the bootstrap used ON CONFLICT DO NOTHING (not DO UPDATE)
org.mockito.ArgumentCaptor<String> sqlCaptor = org.mockito.ArgumentCaptor.forClass(String.class);
verify(connection, atLeastOnce()).prepareStatement(sqlCaptor.capture());
assertTrue(sqlCaptor.getAllValues().stream()
.anyMatch(sql -> sql.contains("ON CONFLICT (tenant_id) DO NOTHING")));
}
}
}
Loading