feat(license): PostgresLicenseRepository + LicenseRecord
JdbcTemplate-backed repo; upsert is ON CONFLICT (tenant_id), touch updates only last_validated_at, delete is provided for future operator-clear flow (not exposed as REST in v1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -203,4 +203,12 @@ public class StorageBeanConfig {
|
||||
ClickHouseUsageTracker usageTracker) {
|
||||
return new com.cameleer.server.app.analytics.UsageFlushScheduler(usageTracker);
|
||||
}
|
||||
|
||||
// ── License Repository ───────────────────────────────────────────
|
||||
|
||||
@Bean
|
||||
public com.cameleer.server.app.license.LicenseRepository licenseRepository(
|
||||
JdbcTemplate jdbcTemplate) {
|
||||
return new com.cameleer.server.app.license.PostgresLicenseRepository(jdbcTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record LicenseRecord(
|
||||
String tenantId,
|
||||
String token,
|
||||
UUID licenseId,
|
||||
Instant installedAt,
|
||||
String installedBy,
|
||||
Instant expiresAt,
|
||||
Instant lastValidatedAt
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface LicenseRepository {
|
||||
Optional<LicenseRecord> findByTenantId(String tenantId);
|
||||
|
||||
/** Insert or replace the row for tenantId. */
|
||||
void upsert(LicenseRecord record);
|
||||
|
||||
/** Update last_validated_at to `now` and return rows affected (0 = no row). */
|
||||
int touchValidated(String tenantId, Instant now);
|
||||
|
||||
/** Delete the row (used when the operator clears a license; not a public API in v1). */
|
||||
int delete(String tenantId);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PostgresLicenseRepository implements LicenseRepository {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
public PostgresLicenseRepository(JdbcTemplate jdbc) {
|
||||
this.jdbc = jdbc;
|
||||
}
|
||||
|
||||
private static final RowMapper<LicenseRecord> MAPPER = (rs, n) -> new LicenseRecord(
|
||||
rs.getString("tenant_id"),
|
||||
rs.getString("token"),
|
||||
(UUID) rs.getObject("license_id"),
|
||||
rs.getTimestamp("installed_at").toInstant(),
|
||||
rs.getString("installed_by"),
|
||||
rs.getTimestamp("expires_at").toInstant(),
|
||||
rs.getTimestamp("last_validated_at").toInstant()
|
||||
);
|
||||
|
||||
@Override
|
||||
public Optional<LicenseRecord> findByTenantId(String tenantId) {
|
||||
return jdbc.query(
|
||||
"SELECT tenant_id, token, license_id, installed_at, installed_by, expires_at, last_validated_at " +
|
||||
"FROM license WHERE tenant_id = ?",
|
||||
MAPPER, tenantId).stream().findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsert(LicenseRecord r) {
|
||||
jdbc.update(
|
||||
"INSERT INTO license (tenant_id, token, license_id, installed_at, installed_by, expires_at, last_validated_at) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?) " +
|
||||
"ON CONFLICT (tenant_id) DO UPDATE SET " +
|
||||
" token = EXCLUDED.token, " +
|
||||
" license_id = EXCLUDED.license_id, " +
|
||||
" installed_at = EXCLUDED.installed_at, " +
|
||||
" installed_by = EXCLUDED.installed_by, " +
|
||||
" expires_at = EXCLUDED.expires_at, " +
|
||||
" last_validated_at = EXCLUDED.last_validated_at",
|
||||
r.tenantId(), r.token(), r.licenseId(),
|
||||
Timestamp.from(r.installedAt()), r.installedBy(),
|
||||
Timestamp.from(r.expiresAt()), Timestamp.from(r.lastValidatedAt())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int touchValidated(String tenantId, Instant now) {
|
||||
return jdbc.update(
|
||||
"UPDATE license SET last_validated_at = ? WHERE tenant_id = ?",
|
||||
Timestamp.from(now), tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(String tenantId) {
|
||||
return jdbc.update("DELETE FROM license WHERE tenant_id = ?", tenantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.cameleer.server.app.license;
|
||||
|
||||
import com.cameleer.server.app.AbstractPostgresIT;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PostgresLicenseRepositoryIT extends AbstractPostgresIT {
|
||||
|
||||
@Autowired LicenseRepository repo;
|
||||
|
||||
@Test
|
||||
void roundTrip() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
LicenseRecord rec = new LicenseRecord(
|
||||
"default", "tok.sig", id, now, "system",
|
||||
now.plus(365, ChronoUnit.DAYS), now);
|
||||
repo.upsert(rec);
|
||||
|
||||
var loaded = repo.findByTenantId("default").orElseThrow();
|
||||
assertThat(loaded.licenseId()).isEqualTo(id);
|
||||
assertThat(loaded.installedBy()).isEqualTo("system");
|
||||
}
|
||||
|
||||
@Test
|
||||
void touchValidated_updatesTimestamp() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Instant t0 = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
repo.upsert(new LicenseRecord("default", "tok.sig", id, t0, "system",
|
||||
t0.plus(7, ChronoUnit.DAYS), t0));
|
||||
|
||||
Thread.sleep(10);
|
||||
Instant t1 = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
int affected = repo.touchValidated("default", t1);
|
||||
assertThat(affected).isEqualTo(1);
|
||||
assertThat(repo.findByTenantId("default").orElseThrow().lastValidatedAt())
|
||||
.isAfterOrEqualTo(t1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user