Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1f31063
Adding initial PDS registration work
JamesKane Dec 5, 2025
b58b864
Add flow diagram for sync
JamesKane Dec 5, 2025
7126366
Fixing config for new metadata db
JamesKane Dec 5, 2025
6287ba6
feat: Support Atmosphere BGS integration with Citizen/PDS linking
JamesKane Dec 5, 2025
f950171
Update the integration plan and add lexicon draft
JamesKane Dec 5, 2025
0632d90
feat: Implement Upsert logic for external biosample submission
JamesKane Dec 5, 2025
b03cfcb
feat: Implement DELETE endpoint for external biosamples
JamesKane Dec 5, 2025
77ddafb
feat: Enhance security for DELETE biosample endpoint
JamesKane Dec 5, 2025
02e9584
fix: Compilation errors in BGS integration code
JamesKane Dec 5, 2025
9442e0d
Lexicon updates
JamesKane Dec 5, 2025
114e277
Lexicon updates
JamesKane Dec 5, 2025
36aae71
Update API Extension Design to reflect CitizenBiosample segregation s…
JamesKane Dec 6, 2025
75178a8
Add Firehose API Specification section to API Extension Design
JamesKane Dec 6, 2025
56fe22f
Remove detailed DTO definitions from API Extension Design document
JamesKane Dec 6, 2025
bebad49
Implement Firehose API for Citizen Biosamples and Projects
JamesKane Dec 6, 2025
27c1457
Add Tapir endpoint definitions for CitizenBiosamples and Projects
JamesKane Dec 6, 2025
8b123e2
Update Atmosphere_Lexicon to make citizenDid a required field
JamesKane Dec 6, 2025
9fc45c9
Add atUri field to record schema in Atmosphere_Lexicon.md
JamesKane Dec 6, 2025
a9e97d4
Refactor CitizenBiosample to use atUri instead of citizenBiosampleDid
JamesKane Dec 6, 2025
43a5ed5
Refactor API to use atUri for Citizen Biosamples and Projects
JamesKane Dec 6, 2025
34e5b46
Require atUri in Atmosphere Lexicon for biosample and project records
JamesKane Dec 6, 2025
302e2ba
Update haplogroup data structures to use robust JSONB model across al…
JamesKane Dec 6, 2025
4d62b60
Fix haplogroup type mismatch errors
JamesKane Dec 6, 2025
1e91750
Support top-level haplogroup assignments for Citizen Biosamples
JamesKane Dec 6, 2025
e3c0535
Align SpecimenDonor and PDSRegistrationService with Lexicon atUri and…
JamesKane Dec 6, 2025
53067be
Fix SpecimenDonor geocoord parameter in GenomicStudyMappers
JamesKane Dec 6, 2025
965b1d5
Add lease management fields and index to pds_registrations table
JamesKane Dec 6, 2025
5af41d7
Introduce Specimen Donor linkage for Citizen Biosamples
JamesKane Dec 6, 2025
73b00d6
Introduce Specimen Donor linkage for Citizen Biosamples
JamesKane Dec 6, 2025
4984fb4
Refactor CitizenBiosampleService to delegate Firehose event handling …
JamesKane Dec 6, 2025
ed94921
Add test coverage for CitizenBiosampleEventHandler, CitizenBiosampleS…
JamesKane Dec 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions .junie/guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Decoding Us — Development Guidelines (Project‑Specific)

This document captures build, configuration, testing, and development tips that are specific to this repository (Scala 3 + Play 3.x). It assumes an advanced Scala/Play developer.

## Build & Configuration

