feat(schema-workflow): PATCH /drafts/{id} — edit без recreate (A)
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
package cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* Partial update черновика без recreate. Полезно maker'у после
|
||||
* {@code CHANGES_REQUESTED} — вместо withdraw + create нового draft'а
|
||||
* (с потерей id, history, reviewer assignment'ов) — обновляем content
|
||||
* существующего.
|
||||
*
|
||||
* <p>Endpoint: {@code PATCH /api/v1/dictionaries/{name}/drafts/{draftId}}.
|
||||
*
|
||||
* <p>Status-gate (enforced backend'ом):
|
||||
* <ul>
|
||||
* <li>{@code DRAFT} — OK.</li>
|
||||
* <li>{@code CHANGES_REQUESTED} — OK; status переводится обратно в
|
||||
* {@code DRAFT} автоматически (maker должен ре-submit).</li>
|
||||
* <li>{@code REVIEW_PENDING} / {@code APPROVED} — 409 invalid_transition.
|
||||
* Pending review не должен ловить moving target; approved content
|
||||
* freezing до publish.</li>
|
||||
* <li>Terminal — 409.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Все поля optional. Если null — соответствующее поле draft'а не
|
||||
* меняется. Это позволяет, например, обновить только reason без замены
|
||||
* proposedSchema.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record UpdateSchemaDraftRequest(
|
||||
/** Replacement JSON Schema (full document). null = не меняем. */
|
||||
JsonNode proposedSchema,
|
||||
/** Новая maker rationale. null = не меняем. */
|
||||
String reason) {}
|
||||
+2
@@ -55,6 +55,7 @@ public class ChangelogService {
|
||||
"DEFINITION_CREATED",
|
||||
"DEFINITION_UPDATED",
|
||||
"SCHEMA_DRAFT_CREATED",
|
||||
"SCHEMA_DRAFT_UPDATED",
|
||||
"SCHEMA_DRAFT_REVIEW_REQUESTED",
|
||||
"SCHEMA_DRAFT_DECIDED",
|
||||
"SCHEMA_DRAFT_PUBLISHED",
|
||||
@@ -201,6 +202,7 @@ public class ChangelogService {
|
||||
case "DEFINITION_CREATED" -> "definition_create";
|
||||
case "DEFINITION_UPDATED" -> "definition_update";
|
||||
case "SCHEMA_DRAFT_CREATED" -> "draft_create";
|
||||
case "SCHEMA_DRAFT_UPDATED" -> "draft_update";
|
||||
case "SCHEMA_DRAFT_REVIEW_REQUESTED" -> "draft_review";
|
||||
case "SCHEMA_DRAFT_DECIDED" -> switch (action == null ? "" : action) {
|
||||
case "DRAFT_APPROVE" -> "draft_approve";
|
||||
|
||||
+60
@@ -11,6 +11,7 @@ import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.CreateSchemaDr
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.DecisionRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.PublishDraftRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.SubmitForReviewRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.UpdateSchemaDraftRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
|
||||
import cloud.nstart.terravault.ordinis.restapi.service.DictionaryDefinitionService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -348,6 +349,65 @@ public class SchemaDraftService {
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maker правит proposedSchema или reason в существующем draft'е без
|
||||
* recreate. Полезно после CHANGES_REQUESTED: вместо withdraw + create
|
||||
* (с потерей draftId, history события, reviewer assignment) — просто
|
||||
* обновляем content.
|
||||
*
|
||||
* <p>Status-gate: только {@code DRAFT} и {@code CHANGES_REQUESTED}.
|
||||
* Из {@code REVIEW_PENDING} update запрещён — pending review должен
|
||||
* сначала разрешиться decision'ом (reviewer не должен ловить moving
|
||||
* target). Из {@code APPROVED} тоже запрещён — после approve content
|
||||
* заморожен до publish (иначе одобрили одну схему, опубликовали другую).
|
||||
*
|
||||
* <p>Side effect: если status был {@code CHANGES_REQUESTED}, переводим
|
||||
* обратно в {@code DRAFT} чтобы maker должен был осознанно submit'нуть
|
||||
* на ревью заново. Это match'ит логичную ментальную модель «правки →
|
||||
* новый draft state → re-submit».
|
||||
*
|
||||
* <p>Audit + outbox event {@code SchemaDraftUpdated} даёт changelog
|
||||
* timeline видимость "maker правил черновик после review feedback".
|
||||
*/
|
||||
@Transactional
|
||||
public DictionarySchemaDraft updateContent(UUID draftId, UpdateSchemaDraftRequest req) {
|
||||
DictionarySchemaDraft draft = findById(draftId);
|
||||
assertMakerActor(draft, "update_content");
|
||||
|
||||
SchemaDraftStatus current = draft.getStatus();
|
||||
if (current != SchemaDraftStatus.DRAFT && current != SchemaDraftStatus.CHANGES_REQUESTED) {
|
||||
throw OrdinisException.conflict(
|
||||
"invalid_transition",
|
||||
"Draft " + draftId + " status=" + current.dbValue()
|
||||
+ " — update возможен только из draft или changes_requested.");
|
||||
}
|
||||
|
||||
JsonNode beforeSchema = draft.getProposedSchema();
|
||||
if (req.proposedSchema() != null) {
|
||||
draft.setProposedSchema(req.proposedSchema());
|
||||
}
|
||||
if (req.reason() != null) {
|
||||
draft.setReason(req.reason());
|
||||
}
|
||||
// CHANGES_REQUESTED → DRAFT: maker должен заново submit'нуть после правок.
|
||||
if (current == SchemaDraftStatus.CHANGES_REQUESTED) {
|
||||
draft.setStatus(SchemaDraftStatus.DRAFT);
|
||||
}
|
||||
|
||||
DictionarySchemaDraft saved = repository.save(draft);
|
||||
DictionaryDefinition def = definitionService.findById(saved.getDictionaryId());
|
||||
emitEvent("SchemaDraftUpdated", saved, def, Map.of(
|
||||
"previousStatus", current.dbValue()));
|
||||
if (auditLogger != null) {
|
||||
auditLogger.schemaDraftEvent(
|
||||
"SCHEMA_DRAFT_UPDATED", "DRAFT_UPDATE", def,
|
||||
beforeSchema, saved.getProposedSchema());
|
||||
}
|
||||
log.info("schema_draft_updated id={} dict={} maker={} prevStatus={}",
|
||||
saved.getId(), def.getName(), saved.getMakerId(), current.dbValue());
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** Maker отзывает свой draft (только non-terminal статусы). */
|
||||
@Transactional
|
||||
public DictionarySchemaDraft withdraw(UUID draftId) {
|
||||
|
||||
+18
@@ -7,6 +7,7 @@ import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.DecisionReques
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.PublishDraftRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.SchemaDraftResponse;
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.SubmitForReviewRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.dto.schemaworkflow.UpdateSchemaDraftRequest;
|
||||
import cloud.nstart.terravault.ordinis.restapi.service.DictionaryDefinitionService;
|
||||
import cloud.nstart.terravault.ordinis.restapi.service.schemaworkflow.SchemaDraftService;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -130,6 +131,23 @@ public class SchemaDraftController {
|
||||
"publishedAt", draft.getPublishedAt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial update content draft'а без recreate. Status-gate:
|
||||
* только DRAFT и CHANGES_REQUESTED. CHANGES_REQUESTED → DRAFT
|
||||
* автоматически после update (maker должен ре-submit).
|
||||
*
|
||||
* <p>409 {@code invalid_transition} — review_pending / approved /
|
||||
* terminal. 403 {@code maker_only} — actor != maker.
|
||||
*/
|
||||
@org.springframework.web.bind.annotation.PatchMapping("/api/v1/dictionaries/{name}/drafts/{draftId}")
|
||||
public SchemaDraftResponse update(
|
||||
@PathVariable String name,
|
||||
@PathVariable UUID draftId,
|
||||
@Valid @RequestBody UpdateSchemaDraftRequest req) {
|
||||
DictionarySchemaDraft draft = draftService.updateContent(draftId, req);
|
||||
return SchemaDraftResponse.from(draft, name);
|
||||
}
|
||||
|
||||
/** Withdraw — maker отменяет non-terminal draft. */
|
||||
@DeleteMapping("/api/v1/dictionaries/{name}/drafts/{draftId}")
|
||||
public SchemaDraftResponse withdraw(
|
||||
|
||||
Reference in New Issue
Block a user