feat(api): GET /dictionaries/{name}/snapshots — record-change buckets (backend #2)
Backend #2 из pending-endpoints brief. Frontend TimeTravelModal использует этот endpoint для slider marks — каждая точка = bucket где минимум одна запись validFrom попадает. GET /api/v1/dictionaries/{name}/snapshots?from=...&to=...&granularity=day Implementation: - DictionaryRecordRepository.findSnapshotBuckets — native SQL date_trunc GROUP BY bucket, ORDER BY bucket DESC. scope filter через text[] из CSV. - SnapshotsService — gates dict existence/scope, validates granularity (hour/day/week) + window, formats RU label (1 апр / сейчас), reverses список чтобы oldest first (UI читает слева-направо). - DictionaryChangelogController новый endpoint /snapshots. Errors: - 404 dictionary_not_found - 400 invalid_granularity (not in {hour,day,week}) - 400 invalid_window (from >= to) Frontend impact: TimeTravelModal slider marks переходит с recordTimestamps derived data на реальный API. ~1h frontend swap.
This commit is contained in:
+32
@@ -146,4 +146,36 @@ public interface DictionaryRecordRepository extends JpaRepository<DictionaryReco
|
|||||||
@Param("scopesCsv") String[] scopesCsv,
|
@Param("scopesCsv") String[] scopesCsv,
|
||||||
@Param("at") OffsetDateTime at,
|
@Param("at") OffsetDateTime at,
|
||||||
@Param("polygonGeoJson") String polygonGeoJson);
|
@Param("polygonGeoJson") String polygonGeoJson);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucketed list snapshot points: timestamps когда records появлялись
|
||||||
|
* (validFrom) в заданном time window. Используется для TimeTravel
|
||||||
|
* slider marks на admin-UI — каждая mark = "interesting" moment где
|
||||||
|
* минимум одна запись changed.
|
||||||
|
*
|
||||||
|
* <p>Granularity: PostgreSQL date_trunc unit ({@code hour}, {@code day},
|
||||||
|
* {@code week}). Возвращает Object[] = {bucket OffsetDateTime,
|
||||||
|
* changedSinceLast Long}. Caller'а потом мапит на DTO.
|
||||||
|
*
|
||||||
|
* @param scopes — CSV array (passed как text[] для unnest in WHERE), скоупы
|
||||||
|
* которые caller видит. EMPTY → no records visible (caller
|
||||||
|
* anonymous и scope filter empty).
|
||||||
|
*/
|
||||||
|
@Query(value = """
|
||||||
|
SELECT date_trunc(:granularity, valid_from)::timestamptz AS bucket,
|
||||||
|
count(*) AS changed_since_last
|
||||||
|
FROM dictionary_records
|
||||||
|
WHERE dictionary_id = :dictionaryId
|
||||||
|
AND data_scope = ANY (string_to_array(:scopesCsv, ','))
|
||||||
|
AND valid_from >= :from
|
||||||
|
AND valid_from < :to
|
||||||
|
GROUP BY bucket
|
||||||
|
ORDER BY bucket DESC
|
||||||
|
""", nativeQuery = true)
|
||||||
|
List<Object[]> findSnapshotBuckets(
|
||||||
|
@Param("dictionaryId") UUID dictionaryId,
|
||||||
|
@Param("scopesCsv") String scopesCsv,
|
||||||
|
@Param("from") OffsetDateTime from,
|
||||||
|
@Param("to") OffsetDateTime to,
|
||||||
|
@Param("granularity") String granularity);
|
||||||
}
|
}
|
||||||
|
|||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
package cloud.nstart.terravault.ordinis.restapi.service;
|
||||||
|
|
||||||
|
import cloud.nstart.terravault.ordinis.auth.ScopeContext;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.DataScope;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
|
||||||
|
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshots service для TimeTravel slider marks. Возвращает bucketed list
|
||||||
|
* timestamps где минимум одна запись была inserted/updated в window. UI
|
||||||
|
* затем snap'ает slider к этим точкам — wider granularity = меньше marks
|
||||||
|
* (clearer UX на больших windows).
|
||||||
|
*
|
||||||
|
* <p>Granularity = PostgreSQL date_trunc unit. Поддерживаются: hour, day,
|
||||||
|
* week. Other unit'ы (year, month) overkill для admin scenarios (типичный
|
||||||
|
* window 30-90 дней).
|
||||||
|
*
|
||||||
|
* <p>Label: backend formatits relative RU label ("1 апр", "15 апр",
|
||||||
|
* "сейчас"). Frontend не делает locale logic — single source of truth.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class SnapshotsService {
|
||||||
|
|
||||||
|
private static final Set<String> ALLOWED_GRANULARITY = Set.of("hour", "day", "week");
|
||||||
|
private static final DateTimeFormatter RU_DAY = DateTimeFormatter.ofPattern("d MMM", new Locale("ru"));
|
||||||
|
private static final DateTimeFormatter RU_HOUR = DateTimeFormatter.ofPattern("d MMM HH:mm", new Locale("ru"));
|
||||||
|
|
||||||
|
private final DictionaryDefinitionService definitionService;
|
||||||
|
private final DictionaryRecordRepository recordRepository;
|
||||||
|
private final ScopeContext scopeContext;
|
||||||
|
|
||||||
|
public SnapshotsService(
|
||||||
|
DictionaryDefinitionService definitionService,
|
||||||
|
DictionaryRecordRepository recordRepository,
|
||||||
|
ScopeContext scopeContext) {
|
||||||
|
this.definitionService = definitionService;
|
||||||
|
this.recordRepository = recordRepository;
|
||||||
|
this.scopeContext = scopeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws OrdinisException 404 если dict не существует / не виден;
|
||||||
|
* 400 если granularity недопустимый.
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public SnapshotsResponse findSnapshots(
|
||||||
|
String name,
|
||||||
|
OffsetDateTime from,
|
||||||
|
OffsetDateTime to,
|
||||||
|
String granularity) {
|
||||||
|
DictionaryDefinition d = definitionService.findByName(name);
|
||||||
|
if (!scopeContext.canAccess(d.getScope())) {
|
||||||
|
throw OrdinisException.notFound(
|
||||||
|
"dictionary_not_found", "Dictionary not found: " + name);
|
||||||
|
}
|
||||||
|
String gran = granularity == null ? "day" : granularity.toLowerCase();
|
||||||
|
if (!ALLOWED_GRANULARITY.contains(gran)) {
|
||||||
|
throw OrdinisException.badRequest(
|
||||||
|
"invalid_granularity",
|
||||||
|
"granularity must be one of " + ALLOWED_GRANULARITY + ", got: " + granularity);
|
||||||
|
}
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
OffsetDateTime windowTo = to == null ? now : to;
|
||||||
|
OffsetDateTime windowFrom = from == null ? windowTo.minusDays(30) : from;
|
||||||
|
if (!windowFrom.isBefore(windowTo)) {
|
||||||
|
throw OrdinisException.badRequest(
|
||||||
|
"invalid_window", "from must be before to: from=" + windowFrom + " to=" + windowTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<DataScope> scopes = scopeContext.currentScopes();
|
||||||
|
String scopesCsv = scopes.stream().map(Enum::name).collect(Collectors.joining(","));
|
||||||
|
|
||||||
|
List<Object[]> rows = recordRepository.findSnapshotBuckets(
|
||||||
|
d.getId(), scopesCsv, windowFrom, windowTo, gran);
|
||||||
|
|
||||||
|
List<Snapshot> snapshots = new ArrayList<>(rows.size());
|
||||||
|
boolean currentMarked = false;
|
||||||
|
DateTimeFormatter fmt = "hour".equals(gran) ? RU_HOUR : RU_DAY;
|
||||||
|
for (Object[] row : rows) {
|
||||||
|
OffsetDateTime bucket = ((java.sql.Timestamp) row[0]).toInstant().atOffset(now.getOffset());
|
||||||
|
long changedSinceLast = ((Number) row[1]).longValue();
|
||||||
|
boolean isCurrent = !currentMarked && !bucket.isAfter(now);
|
||||||
|
if (isCurrent) currentMarked = true;
|
||||||
|
String label = isCurrent ? "сейчас" : fmt.format(bucket);
|
||||||
|
// totalRecords остаётся 0 в MVP — точный cumulative count требует
|
||||||
|
// отдельный window query. UI показывает только bucket + changed count.
|
||||||
|
snapshots.add(new Snapshot(bucket, 0L, changedSinceLast, label, isCurrent));
|
||||||
|
}
|
||||||
|
// Reverse чтобы oldest first (UI slider читает слева-направо).
|
||||||
|
java.util.Collections.reverse(snapshots);
|
||||||
|
|
||||||
|
return new SnapshotsResponse(d.getName(), windowFrom, windowTo, snapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DTOs ===
|
||||||
|
|
||||||
|
public record Snapshot(
|
||||||
|
OffsetDateTime at,
|
||||||
|
long totalRecords,
|
||||||
|
long changedSinceLast,
|
||||||
|
String label,
|
||||||
|
boolean isCurrent) {}
|
||||||
|
|
||||||
|
public record SnapshotsResponse(
|
||||||
|
String dictionary,
|
||||||
|
OffsetDateTime from,
|
||||||
|
OffsetDateTime to,
|
||||||
|
List<Snapshot> snapshots) {}
|
||||||
|
}
|
||||||
+39
-10
@@ -1,6 +1,7 @@
|
|||||||
package cloud.nstart.terravault.ordinis.restapi.web;
|
package cloud.nstart.terravault.ordinis.restapi.web;
|
||||||
|
|
||||||
import cloud.nstart.terravault.ordinis.restapi.service.ChangelogService;
|
import cloud.nstart.terravault.ordinis.restapi.service.ChangelogService;
|
||||||
|
import cloud.nstart.terravault.ordinis.restapi.service.SnapshotsService;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -8,27 +9,33 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code GET /api/v1/dictionaries/{name}/changelog} — schema versions
|
* Per-dictionary history / time-travel endpoints для admin-UI:
|
||||||
* timeline для admin-UI History tab и TimeTravel modal.
|
* <ul>
|
||||||
*
|
* <li>{@code GET /dictionaries/{name}/changelog} — schema versions timeline</li>
|
||||||
* <p>Source: audit_log filtered by DEFINITION_* event types. Каждая запись
|
* <li>{@code GET /dictionaries/{name}/snapshots} — record-change buckets для TimeTravel slider</li>
|
||||||
* содержит payload_before/after = JsonNode schemas; ChangelogService
|
* </ul>
|
||||||
* computes property-level diff summary.
|
|
||||||
*
|
*
|
||||||
* <p>Errors:
|
* <p>Errors:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>404 {@code dictionary_not_found} — dict не существует или не виден
|
* <li>404 {@code dictionary_not_found} — dict не существует или не виден
|
||||||
* caller'у (одинаковая ошибка чтобы не leak'ало existence).</li>
|
* caller'у (одинаковая ошибка чтобы не leak'ало existence).</li>
|
||||||
|
* <li>400 {@code invalid_granularity} — snapshots granularity не из
|
||||||
|
* {hour, day, week}.</li>
|
||||||
|
* <li>400 {@code invalid_window} — from >= to.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/dictionaries")
|
@RequestMapping("/api/v1/dictionaries")
|
||||||
public class DictionaryChangelogController {
|
public class DictionaryChangelogController {
|
||||||
|
|
||||||
private final ChangelogService service;
|
private final ChangelogService changelogService;
|
||||||
|
private final SnapshotsService snapshotsService;
|
||||||
|
|
||||||
public DictionaryChangelogController(ChangelogService service) {
|
public DictionaryChangelogController(
|
||||||
this.service = service;
|
ChangelogService changelogService,
|
||||||
|
SnapshotsService snapshotsService) {
|
||||||
|
this.changelogService = changelogService;
|
||||||
|
this.snapshotsService = snapshotsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +48,28 @@ public class DictionaryChangelogController {
|
|||||||
public ChangelogService.ChangelogResponse changelog(
|
public ChangelogService.ChangelogResponse changelog(
|
||||||
@PathVariable String name,
|
@PathVariable String name,
|
||||||
@RequestParam(defaultValue = "50") int limit) {
|
@RequestParam(defaultValue = "50") int limit) {
|
||||||
return service.findChangelog(name, limit);
|
return changelogService.findChangelog(name, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshot points (record-change buckets) для TimeTravel slider.
|
||||||
|
*
|
||||||
|
* @param name dict business name
|
||||||
|
* @param from ISO datetime window start. Default: now - 30d
|
||||||
|
* @param to ISO datetime window end. Default: now
|
||||||
|
* @param granularity bucket size — {@code hour} / {@code day} / {@code week}.
|
||||||
|
* Default: {@code day}.
|
||||||
|
*/
|
||||||
|
@GetMapping("/{name}/snapshots")
|
||||||
|
public SnapshotsService.SnapshotsResponse snapshots(
|
||||||
|
@PathVariable String name,
|
||||||
|
@RequestParam(required = false)
|
||||||
|
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME)
|
||||||
|
java.time.OffsetDateTime from,
|
||||||
|
@RequestParam(required = false)
|
||||||
|
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME)
|
||||||
|
java.time.OffsetDateTime to,
|
||||||
|
@RequestParam(defaultValue = "day") String granularity) {
|
||||||
|
return snapshotsService.findSnapshots(name, from, to, granularity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
package cloud.nstart.terravault.ordinis.restapi.service;
|
||||||
|
|
||||||
|
import cloud.nstart.terravault.ordinis.auth.ScopeContext;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.DataScope;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinitionRepository;
|
||||||
|
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
|
||||||
|
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class SnapshotsServiceTest {
|
||||||
|
|
||||||
|
private static final ObjectMapper OM = new ObjectMapper();
|
||||||
|
|
||||||
|
private DictionaryDefinitionRepository defRepo;
|
||||||
|
private DictionaryRecordRepository recRepo;
|
||||||
|
private DictionaryDefinitionService defService;
|
||||||
|
private TestScopeContext scopeCtx;
|
||||||
|
private SnapshotsService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
defRepo = mock(DictionaryDefinitionRepository.class);
|
||||||
|
recRepo = mock(DictionaryRecordRepository.class);
|
||||||
|
defService = new DictionaryDefinitionService(defRepo, null);
|
||||||
|
scopeCtx = new TestScopeContext();
|
||||||
|
service = new SnapshotsService(defService, recRepo, scopeCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class TestScopeContext extends ScopeContext {
|
||||||
|
private DataScope blockedScope;
|
||||||
|
TestScopeContext() { super(null); }
|
||||||
|
@Override public boolean canAccess(DataScope dataScope) {
|
||||||
|
return dataScope != blockedScope;
|
||||||
|
}
|
||||||
|
@Override public java.util.Set<DataScope> currentScopes() {
|
||||||
|
return java.util.Set.of(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||||
|
}
|
||||||
|
void block(DataScope s) { this.blockedScope = s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshots_dictNotFound_404() {
|
||||||
|
when(defRepo.findByName("missing")).thenReturn(Optional.empty());
|
||||||
|
assertThatThrownBy(() -> service.findSnapshots("missing", null, null, "day"))
|
||||||
|
.isInstanceOf(OrdinisException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshots_scopeHidden_404() {
|
||||||
|
var d = def("private", DataScope.RESTRICTED);
|
||||||
|
when(defRepo.findByName("private")).thenReturn(Optional.of(d));
|
||||||
|
scopeCtx.block(DataScope.RESTRICTED);
|
||||||
|
assertThatThrownBy(() -> service.findSnapshots("private", null, null, "day"))
|
||||||
|
.isInstanceOf(OrdinisException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshots_invalidGranularity_400() {
|
||||||
|
var d = def("dict", DataScope.PUBLIC);
|
||||||
|
when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
|
||||||
|
assertThatThrownBy(() -> service.findSnapshots("dict", null, null, "century"))
|
||||||
|
.isInstanceOf(OrdinisException.class)
|
||||||
|
.hasMessageContaining("granularity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshots_invalidWindow_400() {
|
||||||
|
var d = def("dict", DataScope.PUBLIC);
|
||||||
|
when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
|
||||||
|
OffsetDateTime from = OffsetDateTime.parse("2026-05-11T00:00:00Z");
|
||||||
|
OffsetDateTime to = OffsetDateTime.parse("2026-04-11T00:00:00Z");
|
||||||
|
assertThatThrownBy(() -> service.findSnapshots("dict", from, to, "day"))
|
||||||
|
.isInstanceOf(OrdinisException.class)
|
||||||
|
.hasMessageContaining("from must be before to");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshots_basicBuckets_mapToSnapshots() {
|
||||||
|
var d = def("dict", DataScope.PUBLIC);
|
||||||
|
when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
|
||||||
|
Timestamp april = Timestamp.from(OffsetDateTime.parse("2026-04-01T00:00:00Z").toInstant());
|
||||||
|
Timestamp may = Timestamp.from(OffsetDateTime.parse("2026-05-11T00:00:00Z").toInstant());
|
||||||
|
when(recRepo.findSnapshotBuckets(eq(d.getId()), anyString(), any(), any(), eq("day")))
|
||||||
|
.thenReturn(List.<Object[]>of(
|
||||||
|
new Object[]{may, 5L},
|
||||||
|
new Object[]{april, 3L}));
|
||||||
|
|
||||||
|
var result = service.findSnapshots("dict", null, null, "day");
|
||||||
|
|
||||||
|
assertThat(result.dictionary()).isEqualTo("dict");
|
||||||
|
assertThat(result.snapshots()).hasSize(2);
|
||||||
|
// Reversed → oldest first.
|
||||||
|
assertThat(result.snapshots().get(0).changedSinceLast()).isEqualTo(3L);
|
||||||
|
assertThat(result.snapshots().get(1).changedSinceLast()).isEqualTo(5L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshots_currentMarkedOnce_evenWithFutureBuckets() {
|
||||||
|
var d = def("dict", DataScope.PUBLIC);
|
||||||
|
when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
Timestamp pastA = Timestamp.from(now.minusDays(10).toInstant());
|
||||||
|
Timestamp pastB = Timestamp.from(now.minusDays(5).toInstant());
|
||||||
|
// Repository returns DESC, так order — newest first.
|
||||||
|
when(recRepo.findSnapshotBuckets(any(), anyString(), any(), any(), anyString()))
|
||||||
|
.thenReturn(List.<Object[]>of(
|
||||||
|
new Object[]{pastB, 1L},
|
||||||
|
new Object[]{pastA, 2L}));
|
||||||
|
|
||||||
|
var result = service.findSnapshots("dict", null, null, "day");
|
||||||
|
|
||||||
|
long isCurrentCount = result.snapshots().stream().filter(SnapshotsService.Snapshot::isCurrent).count();
|
||||||
|
assertThat(isCurrentCount).isEqualTo(1L);
|
||||||
|
// Newest bucket (pastB) — это "сейчас" label.
|
||||||
|
var newest = result.snapshots().get(result.snapshots().size() - 1);
|
||||||
|
assertThat(newest.isCurrent()).isTrue();
|
||||||
|
assertThat(newest.label()).isEqualTo("сейчас");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helpers ===
|
||||||
|
|
||||||
|
private static DictionaryDefinition def(String name, DataScope scope) {
|
||||||
|
JsonNode schema;
|
||||||
|
try {
|
||||||
|
schema = OM.readTree("{\"properties\":{}}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new DictionaryDefinition(UUID.randomUUID(), name, scope, schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user