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.
This commit is contained in:
+18
@@ -48,4 +48,22 @@ public interface DictionaryRecordRepository extends JpaRepository<DictionaryReco
|
||||
@Param("dictionaryId") UUID dictionaryId,
|
||||
@Param("allowedScopes") List<DataScope> allowedScopes,
|
||||
@Param("at") OffsetDateTime at);
|
||||
|
||||
/**
|
||||
* Поиск активных записей в момент {@code at} по значению поля внутри JSONB data.
|
||||
* Используется для x-unique валидации: ищем записи с тем же значением, исключая
|
||||
* текущую версию (если update).
|
||||
*/
|
||||
@Query(value = """
|
||||
SELECT * FROM dictionary_records r
|
||||
WHERE r.dictionary_id = :dictionaryId
|
||||
AND r.valid_from <= :at
|
||||
AND r.valid_to > :at
|
||||
AND r.data ->> :fieldName = :fieldValue
|
||||
""", nativeQuery = true)
|
||||
List<DictionaryRecord> findActiveByJsonField(
|
||||
@Param("dictionaryId") UUID dictionaryId,
|
||||
@Param("at") OffsetDateTime at,
|
||||
@Param("fieldName") String fieldName,
|
||||
@Param("fieldValue") String fieldValue);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user