Skip to content

Add batch versions latest API endpoint#965

Merged
mbryzek merged 1 commit intomainfrom
batch-version-check
Mar 10, 2026
Merged

Add batch versions latest API endpoint#965
mbryzek merged 1 commit intomainfrom
batch-version-check

Conversation

@mbryzek
Copy link
Collaborator

@mbryzek mbryzek commented Mar 9, 2026

Summary

  • Adds POST /:orgKey/batch/versions/latest endpoint that returns the latest version for multiple applications in a single request
  • Uses efficient PostgreSQL DISTINCT ON query instead of N sequential lookups
  • Includes input size limit (500 keys max) and proper soft-delete filtering

Test plan

  • 5 new controller tests covering existing apps, non-existent apps, mixed, multiple, and empty list
  • Full test suite passes (709 tests)

🤖 Generated with Claude Code

POST /:orgKey/batch/versions/latest returns the latest version for
multiple applications in a single request, replacing N sequential
HTTP calls with one efficient DISTINCT ON query.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 9, 2026 19:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new batch endpoint to fetch “latest version” values for multiple applications in a single request, backed by a single SQL query and covered by controller tests.

Changes:

  • Adds POST /:orgKey/batch/versions/latest to the API spec, routes, controller, and service layer.
  • Implements an efficient PostgreSQL DISTINCT ON query for latest-version lookup across many application keys.
  • Generates new client models/endpoint bindings and adds controller tests.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
spec/apibuilder-api.json Defines new request/response models and the new batch endpoint.
generated/app/ApicollectiveApibuilderApiV0Client.scala Adds generated models + client method for batchVersionsLatest.
api/test/controllers/BatchVersionsLatestSpec.scala Adds controller specs for existing/non-existing/mixed/empty inputs.
api/conf/routes Wires the new POST /:orgKey/batch/versions/latest route.
api/app/services/BatchVersionsLatestService.scala Adds validation + mapping from request keys to latest-version results.
api/app/db/InternalVersionsDao.scala Adds a single-query lookup for latest versions per application key.
api/app/controllers/BatchVersionsLatest.scala Adds the controller action to serve the new endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1035 to +1047
"batch_versions_latest": {
"path": "/:orgKey/batch/versions/latest",
"operations": [
{
"method": "POST",
"description": "Retrieve the latest version for multiple applications in one API call.",
"body": { "type": "batch_versions_latest_form" },
"responses": {
"200": { "type": "batch_versions_latest" }
}
}
]
},
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec declares only a 200 response, but the controller can return a non-200 on validation failure (currently 409). This is an API-contract mismatch for generated clients. Either (a) add the error response code(s) and type(s) to the spec (e.g., 400/409 with the standard error model), or (b) change the controller to only return documented status codes.

Copilot uses AI. Check for mistakes.
def post(orgKey: String): Action[BatchVersionsLatestForm] = Anonymous(parse.json[BatchVersionsLatestForm]) { request =>
service.process(orgKey, request.body) match {
case Valid(result) => Ok(Json.toJson(result))
case Invalid(errors) => Conflict(Json.toJson(Validation.errors(errors)))
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning 409 Conflict for input validation failures (e.g., too many keys) is atypical; 400 Bad Request is usually the correct status for invalid request bodies. Consider switching to BadRequest(...) here (and documenting that code in the API spec) so clients can handle validation errors consistently.

Suggested change
case Invalid(errors) => Conflict(Json.toJson(Validation.errors(errors)))
case Invalid(errors) => BadRequest(Json.toJson(Validation.errors(errors)))

Copilot uses AI. Check for mistakes.
"batch_versions_latest_form": {
"description": "Form to request the latest version for multiple applications in a single API call.",
"fields": [
{ "name": "application_keys", "type": "[string]", "description": "List of application keys to look up" }
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server enforces a max of 500 application_keys, but that limit isn’t documented in the form/field description. Please document the 500-key maximum in the description (or field description) so API consumers know the constraint before hitting runtime validation.

Suggested change
{ "name": "application_keys", "type": "[string]", "description": "List of application keys to look up" }
{ "name": "application_keys", "type": "[string]", "description": "List of application keys to look up (maximum 500 keys per request)" }

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +236
val keyParams = applicationKeys.zipWithIndex.map { case (_, i) => s"{app_key_$i}" }.mkString(", ")
val namedParams = applicationKeys.zipWithIndex.map { case (key, i) =>
NamedParameter(s"app_key_$i", key)
}

dao.db.withConnection { implicit c =>
SQL(
s"""select distinct on (a.key) a.key as app_key, versions.version
| from versions
| join applications a on a.guid = versions.application_guid
| join organizations o on o.guid = a.organization_guid
| where o.key = {org_key}
| and a.key in ($keyParams)
| and versions.deleted_at is null
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building a long IN ({app_key_0}, {app_key_1}, ...) clause via string interpolation makes the query harder to read/maintain and grows linearly with key count. Consider switching to a single-array parameter approach (e.g., a.key = any({application_keys})) or an Anorm-supported list binding helper, which typically reduces SQL string construction and keeps the query shape stable.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +34
val result = await {
client.batchVersionsLatest.post(
org.key,
BatchVersionsLatestForm(applicationKeys = Seq(randomString()))
)
}
result.applications.size must equal(1)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts that latestVersion is None, but it doesn’t assert that the returned applicationKey matches the requested random key. Adding that assertion would make the test more robust (it would catch cases where the endpoint returns a placeholder or mismatched key).

Suggested change
val result = await {
client.batchVersionsLatest.post(
org.key,
BatchVersionsLatestForm(applicationKeys = Seq(randomString()))
)
}
result.applications.size must equal(1)
val nonExistent = randomString()
val result = await {
client.batchVersionsLatest.post(
org.key,
BatchVersionsLatestForm(applicationKeys = Seq(nonExistent))
)
}
result.applications.size must equal(1)
result.applications.head.applicationKey must equal(nonExistent)

Copilot uses AI. Check for mistakes.
@mbryzek mbryzek merged commit b94314c into main Mar 10, 2026
4 of 6 checks passed
@mbryzek mbryzek deleted the batch-version-check branch March 10, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants