fix(webhook): scope_filter Hibernate ordinal bug + UI toggle для active

Два бага в /webhooks (user report):

1. **500 при выборе scope в форме**

POST /admin/webhooks/subscriptions с scopeFilter:["PUBLIC"] возвращал 500.

Root cause: WebhookSubscription.scopeFilter был List<DataScope> с
@JdbcTypeCode(SqlTypes.ARRAY). Hibernate сериализовал enum в TEXT[] как
ordinal (е.g. {0} для PUBLIC), а DB CHECK constraint
chk_webhook_scope_filter ожидает строковые имена. Insert падал
DataIntegrityViolationException → 500.

Fix: field теперь List<String> (raw enum names). Getter/setter
конвертируют в/из DataScope — API surface (DTO, service signatures,
WebhookDispatcher) не меняется.

2. **Нет toggle active/inactive в UI**

useUpdateWebhook hook был, но не использовался в UI. Status показывался
только read-only badge'ом. Невозможно деактивировать webhook через UI.

Fix: добавлен toggle button рядом с Status badge на /webhooks/:id.
Sends PUT с inverted active flag (backend treats PUT как full replace,
поэтому пересобираем full payload).

i18n: 'webhooks.action.activate' / 'deactivate' default labels (RU).
This commit is contained in:
Zimin A.N.
2026-05-13 14:02:35 +03:00
parent 4bdc7d2245
commit d4ee30a7dd
2 changed files with 57 additions and 7 deletions
@@ -41,10 +41,23 @@ public class WebhookSubscription {
@Column(name = "url", nullable = false, length = 2048)
private String url;
/** TEXT[] postgres → List<String> via SqlTypes.ARRAY. */
/**
* TEXT[] postgres хранит enum names (PUBLIC/INTERNAL/RESTRICTED).
*
* <p><b>Bug history:</b> раньше field был {@code List<DataScope>} +
* {@code @JdbcTypeCode(SqlTypes.ARRAY)} — Hibernate сериализовал enum
* как ordinal (e.g. {@code {0}} для PUBLIC), а DB CHECK constraint
* ({@code chk_webhook_scope_filter}) требует string values из набора
* {@code 'PUBLIC','INTERNAL','RESTRICTED'}. Insert падал с
* {@code DataIntegrityViolationException} → 500 на POST с scopeFilter.
*
* <p><b>Fix:</b> field хранит {@code List<String>} (raw enum names).
* Getter/setter конвертируют в/из {@link DataScope} — внешний API
* (DTO + service signature + WebhookDispatcher) не меняется.
*/
@JdbcTypeCode(SqlTypes.ARRAY)
@Column(name = "scope_filter", columnDefinition = "TEXT[]")
private List<DataScope> scopeFilter;
private List<String> scopeFilter;
@JdbcTypeCode(SqlTypes.ARRAY)
@Column(name = "dictionary_filter", columnDefinition = "TEXT[]")
@@ -96,7 +109,10 @@ public class WebhookSubscription {
public UUID getId() { return id; }
public String getName() { return name; }
public String getUrl() { return url; }
public List<DataScope> getScopeFilter() { return scopeFilter; }
public List<DataScope> getScopeFilter() {
if (scopeFilter == null) return null;
return scopeFilter.stream().map(DataScope::valueOf).toList();
}
public List<String> getDictionaryFilter() { return dictionaryFilter; }
public List<String> getEventTypeFilter() { return eventTypeFilter; }
public String getHmacSecret() { return hmacSecret; }
@@ -108,7 +124,10 @@ public class WebhookSubscription {
public void setName(String v) { this.name = v; touch(); }
public void setUrl(String v) { this.url = v; touch(); }
public void setScopeFilter(List<DataScope> v) { this.scopeFilter = v; touch(); }
public void setScopeFilter(List<DataScope> v) {
this.scopeFilter = (v == null) ? null : v.stream().map(Enum::name).toList();
touch();
}
public void setDictionaryFilter(List<String> v) { this.dictionaryFilter = v; touch(); }
public void setEventTypeFilter(List<String> v) { this.eventTypeFilter = v; touch(); }
public void setActive(boolean v) { this.active = v; touch(); }