Documentation navigationNavigare documentație
Guide 3 of 7Ghidul 3 din 7 · API 0.8.0
Contracts and versioningContracte și versionare
How input contracts, deterministic cases and canonical configuration families move from mutable drafts to immutable, calendar-effective versions.Cum trec contractele de intrare, cazurile deterministe și familiile canonice de configurare de la ciorne mutabile la versiuni imuabile, active calendaristic.
Derived from:Derivat din: contracts/openapi/openapi.yamldocs/ARCHITECTURE.md
The contract is a JSON SchemaContractul este o schemă JSON
A contract is a JSON Schema Draft 2020-12 document that defines the accepted input. It starts as a mutable draft: createContract stores the name, description and schema and returns draft_revision 1. validateContractDraft checks syntax and the supported vocabulary without saving anything. updateContractDraft replaces the draft and requires expected_revision equal to the current draft_revision; a stale value fails with 409, so two engineers cannot silently overwrite each other.Un contract este un document JSON Schema Draft 2020-12 care definește intrarea acceptată. Începe ca ciornă mutabilă: createContract stochează numele, descrierea și schema și întoarce draft_revision 1. validateContractDraft verifică sintaxa și vocabularul acceptat fără să salveze nimic. updateContractDraft înlocuiește ciorna și cere expected_revision egal cu draft_revision curent; o valoare expirată eșuează cu 409, așa că doi ingineri nu se pot suprascrie în tăcere.
Request bodies for the contract family are bounded at 1,085,440 bytes, which is 1 MiB of schema plus the envelope. Reads are open to administrator, engineer, operator and auditor; mutations require administrator or engineer.Body-urile cererilor pentru familia de contracte sunt limitate la 1.085.440 de octeți, adică 1 MiB de schemă plus plicul. Citirile sunt deschise pentru administrator, inginer, operator și auditor; mutațiile cer administrator sau inginer.
contract-versioning/contract-v1.json · Version 1: three required person fields.Versiunea 1: trei câmpuri obligatorii ale persoanei.{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"additionalProperties": false,
"required": ["first_name", "last_name", "email"],
"properties": {
"first_name": { "type": "string", "minLength": 1 },
"last_name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" }
}
}
Sequential immutable versionsVersiuni imuabile secvențiale
createContractVersion freezes the current draft as version number n+1 with a content_hash over the canonical schema, a source_revision and a status of versioned. publishContractVersion moves the version to published; deprecateContractVersion marks it deprecated so new releases stop binding it while existing releases keep working. Published content never changes: a correction is a new draft revision and a new version.createContractVersion îngheață ciorna curentă ca versiune cu number n+1, cu un content_hash peste schema canonică, un source_revision și un status de versioned. publishContractVersion trece versiunea în published; deprecateContractVersion o marchează deprecated, astfel încât release-urile noi nu o mai leagă, iar cele existente continuă să funcționeze. Conținutul publicat nu se schimbă niciodată: o corecție este o revizie nouă a ciornei și o versiune nouă.
Every downstream object records the version it was built from. A case stores contract_version_id and contract_hash, a release stores the same identifiers, and an execution stores the release hash. Comparing hashes is enough to prove which schema validated a document.Fiecare obiect din aval înregistrează versiunea din care a fost construit. Un caz stochează contract_version_id și contract_hash, un release stochează aceiași identificatori, iar o execuție stochează hash-ul release-ului. Compararea hash-urilor este suficientă pentru a demonstra ce schemă a validat un document.
contract-versioning/contract-v2.json · Version 2 adds optional middle_name and phone; every version 1 input still validates.Versiunea 2 adaugă middle_name și phone opționale; orice intrare a versiunii 1 validează în continuare.{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"additionalProperties": false,
"required": ["first_name", "last_name", "email"],
"properties": {
"first_name": { "type": "string", "minLength": 1 },
"middle_name": { "type": "string", "minLength": 1 },
"last_name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"phone": { "type": "string", "pattern": "^\\+[1-9][0-9]{6,14}$" }
}
}
Deterministic casesCazuri deterministe
A case is a stored input for one published contract version. generateCase asks the casegen worker for a valid, boundary or intentionally invalid payload from contract_version_id, kind and an integer seed; the same triple returns an equivalent payload and the same content_hash, and the response records the provider, its version, the locale and the generator version. importFixtureCase validates and idempotently stores a hand-written payload of up to 532,480 bytes, so real edge cases become reproducible fixtures. Cases feed transform previews, template previews and release validation.Un caz este o intrare stocată pentru o versiune publicată de contract. generateCase cere workerului casegen un payload valid, de limită sau intenționat invalid pornind de la contract_version_id, kind și un seed întreg; același triplet întoarce un payload echivalent și același content_hash, iar răspunsul înregistrează furnizorul, versiunea lui, localizarea și versiunea generatorului. importFixtureCase validează și stochează idempotent un payload scris manual de până la 532.480 de octeți, astfel încât cazurile-limită reale devin fixture-uri reproductibile. Cazurile alimentează previzualizările de transformare, previzualizările de șablon și validarea release-urilor.
Canonical families: revision, ETag, idempotencyFamilii canonice: revizie, ETag, idempotență
Ingestion models, canonical transformers and canonical templates follow one stricter protocol. Creating a family requires an Idempotency-Key; the same key with the same body replays the stored result with idempotent_replay: true, and the same key with different content answers 409. Reading a draft returns a strong ETag that is the decimal draft revision; every draft mutation and every version call must send it back unchanged in If-Match. A stale precondition fails with 412 and a missing one with 428.Modelele de ingestie, transformatorii canonici și șabloanele canonice urmează un protocol mai strict. Crearea unei familii cere un Idempotency-Key; aceeași cheie cu același body reia rezultatul stocat cu idempotent_replay: true, iar aceeași cheie cu conținut diferit răspunde 409. Citirea unei ciorne întoarce un ETag ferm, care este revizia zecimală a ciornei; fiecare mutație a ciornei și fiecare apel de versionare trebuie să îl trimită înapoi neschimbat în If-Match. O precondiție expirată eșuează cu 412, iar una lipsă cu 428.
Versioning freezes the exact draft revision. Publishing is atomic: the publish operation writes the immutable version and its activation in one transaction with an effective_from in UTC, and it answers with the version, activation and content hash.Versionarea îngheață exact revizia ciornei. Publicarea este atomică: operația de publicare scrie versiunea imuabilă și activarea ei într-o singură tranzacție, cu effective_from în UTC, și răspunde cu versiunea, activarea și hash-ul de conținut.
Calendar-effective selectionSelecție activă calendaristic
Definition families are selected by time, not by version number. When a submission arrives, Docula resolves each family at the submission's object_effective_at, the business moment the source object became official, and picks the activation with the greatest effective_from less than or equal to that instant. received_at is recorded separately and never selects behavior. The resolve operations let you ask the same question ahead of time with an at parameter.Familiile de definiții sunt selectate după timp, nu după numărul versiunii. Când sosește o solicitare, Docula rezolvă fiecare familie la object_effective_at al solicitării, momentul de business în care obiectul sursă a devenit oficial, și alege activarea cu cel mai mare effective_from mai mic sau egal cu acel moment. received_at este înregistrat separat și nu selectează niciodată comportamentul. Operațiile de rezolvare permit aceeași întrebare în avans, cu un parametru at.
A replacement is always a new future-effective version. A future activation may be cancelled with a reason_code only while no submission has pinned it; its content is never edited. Historical object_effective_at values are accepted when an exact historically applicable release exists, and values more than five minutes in the future are rejected.O înlocuire este întotdeauna o versiune nouă, activă în viitor. O activare viitoare poate fi anulată cu un reason_code doar cât timp nicio solicitare nu a fixat-o; conținutul ei nu este editat niciodată. Valorile istorice pentru object_effective_at sunt acceptate când există un release exact aplicabil istoric, iar valorile cu mai mult de cinci minute în viitor sunt respinse.
contract-versioning/curl.sh · Create, version, publish, revise, publish again and deprecate.Creează, versionează, publică, revizuiește, publică din nou și depreciază.#!/bin/sh
# Docula example: contract versioning.
# login -> create contract -> version 1 -> publish v1 -> validate v2 draft -> update draft -> version 2 -> publish v2 -> deprecate v1.
# Requires curl and jq. Environment: DOCULA_API, DOCULA_ORIGIN (defaults to DOCULA_API), DOCULA_USERNAME, DOCULA_PASSWORD.
set -eu
API=${DOCULA_API:?set DOCULA_API to the API origin, for example http://127.0.0.1:18081}
ORIGIN=${DOCULA_ORIGIN:-$API}
HERE=$(cd "$(dirname "$0")" && pwd)
JAR=$(mktemp)
trap 'rm -f "$JAR" "$JAR.body"' EXIT
CSRF=$(jq -n --arg u "${DOCULA_USERNAME:?}" --arg p "${DOCULA_PASSWORD:?}" '{username: $u, password: $p}' |
curl --fail --silent --show-error -c "$JAR" -H "Origin: $ORIGIN" -H 'Content-Type: application/json' \
--data-binary @- "$API/api/v1/auth/login" | jq -r '.csrf_token')
api() {
if [ "$#" -ge 3 ]; then
curl --fail --silent --show-error -b "$JAR" -X "$1" -H "Origin: $ORIGIN" -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' --data-binary @"$3" "$API$2"
else
curl --fail --silent --show-error -b "$JAR" -X "$1" -H "Origin: $ORIGIN" -H "X-CSRF-Token: $CSRF" "$API$2"
fi
}
# with_revision FILE REVISION: the API rejects stale expected_revision values with 412, so always read the live one.
with_revision() { jq --argjson rev "$2" '.expected_revision = $rev' "$1" > "$JAR.body"; printf '%s' "$JAR.body"; }
# 1. Create the draft (createContract) and freeze + publish version 1.
CONTRACT=$(api POST /api/v1/contracts "$HERE/create-contract.request.json")
CONTRACT_ID=$(printf '%s' "$CONTRACT" | jq -r '.contract.id')
REVISION=$(printf '%s' "$CONTRACT" | jq -r '.contract.draft_revision')
V1=$(api POST "/api/v1/contracts/$CONTRACT_ID/versions" "$(with_revision "$HERE/create-version.request.json" "$REVISION")")
V1_NUMBER=$(printf '%s' "$V1" | jq -r '.version.number')
api POST "/api/v1/contracts/$CONTRACT_ID/versions/$V1_NUMBER/publish" "$HERE/publish.request.json" > /dev/null
# 2. Check the version 2 schema before touching the draft (validateContractDraft is side-effect free).
api POST "/api/v1/contracts/$CONTRACT_ID/validate" "$HERE/validate-draft.request.json" | jq -e '.valid == true' > /dev/null
# 3. Replace the draft (updateContractDraft) with the current draft revision, then freeze + publish version 2.
REVISION=$(api GET "/api/v1/contracts/$CONTRACT_ID" | jq -r '.draft_revision')
UPDATED=$(api PUT "/api/v1/contracts/$CONTRACT_ID" "$(with_revision "$HERE/update-draft.request.json" "$REVISION")")
REVISION=$(printf '%s' "$UPDATED" | jq -r '.contract.draft_revision')
V2=$(api POST "/api/v1/contracts/$CONTRACT_ID/versions" "$(with_revision "$HERE/create-version.request.json" "$REVISION")")
V2_NUMBER=$(printf '%s' "$V2" | jq -r '.version.number')
api POST "/api/v1/contracts/$CONTRACT_ID/versions/$V2_NUMBER/publish" "$HERE/publish.request.json" > /dev/null
# 4. Deprecate version 1 (deprecateContractVersion). Published versions are immutable: deprecation only closes them to new releases.
api POST "/api/v1/contracts/$CONTRACT_ID/versions/$V1_NUMBER/deprecate" "$HERE/deprecate.request.json" | jq -e '.version.status == "deprecated"' > /dev/null
api GET "/api/v1/contracts/$CONTRACT_ID" | jq '{id, draft_revision, versions: [.versions[] | {number, status, content_hash}]}'
curl sequence sketchSchiță de secvență curl
curl --request POST "$DOCULA_URL/api/v1/contracts" --cookie "$COOKIE_JAR" \ --header "Content-Type: application/json" --header "Origin: $DOCULA_URL" --header "X-CSRF-Token: $CSRF_TOKEN" \ --data-binary @contract.jsoncurl --request POST "$DOCULA_URL/api/v1/contracts/$CONTRACT_ID/validate" --cookie "$COOKIE_JAR" \ --header "Content-Type: application/json" --header "Origin: $DOCULA_URL" --header "X-CSRF-Token: $CSRF_TOKEN" \ --data-binary @contract-schema.jsoncurl --request POST "$DOCULA_URL/api/v1/contracts/$CONTRACT_ID/versions" --cookie "$COOKIE_JAR" \ --header "Content-Type: application/json" --header "Origin: $DOCULA_URL" --header "X-CSRF-Token: $CSRF_TOKEN" \ --data-binary '{"expected_revision": 1}'curl --request POST "$DOCULA_URL/api/v1/contracts/$CONTRACT_ID/versions/1/publish" --cookie "$COOKIE_JAR" \ --header "Content-Type: application/json" --header "Origin: $DOCULA_URL" --header "X-CSRF-Token: $CSRF_TOKEN" \ --data-binary '{}'curl --request POST "$DOCULA_URL/api/v1/cases/generate" --cookie "$COOKIE_JAR" \ --header "Content-Type: application/json" --header "Origin: $DOCULA_URL" --header "X-CSRF-Token: $CSRF_TOKEN" \ --data-binary @generate-case.jsoncurl --request GET "$DOCULA_URL/api/v1/ingestion-models/$INGESTION_MODEL_ID/versions:resolve" --get --data-urlencode "at=$OBJECT_EFFECTIVE_AT" --cookie "$COOKIE_JAR"
OperationsOperații
- POST createContract
/api/v1/contracts - POST validateContractDraft
/api/v1/contracts/{contractId}/validate - PUT updateContractDraft
/api/v1/contracts/{contractId} - POST createContractVersion
/api/v1/contracts/{contractId}/versions - POST publishContractVersion
/api/v1/contracts/{contractId}/versions/{versionNumber}/publish - POST deprecateContractVersion
/api/v1/contracts/{contractId}/versions/{versionNumber}/deprecate - POST generateCase
/api/v1/cases/generate - POST importFixtureCase
/api/v1/cases/import - POST createIngestionModel
/api/v1/ingestion-models - GET getIngestionModelDraft
/api/v1/ingestion-models/{ingestionModelId}/draft - PATCH updateIngestionModelDraft
/api/v1/ingestion-models/{ingestionModelId}/draft - POST versionIngestionModel
/api/v1/ingestion-models/{ingestionModelId}/versions - POST publishIngestionModelVersionCalendar
/api/v1/ingestion-models/{ingestionModelId}/versions/{versionNumber}/publish - POST cancelIngestionModelActivation
/api/v1/ingestion-models/{ingestionModelId}/activations/{activationId}/cancel - GET resolveIngestionModelVersion
/api/v1/ingestion-models/{ingestionModelId}/versions:resolve
