upload current sources
This commit is contained in:
@@ -0,0 +1,731 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "arena-plm connectivity worker API",
|
||||
"version": "0.2.1",
|
||||
"x-sap-shortText": "Inbound machine-to-machine surface of the arena-plm connectivity worker: Arena PLM webhook intake, health probe, and sync-run control.",
|
||||
"x-sap-stateInfo": {
|
||||
"state": "beta"
|
||||
},
|
||||
"description": "POC-stage contract for the inbound HTTP surface of the arena-plm connectivity worker (a machine-to-machine TypeScript worker on BTP Cloud Foundry; no UI, no user-facing URLs). This document governs the implementation (API-first).\n\nScope: inbound endpoints only. The worker's outbound legs — the Arena PLM REST API (`api.arenasolutions.com/v1`) and the SAP S/4HANA released OData APIs — are governed by their providers' contracts and are out of scope for this document.\n\nStability: this is a POC-stage contract. The Arena webhook payload (ArenaWebhookEvent) is provisional pending the Arena POC, and the authentication scheme is a placeholder pending PRD open question OQ-6."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/",
|
||||
"description": "CF route, decided at deployment (OQ-6)"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "system",
|
||||
"description": "Liveness/readiness for Cloud Foundry health checks."
|
||||
},
|
||||
{
|
||||
"name": "webhooks",
|
||||
"description": "Inbound event intake from Arena PLM (ARN-005)."
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Machine-to-machine administrative operations (sync run control)."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/health": {
|
||||
"get": {
|
||||
"operationId": "getHealth",
|
||||
"summary": "Liveness/readiness probe",
|
||||
"description": "Used by Cloud Foundry health checks. Unauthenticated by design; exposes no business data. `arenaSession.state` reflects the worker's serialized Arena session lifecycle (Arena sessions have a ~25-minute TTL and logins must be serialized; see KNOWLEDGE.md). `poller` reports the delta poller's most recent tick; it is absent until the poller has ticked at least once.",
|
||||
"tags": [
|
||||
"system"
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Worker is up (HTTP 200 in both states — this is a liveness probe, not a readiness gate). `status` is `degraded` when the most recent delta-poll tick failed (an Arena error, or the ingest buffer being full at poll time — the tick is failed without advancing the cursor so the window is re-covered) or the most recent Arena login failed; it returns to `ok` on the next successful tick/login.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HealthStatus"
|
||||
},
|
||||
"examples": {
|
||||
"ok": {
|
||||
"summary": "Fully operational",
|
||||
"value": {
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"uptimeSeconds": 5231.7,
|
||||
"arenaSession": {
|
||||
"state": "active"
|
||||
},
|
||||
"poller": {
|
||||
"lastRunAt": "2026-09-14T08:15:00Z",
|
||||
"lastSuccessAt": "2026-09-14T08:15:00Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"degraded": {
|
||||
"summary": "Last poll tick failed",
|
||||
"value": {
|
||||
"status": "degraded",
|
||||
"version": "0.1.0",
|
||||
"uptimeSeconds": 5531.2,
|
||||
"arenaSession": {
|
||||
"state": "expired"
|
||||
},
|
||||
"poller": {
|
||||
"lastRunAt": "2026-09-14T08:20:00Z",
|
||||
"lastSuccessAt": "2026-09-14T08:15:00Z",
|
||||
"lastError": "Upstream Arena PLM request failed (HTTP 503)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhooks/arena": {
|
||||
"post": {
|
||||
"operationId": "receiveArenaWebhook",
|
||||
"summary": "Arena PLM event intake",
|
||||
"description": "Receives Arena PLM webhook events (ARN-005). The worker validates only that the body is a JSON object, then queues the event for asynchronous processing; acceptance (202) does not imply the event was processed or will result in a staged change. The ingest queue is a bounded transient buffer (`WEBHOOK_QUEUE_MAX_SIZE`, default 10000; never durable worker-local storage — ARN-003); when it is full the event is NOT accepted and the sender must retry (503 with `Retry-After`). The request body schema (ArenaWebhookEvent) is PROVISIONAL pending the Arena POC and will be tightened then.",
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ArenaWebhookEvent"
|
||||
},
|
||||
"example": {
|
||||
"eventType": "ITEM_MODIFIED",
|
||||
"itemGuid": "K3C9D0E1F2",
|
||||
"itemNumber": "900-00042",
|
||||
"occurredAt": "2026-09-10T08:15:30Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Event accepted and queued for asynchronous processing.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WebhookAccepted"
|
||||
},
|
||||
"example": {
|
||||
"received": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Malformed request (e.g. body is not a JSON object).",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Request body must be a JSON object."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"415": {
|
||||
"$ref": "#/components/responses/UnsupportedMediaType"
|
||||
},
|
||||
"503": {
|
||||
"description": "Ingest queue is full (bounded transient buffer at `WEBHOOK_QUEUE_MAX_SIZE`). The event was NOT queued; the sender should retry after the `Retry-After` delay. Backpressure is preferred over unbounded memory growth or spilling to worker-local storage (ARN-003).",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"description": "Seconds the sender should wait before retrying (RFC 9110 §10.2.3, delay-seconds form). The worker sends 30.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"example": 30
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Service Unavailable",
|
||||
"status": 503,
|
||||
"detail": "Ingest queue is full; retry later."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/sync-runs": {
|
||||
"post": {
|
||||
"operationId": "startSyncRun",
|
||||
"summary": "Start a sync run",
|
||||
"description": "Starts a sync run (full or delta). When `objects` is omitted, the run uses the worker's configured chain order (default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS; may be a subset via SYNC_OBJECT_ORDER) — requirement ARN-001: config-driven sync chain order. When `objects` is provided, it selects and orders the object types for this run.",
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SyncRunRequest"
|
||||
},
|
||||
"example": {
|
||||
"scope": "full"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Sync run created. Execution is asynchronous; poll `GET /admin/sync-runs/{runId}` for progress. All entries in `objects` start as `pending` with no `processed` counter (it appears once the object has started).",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SyncRun"
|
||||
},
|
||||
"example": {
|
||||
"runId": "7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d",
|
||||
"scope": "full",
|
||||
"state": "queued",
|
||||
"startedAt": "2026-09-10T08:20:00Z",
|
||||
"objects": [
|
||||
{
|
||||
"objectType": "PRODUCT",
|
||||
"state": "pending"
|
||||
},
|
||||
{
|
||||
"objectType": "CHANGE_MASTER",
|
||||
"state": "pending"
|
||||
},
|
||||
{
|
||||
"objectType": "BOM",
|
||||
"state": "pending"
|
||||
},
|
||||
{
|
||||
"objectType": "DOCUMENTS",
|
||||
"state": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Malformed request: invalid `scope` value, `objects` containing an unknown or duplicate object type, a property of the wrong type (no coercion — `{\"scope\": [\"full\"]}` or `{\"objects\": \"BOM\"}` are rejected), or any property not defined by SyncRunRequest (strict rejection, `additionalProperties: false`).",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Invalid value 'incremental' for 'scope'; expected 'full' or 'delta'."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/PayloadTooLarge"
|
||||
},
|
||||
"415": {
|
||||
"$ref": "#/components/responses/UnsupportedMediaType"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/sync-runs/{runId}": {
|
||||
"get": {
|
||||
"operationId": "getSyncRun",
|
||||
"summary": "Get a sync run by ID",
|
||||
"description": "Returns the current state of a sync run, including per-object progress in execution order.",
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "runId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Identifier of the sync run, as returned by `POST /admin/sync-runs`.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The sync run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SyncRun"
|
||||
},
|
||||
"example": {
|
||||
"runId": "7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d",
|
||||
"scope": "full",
|
||||
"state": "failed",
|
||||
"startedAt": "2026-09-10T08:20:00Z",
|
||||
"finishedAt": "2026-09-10T08:34:12Z",
|
||||
"error": "BOM: Upstream Arena PLM request failed (HTTP 503).",
|
||||
"objects": [
|
||||
{
|
||||
"objectType": "PRODUCT",
|
||||
"state": "completed",
|
||||
"processed": 15
|
||||
},
|
||||
{
|
||||
"objectType": "CHANGE_MASTER",
|
||||
"state": "completed",
|
||||
"processed": 4
|
||||
},
|
||||
{
|
||||
"objectType": "BOM",
|
||||
"state": "failed",
|
||||
"processed": 9,
|
||||
"error": "Upstream Arena PLM request failed (HTTP 503)."
|
||||
},
|
||||
{
|
||||
"objectType": "DOCUMENTS",
|
||||
"state": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "No sync run exists with the given `runId`.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Not Found",
|
||||
"status": 404,
|
||||
"detail": "Sync run 7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d not found."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/Default"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"description": "PLACEHOLDER. The final authentication scheme for webhook and admin access is an OPEN decision (PRD OQ-6: webhook receiver hosting and auth for the worker on CF). This scheme stands in for whatever is decided (e.g. XSUAA-issued JWT, static bearer token) and this contract will be revised when OQ-6 is closed. Only `/health` is unauthenticated."
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"Unauthorized": {
|
||||
"description": "Missing or invalid bearer credentials.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Unauthorized",
|
||||
"status": 401,
|
||||
"detail": "Missing or invalid bearer token."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BadGateway": {
|
||||
"description": "An upstream Arena PLM request failed (network error, timeout, or non-success HTTP status from `api.arenasolutions.com`). RESERVED mapping: every operation that invokes Arena synchronously responds with this instead of 500. The upstream response body is never forwarded — `detail` is a fixed, generic phrase — so Arena error payloads (which may echo request data) cannot leak through the worker. No operation in this version calls Arena synchronously (sync runs and polls are asynchronous); the mapping is defined here so it is contract-governed before the first such operation lands.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Bad Gateway",
|
||||
"status": 502,
|
||||
"detail": "Upstream Arena PLM request failed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Default": {
|
||||
"description": "Any status code not listed explicitly for the operation. Every response the worker emits is governed: the body is always a `Problem` (RFC 9457, `application/problem+json`). Cases: an unmatched route or method (404, see `x-unmatched-routes`); a status raised by the HTTP framework before the handler runs (e.g. 400 for a `Content-Length` mismatch or an empty `application/json` body); an unhandled error (500). 5xx bodies never carry a `detail` derived from an error message — `title` mirrors the HTTP status phrase and nothing else is disclosed.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Internal Server Error",
|
||||
"status": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UnsupportedMediaType": {
|
||||
"description": "The request `Content-Type` is not `application/json`. Only `application/json` is parsed; every other media type (including `text/plain`, whose built-in parser is removed) and a missing `Content-Type` on a request with a body is rejected before the handler runs. [source: context7 /fastify/fastify docs/Reference/Errors.md FST_ERR_CTP_INVALID_MEDIA_TYPE; docs/Reference/ContentTypeParser.md removeContentTypeParser, verified 2026-09-14]",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Unsupported Media Type",
|
||||
"status": 415,
|
||||
"detail": "Unsupported Media Type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request body exceeds the worker's body limit (framework default 1 MiB = 1048576 bytes; the worker does not raise it). The body is not parsed and nothing is queued or created. [source: context7 /fastify/fastify docs/Reference/Server.md `bodyLimit`; docs/Reference/Errors.md FST_ERR_CTP_BODY_TOO_LARGE, verified 2026-09-14]",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
},
|
||||
"example": {
|
||||
"title": "Payload Too Large",
|
||||
"status": 413,
|
||||
"detail": "Request body is too large"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"HealthStatus": {
|
||||
"type": "object",
|
||||
"description": "Worker liveness/readiness report.",
|
||||
"required": [
|
||||
"status",
|
||||
"version",
|
||||
"uptimeSeconds",
|
||||
"arenaSession"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ok",
|
||||
"degraded"
|
||||
],
|
||||
"description": "`ok` when fully operational; `degraded` when the most recent delta-poll tick failed or the most recent Arena login failed. A tick fails when Arena cannot be reached/answers an error, or when the ingest buffer is full at poll time (before the fetch, or partially while enqueuing) — the tick is failed rather than dropping changes, the poll cursor holds, and the next tick re-covers the window (duplicates on recovery are acceptable: SAP is the ledger, KNOWLEDGE.md 2026-09-04). Derived from the worker's health registry, which the poller and the Arena session manager report into."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Deployed worker version.",
|
||||
"examples": [
|
||||
"0.1.0"
|
||||
]
|
||||
},
|
||||
"uptimeSeconds": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Seconds since the worker process started."
|
||||
},
|
||||
"arenaSession": {
|
||||
"type": "object",
|
||||
"description": "State of the worker's (serialized) Arena PLM API session.",
|
||||
"required": [
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"active",
|
||||
"expired",
|
||||
"refreshing"
|
||||
],
|
||||
"description": "`none`: no session has been established since process start; `active`: a cached session exists and is within its configured TTL; `expired`: a cached session exists but is past its TTL (the next Arena call re-logs in lazily); `refreshing`: a login is in flight (serialized behind the promise lock — concurrent callers join it). `refreshing` takes precedence over the other three while a login is in flight."
|
||||
}
|
||||
}
|
||||
},
|
||||
"poller": {
|
||||
"type": "object",
|
||||
"description": "Most recent delta-poll tick. Absent until the poller has ticked at least once. Timestamps are the worker's clock at the start of the tick.",
|
||||
"properties": {
|
||||
"lastRunAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Start time of the most recent tick (successful or not)."
|
||||
},
|
||||
"lastSuccessAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Start time of the most recent tick that completed without error. Absent if no tick has succeeded yet."
|
||||
},
|
||||
"lastError": {
|
||||
"type": "string",
|
||||
"description": "Fixed, non-sensitive phrase classifying the failure of the most recent tick (allowlist keyed on error class / upstream HTTP status, e.g. `Upstream Arena PLM request failed (HTTP 503).`, `Ingest buffer full at poll time.`, `Unclassified worker error; see worker logs.`); absent when the most recent tick succeeded. Never the raw error message — upstream response bodies, credentials, and session tokens cannot appear here; the full error stays in the worker log."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ArenaWebhookEvent": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "PROVISIONAL envelope for Arena PLM webhook events. All fields are optional and unknown properties are accepted (`additionalProperties: true`) because the real Arena webhook payload shape has not yet been verified against the live Arena API. This schema is provisional pending the Arena POC and WILL be tightened (required fields, closed property set, event-type enum) once the POC establishes ground truth. Do not code against fields beyond those named here.",
|
||||
"properties": {
|
||||
"eventType": {
|
||||
"type": "string",
|
||||
"description": "Arena event type identifier (exact vocabulary to be established by the Arena POC).",
|
||||
"examples": [
|
||||
"ITEM_MODIFIED"
|
||||
]
|
||||
},
|
||||
"itemGuid": {
|
||||
"type": "string",
|
||||
"description": "Arena internal identifier (GUID) of the affected item, if the event concerns an item."
|
||||
},
|
||||
"itemNumber": {
|
||||
"type": "string",
|
||||
"description": "Arena item number of the affected item, if the event concerns an item.",
|
||||
"examples": [
|
||||
"900-00042"
|
||||
]
|
||||
},
|
||||
"occurredAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the event occurred in Arena, if provided."
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebhookAccepted": {
|
||||
"type": "object",
|
||||
"description": "Acknowledgement that a webhook event was accepted and queued.",
|
||||
"required": [
|
||||
"received"
|
||||
],
|
||||
"properties": {
|
||||
"received": {
|
||||
"type": "boolean",
|
||||
"const": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SyncRunRequest": {
|
||||
"type": "object",
|
||||
"description": "Request to start a sync run. STRICT: unknown properties are rejected with 400 (seam ruling 2026-09-10 — on a machine-to-machine admin API a typo'd property such as `object` instead of `objects` must never silently fall back to defaults). Values are validated as sent: no type coercion (`{\"scope\": [\"full\"]}` is 400, not unwrapped to `\"full\"`) and no server-side default injection.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"scope"
|
||||
],
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"delta"
|
||||
],
|
||||
"description": "`full`: initial/complete sync; `delta`: changes since the last successful run."
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Object types to sync, in execution order. Omit to use the worker's configured chain order (default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS; may be a subset via SYNC_OBJECT_ORDER) — requirement ARN-001. No schema `default` is declared: the server injects nothing (`useDefaults: false`) and the effective order is the worker's configuration, not a contract constant.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SyncObjectType"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SyncObjectType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PRODUCT",
|
||||
"CHANGE_MASTER",
|
||||
"BOM",
|
||||
"DOCUMENTS"
|
||||
],
|
||||
"description": "Object types handled by the sync chain (ARN-001: PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS). The token is `DOCUMENTS` (plural), as in the requirement."
|
||||
},
|
||||
"SyncRun": {
|
||||
"type": "object",
|
||||
"description": "A sync run and its current state. The run executes its object types sequentially in the order of `objects` (ARN-001) and stops the chain at the first failed object — remaining objects stay `pending`. `finishedAt` is set when the run reaches `completed` or `failed`.",
|
||||
"required": [
|
||||
"runId",
|
||||
"scope",
|
||||
"state",
|
||||
"startedAt",
|
||||
"objects"
|
||||
],
|
||||
"properties": {
|
||||
"runId": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Server-assigned identifier of the run."
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"delta"
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"queued",
|
||||
"running",
|
||||
"completed",
|
||||
"failed"
|
||||
],
|
||||
"description": "`completed`: every object in the chain completed. `failed`: the run aborted — the chain stopped at a failed object or a run-level error occurred; see `error` and the per-object entries in `objects`."
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the run was created/accepted."
|
||||
},
|
||||
"finishedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the run reached a terminal state (`completed` or `failed`). Absent while queued or running."
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Per-object progress in execution order for this run (ARN-001). An ordered progress array communicates chain order and stop-on-failure directly, which a keyed counts map cannot. All entries are `pending` at run creation.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SyncObjectProgress"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "Run-level abort reason; present when `state` is `failed`. The failed object type followed by the same fixed, non-sensitive phrase recorded in that object's `error` (e.g. `BOM: Upstream Arena PLM request failed (HTTP 503).`), or `<type>: no executor registered`. Never a raw error message."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SyncObjectProgress": {
|
||||
"type": "object",
|
||||
"description": "Progress of one object type within a sync run's sequential execution chain.",
|
||||
"required": [
|
||||
"objectType",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"objectType": {
|
||||
"$ref": "#/components/schemas/SyncObjectType"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"running",
|
||||
"completed",
|
||||
"failed"
|
||||
],
|
||||
"description": "All entries are `pending` at run creation; each advances to `running`, then `completed` or `failed`, in chain order. A `failed` entry stops the chain; entries after it remain `pending`."
|
||||
},
|
||||
"processed": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of records processed for this object type; present once the object has run (from `running` onwards), absent while `pending`. POC-stage granularity: this will be refined into created/updated/failed counters when the S/4HANA execution leg lands (same tightening wave as ArenaWebhookEvent)."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "Fixed, non-sensitive phrase classifying the failure (allowlist keyed on error class / upstream HTTP status — the same mapping as `HealthStatus.poller.lastError`); present when this object's `state` is `failed`. Never the raw error message: upstream response bodies, credentials, and session tokens cannot appear here; the full error stays in the worker log."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"description": "Error envelope per RFC 9457 (application/problem+json).",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"format": "uri-reference",
|
||||
"description": "URI reference identifying the problem type. Defaults to `about:blank`.",
|
||||
"default": "about:blank"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short, human-readable summary of the problem type."
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 599,
|
||||
"description": "HTTP status code for this occurrence."
|
||||
},
|
||||
"detail": {
|
||||
"type": "string",
|
||||
"description": "Human-readable explanation specific to this occurrence."
|
||||
},
|
||||
"instance": {
|
||||
"type": "string",
|
||||
"format": "uri-reference",
|
||||
"description": "URI reference identifying this specific occurrence."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-unmatched-routes": "Any request whose method+path is not listed under `paths` is answered 404 with a `Problem` body (application/problem+json), including a known path with an unlisted method — the router answers 404, not 405, for a method mismatch (probed against the worker 2026-09-14; Fastify default not-found handling, see `components.responses.Default`). It is the route-level case of every operation's `default` response and cannot be attached to an operation because no operation matches."
|
||||
}
|
||||
Reference in New Issue
Block a user