- Toolchain
- Scala: 3.3.6 (see `build.sbt`)
- Play: sbt plugin `org.playframework:sbt-plugin:3.0.9` (see `project/plugins.sbt`)
- sbt: Use a recent sbt 1.10.x; the repo relies on conventional Play/sbt layout.
- Project settings
- Module: single root project, `enablePlugins(PlayScala)` in `build.sbt`.
- JVM: tested with Temurin JDK 21 (Docker runtime uses `eclipse-temurin:21-jre-jammy`).
- Scalac flag: `-Xmax-inlines 128` is set — keep in mind for heavy inline usages/macros.
- Key library versions (selected)
- play-slick 6.2.0 with Postgres JDBC 42.7.8
- slick‑pg 0.23.1 (+ jts, play-json integrations)
- Tapir 1.11.50 (core, play-server, json-play, swagger-ui-bundle)
- Apache Pekko 1.1.5 (pinned; see Quartz note below)
- pekko‑quartz‑scheduler 1.3.0-pekko-1.1.x
- scalatestplus‑play 7.0.2 (Test)
- Pekko/Quartz pin
- `APACHE_PEKKO_VERSION` is deliberately pinned to 1.1.5 because Quartz requires 1.1.x. Bumping beyond this can cause startup errors. Update Quartz first if you need to lift the pin.

### Application configuration (conf/application.conf)

- Secrets and toggles
- `play.http.secret.key` can be overridden by `APPLICATION_SECRET`.
- Sessions are disabled (`play.http.session.disabled = true`). Re‑enable if/when needed.
- Recaptcha: `recaptcha.enable` (env `ENABLE_RECAPTCHA`), keys from env `RECAPTCHA_SECRET_KEY`, `RECAPTCHA_SITE_KEY`.
- Modules
- Enabled: `modules.BaseModule`, `ServicesModule`, `RecaptchaModule`, `StartupModule`, `ApplicationModule`, `ApiSecurityModule`, and Caffeine cache module (`play.api.cache.caffeine.CaffeineCacheModule`). Startup work is performed by `modules.StartupService` (see `app/modules/StartupModule.scala`).
- Caching
- Caffeine is the cache provider; default cache and an explicit `sitemap` cache are configured.
- Database (play-slick)
- Profile: `slick.jdbc.PostgresProfile$`
- JDBC: `jdbc:postgresql://localhost:5432/decodingus_db`
- Default dev creds: `decodingus_user` / `decodingus_password` (override in prod via env/Secrets Manager).
- Evolutions
- `play.evolutions.autocommit = true` enables automatic application of evolutions. Disable for production and manage via CI/migrations.
- AWS & misc
- AWS region default: `us-east-1`; example secrets path included.
- Contact recipient: override via `CONTACT_RECIPIENT_EMAIL`.
- `biosample.hash.salt` configurable via `BIOSAMPLE_HASH_SALT`.
- Logging
- Pekko loglevel DEBUG by default; consider overriding in production.

### Local run

- With sbt (recommended during development):
- `sbt run` — starts Play dev server on :9000. Ensure Postgres is available if you touch DB‑backed pages or services.
- With Docker (prebuilt stage)
- The `Dockerfile` expects a staged distribution at `target/universal/stage`.
- Build a universal distribution: `sbt stage`
- Build and run image:
- `docker build -t decodingus:local .`
- `docker run -p 9000:9000 --env APPLICATION_SECRET=... decodingus:local`
- You must also provide DB connectivity (e.g., link a Postgres container or env JDBC overrides) for features requiring DB.

### Database quickstart (developer machine)

- Create local Postgres role and database (adjust to your local policy):
- `createuser -P decodingus_user` (password `decodingus_password`), `createdb -O decodingus_user decodingus_db`.
- On first app start with evolutions enabled, Play will apply SQL files from `conf/evolutions/default` in order.
- For tests, prefer isolating DB‑heavy specs using test containers or an in‑memory profile; current suite primarily uses Play functional tests and does not require DB for the simple Home page.

## Testing

