feat(validation,rest-api,outbox): write-side end-to-end working

ordinis-validation:
- ValidationError, ValidationResult, ValidationException
- RecordValidator: networknt JSON Schema (Draft-7) + custom rule для
  x-localized полей. Walk schema, для каждого x-localized:true property
  проверяет: значение это object с locale keys (BCP 47 pattern xx-XX),
  все ключи в supported_locales, default_locale обязательно присутствует,
  значения non-empty strings.

ordinis-outbox:
- KafkaTopicResolver: ordinis.<bundle>.events.<scope>
- OutboxRecorder: запись события в outbox в той же транзакции (MANDATORY
  propagation), с tracer для trace_id/span_id из MDC если OTel доступен
- OutboxPoller: @Scheduled, batch_size + poll_interval из config, метрики
  ordinis_outbox_published_total / publish_errors_total / kafka_publish_duration_seconds
- OutboxLagGauge: ordinis_outbox_pending_events (gauge для алертов)
- ConditionalOnProperty ordinis.outbox.enabled — отключаем в read-api/projection-writer

ordinis-rest-api:
- DTOs: CreateDictionaryRequest, DictionaryResponse, CreateRecordRequest,
  RecordResponse (с WKT serialization geometry)
- DictionaryDefinitionService: CRUD definitions, outbox events DefinitionCreated/Updated
- DictionaryRecordService: bitemporal write-side, SERIALIZABLE isolation,
  bound check FAR_FUTURE valid_to. Update = close current + create new
  (никогда in-place). Outbox event RecordCreated/Updated/Deleted.
- DictionaryDefinitionController: GET list/by-name, POST create, PUT update
- DictionaryRecordController: GET active/history, POST create, PUT update,
  DELETE (close)
- OrdinisException + ApiErrorResponse + GlobalExceptionHandler
  (422 validation, 404 not found, 409 conflict, 500 internal)

ordinis-app:
- @EnableJpaRepositories + @EntityScan для multi-package сканирования
- application.yml profile-based: dev (PG localhost через port-forward,
  ddl-auto=update, OTel disabled), k8s (Cloud Config + Vault Kubernetes auth)
- spring.kafka producer config с SASL SCRAM-SHA-512

ordinis-domain:
- JpaAuditingConfig: добавлен DateTimeProvider возвращающий OffsetDateTime
  (Spring Data Auditing по дефолту его не поддерживает, без него падает
  IllegalArgumentException 'Cannot convert LocalDateTime to OffsetDateTime')
- @ConditionalOnBean(DataSource) убран — race с lifecycle ломал auditing

E2E test (port-forward к cluster PG+Kafka):
- POST dictionary 'ground_station' → 201, JSONB schema сохранена,
  supported_locales = {ru-RU, en-US}, default_locale = ru-RU
- POST record MOSCOW-1 multi-locale name + geometry POINT(37.618 55.751) → 201
- POST record с en-US без ru-RU → 422 с двумя ошибками:
  * networknt 'required property ru-RU not found'
  * custom 'x-localized.missing_default Default locale ru-RU is required'
- outbox_events: 2 row (DefinitionCreated + RecordCreated), topic
  ordinis.cuod.events.public, ключи правильные
- revinfo + dictionary_records_aud: 1 row (Envers tx-time history активен)
This commit is contained in:
Zimin A.N.
2026-05-03 13:04:36 +03:00
parent b778832f49
commit be77c30687
24 changed files with 1246 additions and 19 deletions
@@ -2,9 +2,13 @@ package cloud.nstart.terravault.ordinis.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(scanBasePackages = "cloud.nstart.terravault.ordinis")
@EnableJpaRepositories(basePackages = "cloud.nstart.terravault.ordinis")
@EntityScan(basePackages = "cloud.nstart.terravault.ordinis")
@EnableScheduling
public class OrdinisApplication {
+48 -9
View File
@@ -12,7 +12,7 @@ management:
endpoints:
web:
exposure:
include: health,info,prometheus,refresh,metrics,env
include: health,info,prometheus,refresh,metrics,env,mappings
base-path: /actuator
endpoint:
health:
@@ -23,7 +23,7 @@ management:
liveness:
include: livenessState
readiness:
include: readinessState
include: readinessState,db
prometheus:
metrics:
export:
@@ -33,7 +33,6 @@ management:
application: ${spring.application.name}
environment: ${ENVIRONMENT:dev}
# Defaults для OTel (можно отключить через otel.sdk.disabled=true в dev)
otel:
exporter:
otlp:
@@ -53,8 +52,16 @@ logging:
root: INFO
cloud.nstart.terravault.ordinis: DEBUG
ordinis:
outbox:
enabled: ${ORDINIS_OUTBOX_ENABLED:true}
poll-interval-ms: ${ORDINIS_OUTBOX_POLL_INTERVAL_MS:1000}
batch-size: ${ORDINIS_OUTBOX_BATCH_SIZE:100}
---
# DEV profile — local run без Cloud Config / Vault / OTel.
# DEV profile: подключается к cluster PG+Kafka через kubectl port-forward.
# Перед запуском: kubectl port-forward -n ordinis-dev svc/ordinis-postgres-rw 5432:5432 &
# kubectl port-forward -n ordinis-dev svc/ordinis-kafka-kafka-bootstrap 9092:9092 &
spring:
config:
activate:
@@ -66,17 +73,45 @@ spring:
enabled: false
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
- org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
- org.springframework.cloud.vault.config.VaultAutoConfiguration
- org.springframework.cloud.vault.config.VaultObservationAutoConfiguration
- org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration
datasource:
url: jdbc:postgresql://${ORDINIS_PG_HOST:localhost}:${ORDINIS_PG_PORT:5432}/${ORDINIS_PG_DB:ordinis}
username: ${ORDINIS_PG_USER:ordinis_app}
password: ${ORDINIS_PG_PASSWORD:}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate.format_sql: true
hibernate.jdbc.lob.non_contextual_creation: true
open-in-view: false
kafka:
bootstrap-servers: ${ORDINIS_KAFKA_BOOTSTRAP:localhost:9092}
properties:
security.protocol: ${ORDINIS_KAFKA_SECURITY:SASL_PLAINTEXT}
sasl.mechanism: ${ORDINIS_KAFKA_SASL_MECHANISM:SCRAM-SHA-512}
sasl.jaas.config: >-
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${ORDINIS_KAFKA_USER:ordinis-app}"
password="${ORDINIS_KAFKA_PASSWORD:}";
producer:
acks: all
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
properties:
enable.idempotence: true
compression.type: lz4
otel:
sdk:
disabled: true
---
# K8s profile реальная инфра: Spring Cloud Config + Vault Kubernetes auth.
# K8s profile: реальная инфра в кластере. Cloud Config + Vault Kubernetes auth.
spring:
config:
activate:
@@ -98,3 +133,7 @@ spring:
enabled: true
backend: secret
application-name: ordinis/cuod
jpa:
hibernate:
ddl-auto: validate
open-in-view: false