Conversation
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>
There was a problem hiding this comment.
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/latestto the API spec, routes, controller, and service layer. - Implements an efficient PostgreSQL
DISTINCT ONquery 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.
| "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" } | ||
| } | ||
| } | ||
| ] | ||
| }, |
There was a problem hiding this comment.
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.
| 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))) |
There was a problem hiding this comment.
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.
| case Invalid(errors) => Conflict(Json.toJson(Validation.errors(errors))) | |
| case Invalid(errors) => BadRequest(Json.toJson(Validation.errors(errors))) |
| "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" } |
There was a problem hiding this comment.
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.
| { "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)" } |
| 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 |
There was a problem hiding this comment.
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.
| val result = await { | ||
| client.batchVersionsLatest.post( | ||
| org.key, | ||
| BatchVersionsLatestForm(applicationKeys = Seq(randomString())) | ||
| ) | ||
| } | ||
| result.applications.size must equal(1) |
There was a problem hiding this comment.
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).
| 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) |
Summary
POST /:orgKey/batch/versions/latestendpoint that returns the latest version for multiple applications in a single requestDISTINCT ONquery instead of N sequential lookupsTest plan
🤖 Generated with Claude Code