- Frameworks
- ScalaTest + scalatestplus‑play. Styles vary by spec; the existing suite uses `PlaySpec` and `GuiceOneAppPerTest`.
- Running tests
- Run full suite: `sbt test`
- Run a single suite: `sbt "testOnly controllers.HomeControllerSpec"`
- By pattern: `sbt "testOnly *HomeControllerSpec"`
- Run a single test by name (when supported by the style): `sbt "testOnly controllers.HomeControllerSpec -- -z \"router\""`
- Application DI in tests
- Prefer DI for controllers/services in Play tests to keep constructor wiring aligned with production. Example from `test/controllers/HomeControllerSpec.scala`:
```scala
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
"HomeController GET" should {
"render the index page from the application" in {
val controller = inject[HomeController]
val home = controller.index().apply(FakeRequest(GET, "/"))
status(home) mustBe OK
contentType(home) mustBe Some("text/html")
contentAsString(home) must include ("Welcome to Play")
}
}
}
```
- Avoid manual `new Controller(...)` unless you supply all constructor dependencies. The controller constructors in this repo often include `Cached` and `SyncCacheApi` which are bound by Play.
- Demo test (validated process)
- A temporary pure unit test was created and executed to validate commands:
```scala
// test/utils/DemoSpec.scala
package utils
import org.scalatest.funsuite.AnyFunSuite
class DemoSpec extends AnyFunSuite {
test("math sanity holds") {
assert(1 + 1 == 2)
}
}
```
- It was run successfully via the test runner; afterward it was removed to avoid leaving extra files in the repo.

### Adding new tests

- Controller tests: use `GuiceOneAppPerTest` or `GuiceOneAppPerSuite` depending on reuse and cost. Use `route(app, FakeRequest(...))` for end‑to‑end route testing.
- Service/utility tests: prefer pure unit tests with ScalaTest `AnyFunSuite` or `AnyFlatSpec` where Play isn’t needed.
- DB‑backed components: consider a separate test configuration/profile and a disposable Postgres schema. If introducing containerized tests, add `testcontainers` and wire a Play `Application` with test DB settings.

## Development Tips (Repo‑specific)

- Tapir
- OpenAPI/Swagger UI bundle is included; the site likely serves docs at `/api/docs` (see `HomeController.sitemap()` for the URL hint).
- Caching
- `HomeController.sitemap()` and `robots()` responses are cached for 24h using Caffeine via `Cached` action. If you change sitemap structure, remember cache keys.
- Startup behaviors
- `StartupService` performs tree initialization by calling `TreeInitializationService.initializeIfNeeded()` asynchronously at app start. Watch logs to understand conditional imports.
- Views & HTMX
- HTMX is available via WebJars. Views are Twirl templates under `app/views`. The landing page content is used by tests to assert presence of "Welcome to Play".
- Security/config
- Replace the default `play.http.secret.key`, recaptcha keys, and salts in any non‑dev environment. Sessions are disabled by default.
- Code style
- Follow existing formatting and idioms in the repo. Keep controllers lean, services injected. Non‑trivial logic belongs in services under `app/services/*`.

## Common Tasks & Commands

- Start dev server: `sbt run`
- Compile: `sbt compile`
- Stage distribution: `sbt stage`
- Run tests: `sbt test`
- Single test: `sbt "testOnly *YourSpec"`

88 changes: 0 additions & 88 deletions GEMINI.md

This file was deleted.

51 changes: 51 additions & 0 deletions app/api/CitizenBiosampleEndpoints.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package api

import models.api.{BiosampleOperationResponse, ExternalBiosampleRequest}
import sttp.tapir.*
import sttp.tapir.generic.auto.*
import sttp.tapir.json.play.*
import java.util.UUID

object CitizenBiosampleEndpoints {

private val createBiosample: PublicEndpoint[ExternalBiosampleRequest, String, BiosampleOperationResponse, Any] = {
endpoint
.post
.in("api" / "external-biosamples")
.in(jsonBody[ExternalBiosampleRequest])
.out(jsonBody[BiosampleOperationResponse])
.errorOut(stringBody)
.description("Creates a new Citizen Biosample with associated metadata and publication links.")
.summary("Create Citizen Biosample")
.tag("Citizen Biosamples")
}

private val updateBiosample: PublicEndpoint[(String, ExternalBiosampleRequest), String, BiosampleOperationResponse, Any] = {
endpoint
.put
.in("api" / "external-biosamples" / path[String]("atUri"))
.in(jsonBody[ExternalBiosampleRequest])
.out(jsonBody[BiosampleOperationResponse])
.errorOut(stringBody)
.description("Updates an existing Citizen Biosample using Optimistic Locking (via atCid).")
.summary("Update Citizen Biosample")
.tag("Citizen Biosamples")
}

private val deleteBiosample: PublicEndpoint[String, String, Unit, Any] = {
endpoint
.delete
.in("api" / "external-biosamples" / path[String]("atUri"))
.out(statusCode(sttp.model.StatusCode.NoContent))
.errorOut(stringBody)
.description("Soft deletes a Citizen Biosample.")
.summary("Delete Citizen Biosample")
.tag("Citizen Biosamples")
}

val all: List[PublicEndpoint[_, _, _, _]] = List(
createBiosample,
updateBiosample,
deleteBiosample
)
}
33 changes: 33 additions & 0 deletions app/api/PDSRegistrationEndpoints.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

