diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40eba61..3893efe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 permissions: - contents: read + contents: write env: CARGO_TERM_COLOR: always steps: @@ -113,6 +113,10 @@ jobs: if: needs.classify_changes.outputs.run_full_ci != 'true' run: echo "Text-only change detected; skipping workspace test run." + # Default checkout: on pull_request this gives us the merge commit + # (refs/pull/N/merge), which is what we want to test. For same-repo PRs + # the regenerated openapi.json is pushed to the head branch below via a + # separate shallow clone. - name: Checkout source if: needs.classify_changes.outputs.run_full_ci == 'true' uses: actions/checkout@v5.0.1 @@ -138,8 +142,45 @@ jobs: - name: Run workspace tests if: needs.classify_changes.outputs.run_full_ci == 'true' + # On same-repo PRs, regenerate openapi.json as part of the drift test + # so the following step can commit the update. Elsewhere the env var + # is empty, leaving the drift test in strict-check mode. + env: + OMNIGRAPH_UPDATE_OPENAPI: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) && '1' || '' }} run: cargo test --workspace --locked + - name: Commit regenerated openapi.json to PR branch + if: | + needs.classify_changes.outputs.run_full_ci == 'true' && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # The workspace was checked out at the PR's merge commit so tests + # see the merged state. Pushing the regenerated openapi.json back + # to the PR branch is done via a separate shallow clone so the + # pushed commit contains only the spec change, not the merge state. + if git diff --quiet -- openapi.json; then + echo "openapi.json is already in sync." + exit 0 + fi + tmp=$(mktemp -d) + git clone --depth 1 --branch "${{ github.head_ref }}" \ + "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" \ + "$tmp" + cp openapi.json "$tmp/openapi.json" + cd "$tmp" + if git diff --quiet -- openapi.json; then + echo "openapi.json matches PR branch; nothing to push." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add openapi.json + git commit -m "chore: regenerate openapi.json" + git push + test_aws_feature: name: Test omnigraph-server --features aws needs: classify_changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6d1030..8d9c687 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,21 @@ cargo test --workspace If you touch S3-backed flows, the CI model uses a local RustFS instance for integration tests. +### OpenAPI spec + +`openapi.json` is a committed artifact generated from the Utoipa annotations in +`crates/omnigraph-server`. For PRs opened from this repository, a CI job +regenerates it automatically and commits the updated file back to the PR +branch. For PRs from forks (where CI cannot push), run the regeneration +manually: + +```bash +OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date +``` + +The workspace test run fails if the committed `openapi.json` drifts from what +the source generates. + ### Cargo features `omnigraph-server` has an optional `aws` feature that pulls in the AWS diff --git a/crates/omnigraph-server/src/lib.rs b/crates/omnigraph-server/src/lib.rs index 5d08734..eea4699 100644 --- a/crates/omnigraph-server/src/lib.rs +++ b/crates/omnigraph-server/src/lib.rs @@ -492,6 +492,7 @@ async fn shutdown_signal() { get, path = "/healthz", tag = "health", + operation_id = "health", responses( (status = 200, description = "Server is healthy", body = HealthOutput), ), @@ -606,6 +607,7 @@ fn authorize_request( get, path = "/snapshot", tag = "snapshots", + operation_id = "getSnapshot", params(SnapshotQuery), responses( (status = 200, description = "Database snapshot", body = api::SnapshotOutput), @@ -646,6 +648,7 @@ async fn server_snapshot( post, path = "/read", tag = "queries", + operation_id = "read", request_body = ReadRequest, responses( (status = 200, description = "Query results", body = ReadOutput), @@ -715,6 +718,7 @@ async fn server_read( post, path = "/export", tag = "queries", + operation_id = "export", request_body = ExportRequest, responses( (status = 200, description = "Exported data as NDJSON", content_type = "application/x-ndjson"), @@ -773,6 +777,7 @@ async fn server_export( post, path = "/change", tag = "mutations", + operation_id = "change", request_body = ChangeRequest, responses( (status = 200, description = "Mutation results", body = ChangeOutput), @@ -831,6 +836,7 @@ async fn server_change( get, path = "/schema", tag = "schema", + operation_id = "getSchema", responses( (status = 200, description = "Current schema source", body = SchemaOutput), (status = 401, description = "Unauthorized", body = ErrorOutput), @@ -866,6 +872,7 @@ async fn server_schema_get( post, path = "/schema/apply", tag = "mutations", + operation_id = "applySchema", request_body = SchemaApplyRequest, responses( (status = 200, description = "Schema apply results", body = SchemaApplyOutput), @@ -904,6 +911,7 @@ async fn server_schema_apply( post, path = "/ingest", tag = "mutations", + operation_id = "ingest", request_body = IngestRequest, responses( (status = 200, description = "Ingest results", body = IngestOutput), @@ -973,6 +981,7 @@ async fn server_ingest( get, path = "/branches", tag = "branches", + operation_id = "listBranches", responses( (status = 200, description = "List of branches", body = BranchListOutput), (status = 401, description = "Unauthorized", body = ErrorOutput), @@ -1009,6 +1018,7 @@ async fn server_branch_list( post, path = "/branches", tag = "branches", + operation_id = "createBranch", request_body = BranchCreateRequest, responses( (status = 200, description = "Branch created", body = BranchCreateOutput), @@ -1056,6 +1066,7 @@ async fn server_branch_create( delete, path = "/branches/{branch}", tag = "branches", + operation_id = "deleteBranch", params( ("branch" = String, Path, description = "Branch name to delete"), ), @@ -1100,6 +1111,7 @@ async fn server_branch_delete( post, path = "/branches/merge", tag = "branches", + operation_id = "mergeBranches", request_body = BranchMergeRequest, responses( (status = 200, description = "Branches merged", body = BranchMergeOutput), @@ -1145,6 +1157,7 @@ async fn server_branch_merge( get, path = "/runs", tag = "runs", + operation_id = "listRuns", responses( (status = 200, description = "List of runs", body = RunListOutput), (status = 401, description = "Unauthorized", body = ErrorOutput), @@ -1182,6 +1195,7 @@ async fn server_run_list( get, path = "/runs/{run_id}", tag = "runs", + operation_id = "getRun", params( ("run_id" = String, Path, description = "Run identifier"), ), @@ -1224,6 +1238,7 @@ async fn server_run_show( post, path = "/runs/{run_id}/publish", tag = "runs", + operation_id = "publishRun", params( ("run_id" = String, Path, description = "Run identifier"), ), @@ -1273,6 +1288,7 @@ async fn server_run_publish( post, path = "/runs/{run_id}/abort", tag = "runs", + operation_id = "abortRun", params( ("run_id" = String, Path, description = "Run identifier"), ), @@ -1321,6 +1337,7 @@ async fn server_run_abort( get, path = "/commits", tag = "commits", + operation_id = "listCommits", params(CommitListQuery), responses( (status = 200, description = "List of commits", body = CommitListOutput), @@ -1362,6 +1379,7 @@ async fn server_commit_list( get, path = "/commits/{commit_id}", tag = "commits", + operation_id = "getCommit", params( ("commit_id" = String, Path, description = "Commit identifier"), ), diff --git a/crates/omnigraph-server/tests/openapi.rs b/crates/omnigraph-server/tests/openapi.rs index d8e0cc7..b257796 100644 --- a/crates/omnigraph-server/tests/openapi.rs +++ b/crates/omnigraph-server/tests/openapi.rs @@ -963,3 +963,31 @@ async fn auth_mode_healthz_still_has_no_security() { "auth-mode: /healthz should still have no security" ); } + +#[test] +fn openapi_spec_is_up_to_date() { + let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../openapi.json"); + + let generated = serde_json::to_string_pretty(&openapi_doc()).unwrap() + "\n"; + + if !env::var("OMNIGRAPH_UPDATE_OPENAPI") + .unwrap_or_default() + .is_empty() + { + fs::write(&spec_path, &generated).unwrap(); + return; + } + + let committed = fs::read_to_string(&spec_path).unwrap_or_else(|_| { + panic!( + "openapi.json not found at {}. Run: OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date", + spec_path.display() + ) + }); + + assert_eq!( + committed, generated, + "openapi.json is out of date. Run: OMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_date" + ); +} diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..bf82676 --- /dev/null +++ b/openapi.json @@ -0,0 +1,1827 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Omnigraph API", + "description": "HTTP API for the Omnigraph graph database", + "license": { + "name": "MIT", + "identifier": "MIT" + }, + "version": "0.2.2" + }, + "paths": { + "/branches": { + "get": { + "tags": [ + "branches" + ], + "operationId": "listBranches", + "responses": { + "200": { + "description": "List of branches", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchListOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + }, + "post": { + "tags": [ + "branches" + ], + "operationId": "createBranch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Branch created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchCreateOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Branch already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/branches/merge": { + "post": { + "tags": [ + "branches" + ], + "operationId": "mergeBranches", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchMergeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Branches merged", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchMergeOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Merge conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/branches/{branch}": { + "delete": { + "tags": [ + "branches" + ], + "operationId": "deleteBranch", + "parameters": [ + { + "name": "branch", + "in": "path", + "description": "Branch name to delete", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Branch deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BranchDeleteOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Branch not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/change": { + "post": { + "tags": [ + "mutations" + ], + "operationId": "change", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Mutation results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Merge conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/commits": { + "get": { + "tags": [ + "commits" + ], + "operationId": "listCommits", + "parameters": [ + { + "name": "branch", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], + "responses": { + "200": { + "description": "List of commits", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitListOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/commits/{commit_id}": { + "get": { + "tags": [ + "commits" + ], + "operationId": "getCommit", + "parameters": [ + { + "name": "commit_id", + "in": "path", + "description": "Commit identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Commit details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Commit not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/export": { + "post": { + "tags": [ + "queries" + ], + "operationId": "export", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Exported data as NDJSON", + "content": { + "application/x-ndjson": {} + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/healthz": { + "get": { + "tags": [ + "health" + ], + "operationId": "health", + "responses": { + "200": { + "description": "Server is healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthOutput" + } + } + } + } + } + } + }, + "/ingest": { + "post": { + "tags": [ + "mutations" + ], + "operationId": "ingest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Ingest results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/read": { + "post": { + "tags": [ + "queries" + ], + "operationId": "read", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Query results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs": { + "get": { + "tags": [ + "runs" + ], + "operationId": "listRuns", + "responses": { + "200": { + "description": "List of runs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunListOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs/{run_id}": { + "get": { + "tags": [ + "runs" + ], + "operationId": "getRun", + "parameters": [ + { + "name": "run_id", + "in": "path", + "description": "Run identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Run details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs/{run_id}/abort": { + "post": { + "tags": [ + "runs" + ], + "operationId": "abortRun", + "parameters": [ + { + "name": "run_id", + "in": "path", + "description": "Run identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Run aborted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/runs/{run_id}/publish": { + "post": { + "tags": [ + "runs" + ], + "operationId": "publishRun", + "parameters": [ + { + "name": "run_id", + "in": "path", + "description": "Run identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Run published", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/schema": { + "get": { + "tags": [ + "schema" + ], + "operationId": "getSchema", + "responses": { + "200": { + "description": "Current schema source", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/schema/apply": { + "post": { + "tags": [ + "mutations" + ], + "operationId": "applySchema", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaApplyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Schema apply results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaApplyOutput" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, + "/snapshot": { + "get": { + "tags": [ + "snapshots" + ], + "operationId": "getSnapshot", + "parameters": [ + { + "name": "branch", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], + "responses": { + "200": { + "description": "Database snapshot", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + } + }, + "components": { + "schemas": { + "BranchCreateOutput": { + "type": "object", + "required": [ + "uri", + "from", + "name" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "from": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + "BranchCreateRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "from": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + } + } + }, + "BranchDeleteOutput": { + "type": "object", + "required": [ + "uri", + "name" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + "BranchListOutput": { + "type": "object", + "required": [ + "branches" + ], + "properties": { + "branches": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BranchMergeOutcome": { + "type": "string", + "enum": [ + "already_up_to_date", + "fast_forward", + "merged" + ] + }, + "BranchMergeOutput": { + "type": "object", + "required": [ + "source", + "target", + "outcome" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "outcome": { + "$ref": "#/components/schemas/BranchMergeOutcome" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + } + } + }, + "BranchMergeRequest": { + "type": "object", + "required": [ + "source" + ], + "properties": { + "source": { + "type": "string" + }, + "target": { + "type": [ + "string", + "null" + ] + } + } + }, + "ChangeOutput": { + "type": "object", + "required": [ + "branch", + "query_name", + "affected_nodes", + "affected_edges" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "affected_edges": { + "type": "integer", + "minimum": 0 + }, + "affected_nodes": { + "type": "integer", + "minimum": 0 + }, + "branch": { + "type": "string" + }, + "query_name": { + "type": "string" + } + } + }, + "ChangeRequest": { + "type": "object", + "required": [ + "query_source" + ], + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "params": {}, + "query_name": { + "type": [ + "string", + "null" + ] + }, + "query_source": { + "type": "string" + } + } + }, + "CommitListOutput": { + "type": "object", + "required": [ + "commits" + ], + "properties": { + "commits": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommitOutput" + } + } + } + }, + "CommitOutput": { + "type": "object", + "required": [ + "graph_commit_id", + "manifest_version", + "created_at" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "graph_commit_id": { + "type": "string" + }, + "manifest_branch": { + "type": [ + "string", + "null" + ] + }, + "manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "merged_parent_commit_id": { + "type": [ + "string", + "null" + ] + }, + "parent_commit_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "ErrorCode": { + "type": "string", + "enum": [ + "unauthorized", + "forbidden", + "bad_request", + "not_found", + "conflict", + "internal" + ] + }, + "ErrorOutput": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "code": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ErrorCode" + } + ] + }, + "error": { + "type": "string" + }, + "merge_conflicts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MergeConflictOutput" + } + } + } + }, + "ExportRequest": { + "type": "object", + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "table_keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "type_names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "HealthOutput": { + "type": "object", + "required": [ + "status", + "version" + ], + "properties": { + "source_version": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "IngestOutput": { + "type": "object", + "required": [ + "uri", + "branch", + "base_branch", + "branch_created", + "mode", + "tables" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "base_branch": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "branch_created": { + "type": "boolean" + }, + "mode": { + "$ref": "#/components/schemas/LoadMode" + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IngestTableOutput" + } + }, + "uri": { + "type": "string" + } + } + }, + "IngestRequest": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "string" + }, + "from": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/LoadMode" + } + ] + } + } + }, + "IngestTableOutput": { + "type": "object", + "required": [ + "table_key", + "rows_loaded" + ], + "properties": { + "rows_loaded": { + "type": "integer", + "minimum": 0 + }, + "table_key": { + "type": "string" + } + } + }, + "LoadMode": { + "type": "string", + "description": "Shadow enum for documenting [`LoadMode`] in the OpenAPI schema.", + "enum": [ + "overwrite", + "append", + "merge" + ] + }, + "MergeConflictKindOutput": { + "type": "string", + "enum": [ + "divergent_insert", + "divergent_update", + "delete_vs_update", + "orphan_edge", + "unique_violation", + "cardinality_violation", + "value_constraint_violation" + ] + }, + "MergeConflictOutput": { + "type": "object", + "required": [ + "table_key", + "kind", + "message" + ], + "properties": { + "kind": { + "$ref": "#/components/schemas/MergeConflictKindOutput" + }, + "message": { + "type": "string" + }, + "row_id": { + "type": [ + "string", + "null" + ] + }, + "table_key": { + "type": "string" + } + } + }, + "ReadOutput": { + "type": "object", + "required": [ + "query_name", + "target", + "row_count", + "rows" + ], + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string" + } + }, + "query_name": { + "type": "string" + }, + "row_count": { + "type": "integer", + "minimum": 0 + }, + "rows": {}, + "target": { + "$ref": "#/components/schemas/ReadTargetOutput" + } + } + }, + "ReadRequest": { + "type": "object", + "required": [ + "query_source" + ], + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "params": {}, + "query_name": { + "type": [ + "string", + "null" + ] + }, + "query_source": { + "type": "string" + }, + "snapshot": { + "type": [ + "string", + "null" + ] + } + } + }, + "ReadTargetOutput": { + "type": "object", + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "snapshot": { + "type": [ + "string", + "null" + ] + } + } + }, + "RunListOutput": { + "type": "object", + "required": [ + "runs" + ], + "properties": { + "runs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RunOutput" + } + } + } + }, + "RunOutput": { + "type": "object", + "required": [ + "run_id", + "target_branch", + "run_branch", + "base_snapshot_id", + "base_manifest_version", + "status", + "created_at", + "updated_at" + ], + "properties": { + "actor_id": { + "type": [ + "string", + "null" + ] + }, + "base_manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "base_snapshot_id": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "operation_hash": { + "type": [ + "string", + "null" + ] + }, + "published_snapshot_id": { + "type": [ + "string", + "null" + ] + }, + "run_branch": { + "type": "string" + }, + "run_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "target_branch": { + "type": "string" + }, + "updated_at": { + "type": "integer", + "format": "int64" + } + } + }, + "SchemaApplyOutput": { + "type": "object", + "required": [ + "uri", + "supported", + "applied", + "step_count", + "manifest_version", + "steps" + ], + "properties": { + "applied": { + "type": "boolean" + }, + "manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "step_count": { + "type": "integer", + "minimum": 0 + }, + "steps": { + "type": "array", + "items": {} + }, + "supported": { + "type": "boolean" + }, + "uri": { + "type": "string" + } + } + }, + "SchemaApplyRequest": { + "type": "object", + "required": [ + "schema_source" + ], + "properties": { + "schema_source": { + "type": "string" + } + } + }, + "SchemaOutput": { + "type": "object", + "required": [ + "schema_source" + ], + "properties": { + "schema_source": { + "type": "string" + } + } + }, + "SnapshotOutput": { + "type": "object", + "required": [ + "branch", + "manifest_version", + "tables" + ], + "properties": { + "branch": { + "type": "string" + }, + "manifest_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotTableOutput" + } + } + } + }, + "SnapshotTableOutput": { + "type": "object", + "required": [ + "table_key", + "table_path", + "table_version", + "row_count" + ], + "properties": { + "row_count": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "table_branch": { + "type": [ + "string", + "null" + ] + }, + "table_key": { + "type": "string" + }, + "table_path": { + "type": "string" + }, + "table_version": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + } + }, + "securitySchemes": { + "bearer_token": { + "type": "http", + "scheme": "bearer" + } + } + } +}