From 07000b715e1ca2a06e53575bf6037bef23305351 Mon Sep 17 00:00:00 2001 From: Byron Ellis Date: Sun, 4 Oct 2026 14:18:26 -0700 Subject: [PATCH] Fix generated OpenAPI contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- services/housetables/build.gradle | 34 ++++++++++++++- services/tables/build.gradle | 71 +++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/services/housetables/build.gradle b/services/housetables/build.gradle index 3c7b7b271d..a9da1caab8 100644 --- a/services/housetables/build.gradle +++ b/services/housetables/build.gradle @@ -28,6 +28,39 @@ openApi { apiDocsUrl.set("http://localhost:8001/v3/api-docs") } +tasks.matching { it.name == 'generateOpenApiDocs' }.configureEach { + doLast { + def specFile = file("$buildDir/specs/${project.name}.json") + def spec = new groovy.json.JsonSlurper().parse(specFile) + def userTableSchema = spec?.components?.schemas?.UserTable + if (!(userTableSchema instanceof Map)) { + throw new GradleException("Generated HTS OpenAPI spec is missing the UserTable schema") + } + + def properties = userTableSchema.properties + if (properties == null) { + properties = [:] + userTableSchema.properties = properties + } + if (!(properties instanceof Map)) { + throw new GradleException("Generated HTS OpenAPI UserTable properties are invalid") + } + + // Springdoc omits the runtime-visible entityType model property from the generated schema. + properties.putIfAbsent( + 'entityType', + [ + type: 'string', + description: + "Type of the catalog object occupying this (databaseId, tableId) key. Null or " + + "'TABLE' means a table; 'VIEW' means a view. Matched case-insensitively.", + example: 'TABLE' + ]) + specFile.setText( + groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(spec)) + '\n', 'UTF-8') + } +} + configurations { all { exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' @@ -51,4 +84,3 @@ dependencies { implementation "org.jetbrains:annotations:16.0.3" testImplementation(testFixtures(project(':services:common'))) } - diff --git a/services/tables/build.gradle b/services/tables/build.gradle index b8b9e3f2d4..b9e8b36880 100644 --- a/services/tables/build.gradle +++ b/services/tables/build.gradle @@ -30,6 +30,77 @@ openApi { apiDocsUrl.set("http://localhost:8000/v3/api-docs") } +tasks.matching { it.name == 'generateOpenApiDocs' }.configureEach { + doLast { + def specFile = file("$buildDir/specs/${project.name}.json") + def spec = new groovy.json.JsonSlurper().parse(specFile) + if (!(spec?.paths instanceof Map)) { + throw new GradleException("Generated Tables OpenAPI spec is missing its paths") + } + + def reasonLockPath = '/v1/databases/{databaseId}/tables/{tableId}/lock/{reason}' + def reasonLockItem = spec.paths[reasonLockPath] + if (reasonLockItem == null) { + reasonLockItem = [:] + spec.paths[reasonLockPath] = reasonLockItem + } + if (!(reasonLockItem instanceof Map)) { + throw new GradleException("Generated Tables OpenAPI lock-by-reason path is invalid") + } + + // Springdoc omits this handler when resolving its enum path variable. + if (!reasonLockItem.containsKey('delete')) { + reasonLockItem.delete = [ + tags: ['Table'], + summary: 'Delete a lock by reason', + description: + 'Requires existing LOCK_ADMIN authorization and a matching active lock reason.', + operationId: 'deleteLockByReasonV1', + parameters: [ + [ + name: 'databaseId', + in: 'path', + description: 'Database ID', + required: true, + schema: [type: 'string'] + ], + [ + name: 'tableId', + in: 'path', + description: 'Table ID', + required: true, + schema: [type: 'string'] + ], + [ + name: 'reason', + in: 'path', + description: 'Expected lock reason', + required: true, + schema: [type: 'string'] + ] + ], + responses: [ + '204': [description: 'lock DELETE: NO_CONTENT'], + '400': [description: 'lock DELETE: BAD_REQUEST'], + '401': [description: 'lock DELETE: UNAUTHORIZED'], + '403': [description: 'lock DELETE: FORBIDDEN'], + '404': [description: 'lock DELETE: TABLE_NOT_FOUND'], + '409': [description: 'lock DELETE: REASON_CONFLICT'] + ] + ] + } + + def deleteOperation = reasonLockItem['delete'] + if (!(deleteOperation instanceof Map)) { + throw new GradleException("Generated Tables OpenAPI lock-by-reason DELETE operation is invalid") + } + deleteOperation.operationId = 'deleteLockByReasonV1' + + specFile.setText( + groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(spec)) + '\n', 'UTF-8') + } +} + dependencies { implementation "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:${otel_annotations_version}" api project(':services:common')