import models.PDSRegistration
import play.api.libs.json.{Format, Json}
import sttp.tapir.*
import sttp.tapir.generic.auto.*
import sttp.tapir.json.play.*

// --- DTOs (Data Transfer Objects) ---
case class PdsRegistrationRequest(
did: String,
handle: String,
pdsUrl: String,
rToken: String
)

object PdsRegistrationRequest {
implicit val format: Format[PdsRegistrationRequest] = Json.format[PdsRegistrationRequest]
}

object PDSRegistrationEndpoints {

val registerPdsEndpoint: PublicEndpoint[PdsRegistrationRequest, String, PDSRegistration, Any] =
endpoint.post
.in("api" / "registerPDS")
.name("Register PDS")
.description("Registers a new PDS (Personal Data Server) with the system.")
.in(jsonBody[PdsRegistrationRequest])
.out(jsonBody[PDSRegistration])
.errorOut(stringBody)

val all = List(registerPdsEndpoint)
}
51 changes: 51 additions & 0 deletions app/api/ProjectEndpoints.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package api

import models.api.{ProjectRequest, ProjectResponse}
import sttp.tapir.*
import sttp.tapir.generic.auto.*
import sttp.tapir.json.play.*
import java.util.UUID

object ProjectEndpoints {

private val createProject: PublicEndpoint[ProjectRequest, String, ProjectResponse, Any] = {
endpoint
.post
.in("api" / "projects")
.in(jsonBody[ProjectRequest])
.out(jsonBody[ProjectResponse])
.errorOut(stringBody)
.description("Creates a new Project.")
.summary("Create Project")
.tag("Projects")
}

private val updateProject: PublicEndpoint[(String, ProjectRequest), String, ProjectResponse, Any] = {
endpoint
.put
.in("api" / "projects" / path[String]("atUri"))
.in(jsonBody[ProjectRequest])
.out(jsonBody[ProjectResponse])
.errorOut(stringBody)
.description("Updates an existing Project using Optimistic Locking (via atCid).")
.summary("Update Project")
.tag("Projects")
}

private val deleteProject: PublicEndpoint[String, String, Unit, Any] = {
endpoint
.delete
.in("api" / "projects" / path[String]("atUri"))
.out(statusCode(sttp.model.StatusCode.NoContent))
.errorOut(stringBody)
.description("Soft deletes a Project.")
.summary("Delete Project")
.tag("Projects")
}

val all: List[PublicEndpoint[_, _, _, _]] = List(
createProject,
updateProject,
deleteProject
)
}
3 changes: 2 additions & 1 deletion app/controllers/ApiRouter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class ApiRouter @Inject()(cc: ControllerComponents, configuration: play.api.Conf
SwaggerInterpreter().fromEndpoints[Future](
endpoints = _root_.api.ReferenceEndpoints.all ++ _root_.api.HaplogroupEndpoints.all
++ _root_.api.SampleEndpoints.all ++ _root_.api.CoverageEndpoints.all
++ _root_.api.SequencerEndpoints.all,
++ _root_.api.SequencerEndpoints.all ++ _root_.api.CitizenBiosampleEndpoints.all
++ _root_.api.ProjectEndpoints.all,
info = apiInfo
)

Expand Down
Loading
Loading