feat(domain,events): bitemporal entities + i18n-aware definitions + sealed event API

ordinis-domain:
- DataScope enum (PUBLIC/INTERNAL/RESTRICTED) с Kafka topic suffix + visibility logic
- DictionaryDefinition entity: scope, schema_json (JsonNode JSONB), schema_version,
  bundle, supported_locales (text[]), default_locale + Spring Data Auditing
- DictionaryRecord entity: bitemporal (Envers tx-time + valid_from/valid_to),
  data JSONB, geometry (JTS via hibernate-spatial), data_scope. EXCLUSION
  CONSTRAINT enforced в БД (Liquibase 0003).
- AuditLog entity для compliance (отдельно от Loki)
- OutboxEvent entity: aggregate_*, kafka_topic/kafka_key, attempt_count,
  markPublished/markFailed, partial index idx_outbox_unpublished
- IdempotencyKey entity для exactly-once семантики write API
- Repositories: findActiveAt, findActiveByDictionaryAndScopes, findUnpublished
- JpaAuditingConfig: ConditionalOnBean(DataSource) — dev profile без БД
  не активирует auditing, metamodel не строится

ordinis-events-api:
- Mirror DataScope enum (events не depend on domain)
- EventEnvelope<T> record: eventId, occurredAt, schemaVersion (semver),
  bundle, dictionaryName, scope, traceId, spanId, payload
- Sealed DictionaryRecordEvent + records Created/Updated/Deleted
- Sealed DictionaryDefinitionEvent + records Created/Updated
- Jackson polymorphism через @JsonTypeInfo discriminator
- Multi-locale payload: события возят ВСЕ локали (consumer сам сплющивает)

i18n design:
- В JSONB поле кодируется как { 'ru-RU': '...', 'en-US': '...' }
- JSON Schema справочника помечает локализуемые поля extension x-localized:true
- supported_locales + default_locale в dictionary_definitions
- Read API будет negotiate через Accept-Language (RFC 7231) и сплющивать

