Commit Graph

6 Commits

Author SHA1 Message Date
Zimin A.N. 7b2fe6f2d5 feat(audit): полноценный writer в audit_log + admin UI (audit + outbox DLQ)
До этого audit_log entity была определена, но никто туда не писал — реальный
аудит шёл только через Hibernate Envers (без user/IP/trace). Теперь:

- AuditLogger service пишет в той же транзакции что и CRUD (DictionaryRecordService).
  Захватывает: user (JWT sub либо preferred_username), scope, IP (X-Forwarded-For),
  UA, trace_id (MDC), request_id. Не ломает основной flow при сбое — только
  громко логирует.
- AuditLogRepository: гибкий search() с nullable фильтрами (dictionary, user,
  action, businessKey, from, to) + Pageable.
- AuditAdminController: GET /admin/audit (paginated) + /admin/audit/export.csv
  (cap 10k строк против OOM).

Frontend:
- /audit route: фильтр-панель, таблица с цветными бейджами (CREATE/UPDATE/CLOSE),
  expand-row с JSON before/after diff, footer IP/UA/req_id, кнопка экспорта CSV.
  Pagination 50/100/200.
- /outbox route: stat-cards pending/DLQ + DLQ-таблица с last_error.
- Nav links + i18n (RU + EN).
2026-05-04 15:44:14 +03:00
Zimin A.N. e182b30c58 feat(outbox): attempt cap → DLQ + admin endpoint + 6 unit-тестов
После N (default 1000) неудачных попыток публикации событие маркируется
dlq_at и исключается из polling. Без cap poller бесконечно ретраил мёртвые
сообщения (orphan topic, ACL deny), забивая логи и lag-метрику.

- 0011-outbox-dlq.xml: dlq_at column + перестроенный idx_outbox_unpublished
  (исключает DLQ) + idx_outbox_dlq для admin-листинга.
- OutboxPoller: ordinis.outbox.attempt-max (default 1000), markDlq() при
  достижении cap, новый counter ordinis_outbox_dlq_total.
- OutboxLagGauge: ordinis_outbox_dlq_size gauge — алерт на > 0.
- OutboxEventRepository: countPending() (без DLQ), findByDlqAtIsNotNull(Pageable).
- OutboxAdminController: GET /admin/outbox/{stats,dlq} для UI.
- 6 unit-тестов через mock-maker-subclass (JDK 25 ломает Mockito inline).
2026-05-04 15:43:56 +03:00
Zimin A.N. c96b6706fc feat(rest-api): backend enforcement для x-unique и x-id-source schema extensions
x-id-source (auto-derive businessKey):
- DictionaryRecordService.resolveBusinessKey() читает schemaJson['x-id-source'].
- Если задан — businessKey берётся из data[idSource]. Если клиент явно прислал
  и значения не совпадают → 400 business_key_mismatch (защита от рассинхрона).
- Если поле в data пустое → 400 id_source_missing.
- Если x-id-source не задан и businessKey пуст → 400 business_key_required.

x-unique (constraint на дубли значений):
- DictionaryRecordRepository.findActiveByJsonField() — native SQL c data->>field.
- DictionaryRecordService.enforceUniqueFields() walks schema.properties, для
  каждого property с x-unique=true ищет активные записи с тем же значением.
  Исключает current record на update.
- Конфликт → 409 unique_field_violation с указанием business_key конфликтующей.

Делается на каждом create/update в той же SERIALIZABLE транзакции что и save —
без отдельного DB constraint (для MVP). Partial unique index per-field —
оптимизация на потом, когда будет много справочников с x-unique.
2026-05-04 03:52:51 +03:00
Zimin A.N. be77c30687 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 активен)
2026-05-03 13:04:36 +03:00
Zimin A.N. b778832f49 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, / отвечает
2026-05-03 12:10:36 +03:00
Zimin A.N. a28fd0b352 feat: Maven multimodule scaffolding for Ordinis MDM service
- 10 модулей (domain, validation, events-api, outbox, rest-api, app, read-api, projection-writer, cuod-bundle, migrations)
- Parent pom + BOM с Spring Boot 3.4.2, Spring Cloud 2024.0.0, Vault Config 4.2.0
- ordinis-app: Spring Boot main с Actuator/Prometheus/Logstash JSON encoder/OpenTelemetry/Cloud Config/Vault skeleton
- ordinis-migrations: Liquibase changelogs (dictionary_definitions/records, audit_log, outbox_events, idempotency_keys, EXCLUSION CONSTRAINT через btree_gist)
- Multi-stage Dockerfiles для app и migrations
2026-05-03 00:41:53 +03:00