Validation:
- mvn install all 11 modules: BUILD SUCCESS
- ordinis-app boot dev profile: /actuator/health UP, / отвечает
This commit is contained in:
Zimin A.N.
2026-05-03 12:10:36 +03:00
parent d1c8d5755e
commit b778832f49
26 changed files with 1000 additions and 0 deletions
+4
View File
@@ -19,5 +19,9 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,13 @@
package cloud.nstart.terravault.ordinis.events;
/**
* Mirror domain {@code DataScope} но без зависимости на JPA. События — публичный
* API, consumers не должны тянуть domain entities ради enum.
*
* <p>Значения должны соответствовать {@code domain.DataScope}.
*/
public enum DataScope {
PUBLIC,
INTERNAL,
RESTRICTED
}
@@ -0,0 +1,46 @@
package cloud.nstart.terravault.ordinis.events;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.OffsetDateTime;
import java.util.UUID;
/**
* Метаданные-обёртка вокруг payload каждого события. Едет в Kafka как value.
*
* <p>Schema versioning: {@code schemaVersion} = semver shared DTO модуля
* (ordinis-events-api). Consumer проверяет совместимость через major version.
*
* @param <T> тип payload (DictionaryRecordEvent, DictionaryDefinitionEvent)
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record EventEnvelope<T>(
UUID eventId,
OffsetDateTime occurredAt,
String schemaVersion,
String bundle,
String dictionaryName,
DataScope dataScope,
String traceId,
String spanId,
String causedBy,
T payload) {
public static <T> EventEnvelope<T> of(
String dictionaryName,
DataScope scope,
String bundle,
T payload) {
return new EventEnvelope<>(
UUID.randomUUID(),
OffsetDateTime.now(),
EventSchemaVersion.CURRENT,
bundle,
dictionaryName,
scope,
null,
null,
null,
payload);
}
}
@@ -0,0 +1,12 @@
package cloud.nstart.terravault.ordinis.events;
/**
* Semver версии DTO модуля. Major bump = breaking change. Consumers должны
* проверять major перед deserialization.
*/
public final class EventSchemaVersion {
public static final String CURRENT = "1.0.0";
public static final int MAJOR = 1;
private EventSchemaVersion() {}
}
@@ -0,0 +1,24 @@
package cloud.nstart.terravault.ordinis.events.definition;
import cloud.nstart.terravault.ordinis.events.DataScope;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record DictionaryDefinitionCreated(
UUID definitionId,
String dictionaryName,
String displayName,
String description,
DataScope scope,
JsonNode schemaJson,
String schemaVersion,
String bundle,
List<String> supportedLocales,
String defaultLocale,
OffsetDateTime createdAt,
String createdBy) implements DictionaryDefinitionEvent {}
@@ -0,0 +1,17 @@
package cloud.nstart.terravault.ordinis.events.definition;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DictionaryDefinitionCreated.class, name = "DefinitionCreated"),
@JsonSubTypes.Type(value = DictionaryDefinitionUpdated.class, name = "DefinitionUpdated")
})
public sealed interface DictionaryDefinitionEvent
permits DictionaryDefinitionCreated, DictionaryDefinitionUpdated {
java.util.UUID definitionId();
String dictionaryName();
}
@@ -0,0 +1,29 @@
package cloud.nstart.terravault.ordinis.events.definition;
import cloud.nstart.terravault.ordinis.events.DataScope;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
/**
* Изменение определения справочника (schema, supported_locales и т.д.). Вместе
* с новой версией прилетает {@code previousSchemaJson} для diff.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record DictionaryDefinitionUpdated(
UUID definitionId,
String dictionaryName,
String displayName,
String description,
DataScope scope,
JsonNode schemaJson,
JsonNode previousSchemaJson,
String schemaVersion,
String bundle,
List<String> supportedLocales,
String defaultLocale,
OffsetDateTime updatedAt,
String updatedBy) implements DictionaryDefinitionEvent {}
@@ -0,0 +1,25 @@
package cloud.nstart.terravault.ordinis.events.record;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.OffsetDateTime;
import java.util.UUID;
/**
* Создана новая версия записи справочника. {@code data} — полный multi-locale
* payload (consumer сам сплющивает по нужной локали).
*
* <p>{@code geometryWkt} — WKT представление геометрии (если есть). EWKT с SRID
* 4326 для совместимости с PostGIS. Может быть null.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record DictionaryRecordCreated(
UUID recordId,
String businessKey,
JsonNode data,
String geometryWkt,
OffsetDateTime validFrom,
OffsetDateTime validTo,
OffsetDateTime createdAt,
String createdBy) implements DictionaryRecordEvent {}
@@ -0,0 +1,19 @@
package cloud.nstart.terravault.ordinis.events.record;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.OffsetDateTime;
import java.util.UUID;
/**
* Логическое удаление записи (точнее: закрытие текущей версии установкой
* valid_to). Реального DELETE из таблицы не происходит — для compliance/audit.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record DictionaryRecordDeleted(
UUID recordId,
String businessKey,
OffsetDateTime closedAt,
OffsetDateTime deletedAt,
String deletedBy,
String reason) implements DictionaryRecordEvent {}
@@ -0,0 +1,24 @@
package cloud.nstart.terravault.ordinis.events.record;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* Sealed interface для всех событий по записям справочника. Jackson
* polymorphic deserialization через {@code "type"} discriminator.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DictionaryRecordCreated.class, name = "RecordCreated"),
@JsonSubTypes.Type(value = DictionaryRecordUpdated.class, name = "RecordUpdated"),
@JsonSubTypes.Type(value = DictionaryRecordDeleted.class, name = "RecordDeleted")
})
public sealed interface DictionaryRecordEvent
permits DictionaryRecordCreated, DictionaryRecordUpdated, DictionaryRecordDeleted {
/** UUID dictionary_records.id. */
java.util.UUID recordId();
/** Бизнес-ключ записи (стабильный across versions). */
String businessKey();
}
@@ -0,0 +1,26 @@
package cloud.nstart.terravault.ordinis.events.record;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.OffsetDateTime;
import java.util.UUID;
/**
* Обновление существующей записи. Modeling: новая версия (новый recordId, тот
* же businessKey) с новой valid_from. Старая версия получила valid_to=newRecord.validFrom.
*
* <p>{@code previousData} даёт consumer'у возможность сделать diff.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record DictionaryRecordUpdated(
UUID recordId,
UUID previousRecordId,
String businessKey,
JsonNode data,
JsonNode previousData,
String geometryWkt,
OffsetDateTime validFrom,
OffsetDateTime validTo,
OffsetDateTime updatedAt,
String updatedBy) implements DictionaryRecordEvent {}