init: 클린 아키텍처 백엔드
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# adapter:outbound:persistence-mongo — module rules
|
||||
|
||||
## Registered identity
|
||||
|
||||
- Module ID: `adapter-outbound-persistence-mongo`
|
||||
- Gradle path: `:adapter:outbound:persistence-mongo`
|
||||
- Focused test: `./gradlew :adapter:outbound:persistence-mongo:test --console=plain`
|
||||
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
|
||||
- Registry SSOT: `.harness/project/modules.yaml`.
|
||||
|
||||
Package root: `dev.caskeleton.adapter.outbound.mongo`. Driven (outbound) adapter — **lightweight
|
||||
Spring Data MongoDB scaffolding**. Design rationale lives in [README.md](README.md).
|
||||
|
||||
## Responsibility
|
||||
|
||||
- Demonstrate MongoDB-backed persistence: opt-in Mongo config + a self-contained example document /
|
||||
repository / adapter showing the document↔domain mapping boundary. It does **not** reimplement
|
||||
idempotency / outbox / lock on Mongo (those stay JPA-only).
|
||||
- Opt-in: `MongoPersistenceConfig` re-imports the Mongo auto-configuration (`@ImportAutoConfiguration`)
|
||||
and enables repositories (`@EnableMongoRepositories`, scoped to this package via
|
||||
`basePackageClasses`) only when `ca-skeleton.persistence-mongo.enabled=true` (default off). The
|
||||
connection URI comes from Spring's standard `spring.data.mongodb.uri`.
|
||||
|
||||
## Allowed
|
||||
|
||||
- Project deps: `:application-core`, `:shared-contract` — SSOT is the
|
||||
`adapter-outbound-persistence-mongo` entry in `.harness/project/modules.yaml`; `src/build.gradle`
|
||||
enforces it. No
|
||||
`:domain-core`, no sibling adapters.
|
||||
- External: `org.springframework.boot:spring-boot-starter-data-mongodb` (version via the shared
|
||||
Spring Boot BOM), `spring-boot-configuration-processor` (annotation processor). Test-only:
|
||||
Testcontainers (`testcontainers`, `testcontainers-junit-jupiter`), BOM-managed.
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Inbound adapters, sibling outbound adapters, `app-bootstrap`, `sample-portfolio` (ArchUnit
|
||||
`OUTBOUND_ADAPTERS_*` family rules).
|
||||
- Leaking the `ExampleMongoDocument` type outside the adapter — the adapter maps documents to the
|
||||
module-local `ExampleRecord` at the edge.
|
||||
- Inventing an `application-core` port for the example (scaffolding stays self-contained); adding
|
||||
idempotency/outbox/lock on Mongo.
|
||||
- Fully-qualified inline type references; more than one public top-level type per file.
|
||||
|
||||
## Tests
|
||||
|
||||
`ExampleMongoMapperTest` (pure mapping, no container), `ExampleMongoRepositoryIT` (Testcontainers
|
||||
MongoDB save/find/derived-query, `disabledWithoutDocker`).
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:persistence-mongo:check
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
# adapter:outbound:persistence-mongo — design-decision reference
|
||||
|
||||
MongoDB persistence outbound (driven) adapter — **lightweight scaffolding**. Package root:
|
||||
`dev.caskeleton.adapter.outbound.mongo`. Wires Spring Data MongoDB behind an opt-in
|
||||
`@ConditionalOnProperty` selector and ships a demonstrative document / repository / adapter that
|
||||
shows the document↔domain mapping boundary a fork follows. Mirrors the existing outbound adapters
|
||||
(notification / cache-redis / httpclient / objectstorage / fileserver).
|
||||
|
||||
The allowed/forbidden dependency policy is owned by `src/build.gradle`'s
|
||||
`allowedProjectDependencies['adapter:outbound:persistence-mongo']` (SSOT). Module rules live in
|
||||
[CLAUDE.md](CLAUDE.md); this document records the **design rationale** lifted out of the code
|
||||
comments.
|
||||
|
||||
## Scope — deliberately lightweight
|
||||
|
||||
This module is **scaffolding, not a full persistence implementation**. It demonstrates *how* a fork
|
||||
adds MongoDB-backed storage; it does **not** reimplement idempotency, outbox, or distributed lock on
|
||||
Mongo (those stay JPA-only in `adapter:outbound:persistence-jpa`). There is no `application-core`
|
||||
port here on purpose — the demonstrative example is entirely self-contained inside the adapter
|
||||
package so the skeleton stays decoupled and copy-paste-forkable.
|
||||
|
||||
## Module overview
|
||||
|
||||
An **opt-in** MongoDB module placed behind Spring Data MongoDB:
|
||||
|
||||
- `MongoPersistenceConfig` re-imports the Mongo auto-configuration with `@ImportAutoConfiguration`
|
||||
(`MongoAutoConfiguration`, `DataMongoAutoConfiguration`, `DataMongoRepositoriesAutoConfiguration`)
|
||||
and enables the repositories with `@EnableMongoRepositories(basePackageClasses = …)` scoped to this
|
||||
package — but only when `ca-skeleton.persistence-mongo.enabled=true`. `@ImportAutoConfiguration` is
|
||||
an explicit import unaffected by `spring.autoconfigure.exclude`, so the driver never connects when
|
||||
the module is merely on the classpath. This mirrors ha-tmpl's `MongoPersistenceConfig`.
|
||||
- The connection URI is read from Spring's standard `spring.data.mongodb.uri` (owned by Spring
|
||||
Boot's `MongoProperties`). The module's own `MongoPersistenceProperties`
|
||||
(`ca-skeleton.persistence-mongo.*`) owns only the `enabled` opt-in switch and a demonstrative
|
||||
`database` name.
|
||||
|
||||
Selector: `ca-skeleton.persistence-mongo.enabled=true` (default `false`).
|
||||
|
||||
## The demonstrative example (document↔domain boundary)
|
||||
|
||||
- `ExampleRecord` — a small, self-contained "domain" value (NOT a real domain type, NOT an
|
||||
`application-core` type).
|
||||
- `ExampleMongoDocument` — the `@Document` persistence shape (`@Id`, `@Field` BSON names), kept
|
||||
separate from the domain value exactly like a JPA entity is kept separate from its aggregate.
|
||||
- `ExampleMongoRepository extends MongoRepository<ExampleMongoDocument, String>` — CRUD plus a
|
||||
derived-query method (`findByNameIgnoreCase`) demonstrating Spring Data query derivation.
|
||||
- `ExampleMongoMapper` — a pure, package-private static translator (`toDomain` / `toDocument`),
|
||||
trivially unit-testable without a running MongoDB.
|
||||
- `ExampleMongoRepositoryAdapter` — maps at the edge so the document type never leaks to callers;
|
||||
the exact pattern a fork follows for a real aggregate/port.
|
||||
|
||||
**How a fork replaces this:** swap `ExampleMongoDocument`/`ExampleMongoRepository` for a real
|
||||
document + repository (renaming the collection and fields), map to the fork's real aggregate in
|
||||
`ExampleMongoMapper`, and — if the fork wants a framework-neutral seam — implement an
|
||||
`application-core` port from the adapter. `MongoPersistenceConfig` keeps working unchanged.
|
||||
|
||||
## Tests
|
||||
|
||||
- `ExampleMongoMapperTest` — pure document↔domain mapping round-trip; no MongoDB needed.
|
||||
- `ExampleMongoRepositoryIT` — real save/findById/derived-query round-trip against Testcontainers
|
||||
MongoDB (a core `GenericContainer` running `mongo:7.0`); the repository proxy is built directly
|
||||
with `MongoRepositoryFactory` so no full Spring context is required. Skipped automatically when
|
||||
Docker is unavailable (`@Testcontainers(disabledWithoutDocker = true)`).
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:persistence-mongo:check
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
// Driven adapter: NoSQL persistence scaffolding via Spring Data MongoDB. This is a LIGHTWEIGHT,
|
||||
// opt-in skeleton — it wires the Mongo driver + a demonstrative document/repository/adapter that
|
||||
// shows the document<->domain mapping boundary a fork would follow. It does NOT reimplement
|
||||
// idempotency/outbox/lock on Mongo (those stay JPA-only). Mongo auto-configuration is imported and
|
||||
// the repositories enabled ONLY when ca-skeleton.persistence-mongo.enabled=true
|
||||
// (MongoPersistenceConfig), so the driver never connects when the module is merely on the classpath.
|
||||
//
|
||||
// spring-boot-starter-data-mongodb's version is managed by the Spring Boot BOM (applied to every
|
||||
// module in src/build.gradle), so no module-scoped platform is needed. The Testcontainers MongoDB
|
||||
// integration test uses the BOM-managed Testcontainers, and is skipped when Docker is unavailable.
|
||||
description = 'Outbound adapter: NoSQL persistence scaffolding (Spring Data MongoDB)'
|
||||
|
||||
dependencies {
|
||||
implementation project(':application-core')
|
||||
implementation project(':shared-contract')
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
|
||||
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// test-only: Testcontainers MongoDB integration test for the repository round-trip. Uses the core
|
||||
// GenericContainer (no dedicated module) so the repository save/find runs against a real MongoDB
|
||||
// when Docker is available and is skipped (disabledWithoutDocker) otherwise — mirroring the
|
||||
// object-storage module's MinIO integration test.
|
||||
testImplementation 'org.testcontainers:testcontainers'
|
||||
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,testCompileClasspath
|
||||
ch.qos.logback:logback-classic:1.5.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-core:1.5.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.20=testCompileClasspath,testRuntimeClasspath
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,testAnnotationProcessor
|
||||
com.github.docker-java:docker-java-api:3.7.0=testCompileClasspath,testRuntimeClasspath
|
||||
com.github.docker-java:docker-java-transport-zerodep:3.7.0=testCompileClasspath,testRuntimeClasspath
|
||||
com.github.docker-java:docker-java-transport:3.7.0=testCompileClasspath,testRuntimeClasspath
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,testAnnotationProcessor
|
||||
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
|
||||
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,testCompileClasspath
|
||||
com.github.spotbugs:spotbugs:4.10.2=spotbugs
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,spotbugs,testCompileClasspath
|
||||
com.google.code.gson:gson:2.13.2=spotbugs
|
||||
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,testCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
|
||||
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
|
||||
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.guava:guava:33.5.0-jre=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:guava:33.6.0-jre=checkstyle
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,testAnnotationProcessor
|
||||
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
|
||||
com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath
|
||||
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
|
||||
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath
|
||||
commons-beanutils:commons-beanutils:1.11.0=checkstyle
|
||||
commons-codec:commons-codec:1.19.0=testCompileClasspath,testRuntimeClasspath
|
||||
commons-collections:commons-collections:3.2.2=checkstyle
|
||||
commons-io:commons-io:2.20.0=testCompileClasspath,testRuntimeClasspath
|
||||
commons-io:commons-io:2.21.0=spotbugs
|
||||
commons-logging:commons-logging:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
info.picocli:picocli:4.7.7=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,testAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,testAnnotationProcessor
|
||||
io.micrometer:micrometer-commons:1.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-observation:1.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:2.1.4=testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=testCompileClasspath,testRuntimeClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,testAnnotationProcessor
|
||||
jaxen:jaxen:2.0.0=spotbugs
|
||||
net.bytebuddy:byte-buddy-agent:1.17.8=testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy:1.17.8=testCompileClasspath,testRuntimeClasspath
|
||||
net.java.dev.jna:jna:5.18.1=testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:accessors-smart:2.6.0=testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:json-smart:2.6.0=testCompileClasspath,testRuntimeClasspath
|
||||
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
|
||||
org.antlr:antlr4-runtime:4.13.2=checkstyle
|
||||
org.apache.bcel:bcel:6.12.0=spotbugs
|
||||
org.apache.commons:commons-compress:1.28.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.commons:commons-lang3:3.20.0=checkstyle,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.commons:commons-text:1.15.0=spotbugs
|
||||
org.apache.commons:commons-text:1.3=checkstyle
|
||||
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
|
||||
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
|
||||
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
|
||||
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
|
||||
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.xbean:xbean-reflect:3.7=checkstyle
|
||||
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
|
||||
org.assertj:assertj-core:3.27.6=testCompileClasspath,testRuntimeClasspath
|
||||
org.awaitility:awaitility:4.3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
|
||||
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
|
||||
org.dom4j:dom4j:2.2.0=spotbugs
|
||||
org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:6.0.1=testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-params:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:6.0.1=testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-launcher:6.0.1=testRuntimeClasspath
|
||||
org.junit:junit-bom:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:6.1.0=spotbugs
|
||||
org.mockito:mockito-core:5.20.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-junit-jupiter:5.20.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.mongodb:bson-record-codec:5.6.1=runtimeClasspath,testRuntimeClasspath
|
||||
org.mongodb:bson:5.6.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.mongodb:mongodb-driver-core:5.6.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.mongodb:mongodb-driver-sync:5.6.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.objenesis:objenesis:3.3=testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.resource:1.0.0=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,testCompileClasspath
|
||||
org.ow2.asm:asm-analysis:9.10.1=spotbugs
|
||||
org.ow2.asm:asm-commons:9.10.1=spotbugs
|
||||
org.ow2.asm:asm-tree:9.10.1=spotbugs
|
||||
org.ow2.asm:asm-util:9.10.1=spotbugs
|
||||
org.ow2.asm:asm:9.10.1=spotbugs
|
||||
org.ow2.asm:asm:9.7.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,testAnnotationProcessor
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath
|
||||
org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
|
||||
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-configuration-processor:4.0.0=annotationProcessor
|
||||
org.springframework.boot:spring-boot-data-commons:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-data-mongodb:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-http-client:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-http-converter:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-jackson:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-mongodb:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-persistence:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-restclient:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-resttestclient:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-servlet:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-data-mongodb:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-mongodb:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-tomcat:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-transaction:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-web-server:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-commons:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-mongodb:5.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aop:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-beans:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-context:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-core:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-expression:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-test:7.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-tx:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-web:7.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webmvc:7.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.testcontainers:testcontainers-junit-jupiter:2.0.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.testcontainers:testcontainers:2.0.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
|
||||
org.xmlunit:xmlunit-core:2.10.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-core:3.0.2=testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-databind:3.0.2=testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson:jackson-bom:3.0.2=testCompileClasspath,testRuntimeClasspath
|
||||
empty=
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
/**
|
||||
* MongoDB document model for the demonstrative {@link ExampleRecord}. Kept separate from the
|
||||
* "domain" value exactly like a JPA entity is kept separate from its aggregate — this is the
|
||||
* persistence shape (BSON field names, {@code @Id}), not the domain shape.
|
||||
*
|
||||
* <p>Modelled as a mutable JavaBean because that is the least-surprising shape for Spring Data
|
||||
* MongoDB's mapping (no-arg construct + field population). A fork renames the collection and fields
|
||||
* to match its real document.
|
||||
*/
|
||||
@Document(collection = "ca_skeleton_examples")
|
||||
public class ExampleMongoDocument {
|
||||
|
||||
@Id private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
@Field("qty")
|
||||
private int quantity;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(int quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
/**
|
||||
* Translation between the demonstrative {@link ExampleRecord} "domain" value and its {@link
|
||||
* ExampleMongoDocument} persistence shape. Package-private and static: mapping is a pure function
|
||||
* with no framework dependency, so it is trivially unit-testable without a running MongoDB (see
|
||||
* {@code ExampleMongoMapperTest}).
|
||||
*
|
||||
* <p>This is the boundary a fork keeps: the repository/adapter never leak the document type
|
||||
* outward; callers receive only the domain value.
|
||||
*/
|
||||
final class ExampleMongoMapper {
|
||||
|
||||
private ExampleMongoMapper() {}
|
||||
|
||||
static ExampleRecord toDomain(ExampleMongoDocument document) {
|
||||
return new ExampleRecord(document.getId(), document.getName(), document.getQuantity());
|
||||
}
|
||||
|
||||
static ExampleMongoDocument toDocument(ExampleRecord record) {
|
||||
ExampleMongoDocument document = new ExampleMongoDocument();
|
||||
document.setId(record.id());
|
||||
document.setName(record.name());
|
||||
document.setQuantity(record.quantity());
|
||||
return document;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
/**
|
||||
* Spring Data MongoDB repository for {@link ExampleMongoDocument}. Extending {@link
|
||||
* MongoRepository} supplies the CRUD surface (save / findById / delete / count …); the
|
||||
* derived-query method below demonstrates Spring Data's query derivation. A fork replaces the
|
||||
* document type parameter and the derived queries with its own.
|
||||
*/
|
||||
public interface ExampleMongoRepository extends MongoRepository<ExampleMongoDocument, String> {
|
||||
|
||||
/** Derived query — case-insensitive lookup by the {@code name} field. */
|
||||
List<ExampleMongoDocument> findByNameIgnoreCase(String name);
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Demonstrative repository adapter: the boundary between the Spring Data {@link
|
||||
* ExampleMongoRepository} (documents) and the module-local {@link ExampleRecord} "domain" value.
|
||||
* Every method maps at the edge via {@link ExampleMongoMapper}, so the document type never leaks to
|
||||
* callers — the exact pattern a fork follows for a real aggregate/port.
|
||||
*
|
||||
* <p>Deliberately a plain class (no {@code @Component}); {@link MongoPersistenceConfig} assembles
|
||||
* it as a bean only when the module is opted in, mirroring the object-storage / file-server
|
||||
* adapters.
|
||||
*/
|
||||
public class ExampleMongoRepositoryAdapter {
|
||||
|
||||
private final ExampleMongoRepository repository;
|
||||
|
||||
public ExampleMongoRepositoryAdapter(ExampleMongoRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/** Inserts or updates the document for {@code record} and returns the persisted value. */
|
||||
public ExampleRecord save(ExampleRecord record) {
|
||||
ExampleMongoDocument saved = repository.save(ExampleMongoMapper.toDocument(record));
|
||||
return ExampleMongoMapper.toDomain(saved);
|
||||
}
|
||||
|
||||
/** Reads by id, mapping the document back to the domain value; {@code empty()} when absent. */
|
||||
public Optional<ExampleRecord> findById(String id) {
|
||||
return repository.findById(id).map(ExampleMongoMapper::toDomain);
|
||||
}
|
||||
|
||||
/** Derived-query lookup by name, mapped to domain values. */
|
||||
public List<ExampleRecord> findByName(String name) {
|
||||
return repository.findByNameIgnoreCase(name).stream()
|
||||
.map(ExampleMongoMapper::toDomain)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** Idempotent delete by id. */
|
||||
public void deleteById(String id) {
|
||||
repository.deleteById(id);
|
||||
}
|
||||
|
||||
/** Total document count in the collection. */
|
||||
public long count() {
|
||||
return repository.count();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
/**
|
||||
* A small, self-contained "domain" value used purely to demonstrate the document<->domain
|
||||
* mapping boundary. It is intentionally NOT a real domain type and NOT an {@code application-core}
|
||||
* type — the persistence-mongo module is lightweight scaffolding, so the example lives entirely
|
||||
* inside the adapter package.
|
||||
*
|
||||
* <p>A fork replaces this with its real aggregate (typically owned by {@code domain-core}) and maps
|
||||
* to it in {@link ExampleMongoMapper} exactly as shown here.
|
||||
*/
|
||||
public record ExampleRecord(String id, String name, int quantity) {}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration;
|
||||
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoRepositoriesAutoConfiguration;
|
||||
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||
|
||||
/**
|
||||
* Opt-in wiring for the MongoDB persistence scaffolding, mirroring ha-tmpl's {@code
|
||||
* MongoPersistenceConfig}. The whole configuration — and therefore the Mongo driver connection,
|
||||
* repositories, and the demonstrative adapter bean — activates ONLY when {@code
|
||||
* ca-skeleton.persistence-mongo.enabled=true}.
|
||||
*
|
||||
* <p>{@link ImportAutoConfiguration} is an <em>explicit</em> import that is not affected by {@code
|
||||
* spring.autoconfigure.exclude}, so re-importing the Mongo auto-configuration here cleanly turns
|
||||
* MongoDB on for the opted-in profile without the driver ever connecting when the module is merely
|
||||
* on the classpath. {@link EnableMongoRepositories} is scoped to this package via {@code
|
||||
* basePackageClasses} so repository scanning never reaches beyond the skeleton.
|
||||
*
|
||||
* <p>A fork replaces {@link ExampleMongoRepository}/{@link ExampleMongoDocument} with its real
|
||||
* document + repository and this config keeps working unchanged.
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(
|
||||
prefix = "ca-skeleton.persistence-mongo",
|
||||
name = "enabled",
|
||||
havingValue = "true")
|
||||
@EnableConfigurationProperties(MongoPersistenceProperties.class)
|
||||
@ImportAutoConfiguration({
|
||||
MongoAutoConfiguration.class,
|
||||
DataMongoAutoConfiguration.class,
|
||||
DataMongoRepositoriesAutoConfiguration.class
|
||||
})
|
||||
@EnableMongoRepositories(basePackageClasses = ExampleMongoRepository.class)
|
||||
public class MongoPersistenceConfig {
|
||||
|
||||
/**
|
||||
* Assembles the demonstrative adapter as a bean (the adapter is a plain class), mirroring how the
|
||||
* object-storage / file-server modules assemble their adapters. A fork swaps this for its real
|
||||
* repository adapter.
|
||||
*/
|
||||
@Bean
|
||||
ExampleMongoRepositoryAdapter exampleMongoRepositoryAdapter(ExampleMongoRepository repository) {
|
||||
return new ExampleMongoRepositoryAdapter(repository);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Typed settings for the MongoDB persistence scaffolding, bound from {@code
|
||||
* ca-skeleton.persistence-mongo.*}. Bound as a mutable JavaBean (not a record) so a fork can leave
|
||||
* any subset of fields unset and inherit the defaults below.
|
||||
*
|
||||
* <p>The Mongo <b>connection URI</b> is intentionally NOT modelled here — it is read from Spring's
|
||||
* own standard {@code spring.data.mongodb.uri} (owned by Spring Boot's {@code MongoProperties}),
|
||||
* which keeps credentials/host wiring in the one place operators already expect. This class only
|
||||
* owns the module's own opt-in switch and a demonstrative logical-database name.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "ca-skeleton.persistence-mongo")
|
||||
public class MongoPersistenceProperties {
|
||||
|
||||
/**
|
||||
* Whether to activate the MongoDB scaffolding. Defaults to {@code false} so the driver never
|
||||
* connects when the module is merely present on the classpath; a fork opts in explicitly, and
|
||||
* {@link MongoPersistenceConfig} re-imports the Mongo auto-configuration only then.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* Demonstrative logical database name. This is scaffolding metadata a fork may surface in
|
||||
* diagnostics; the effective database is whatever {@code spring.data.mongodb.uri} (or {@code
|
||||
* spring.data.mongodb.database}) resolves to.
|
||||
*/
|
||||
private String database = "ca_skeleton";
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
public void setDatabase(String database) {
|
||||
this.database = database;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Pure document<->domain mapping contract for {@link ExampleMongoMapper} — no MongoDB needed.
|
||||
*/
|
||||
class ExampleMongoMapperTest {
|
||||
|
||||
@Test
|
||||
void toDocumentCopiesEveryField() {
|
||||
ExampleRecord record = new ExampleRecord("id-1", "widget", 7);
|
||||
|
||||
ExampleMongoDocument document = ExampleMongoMapper.toDocument(record);
|
||||
|
||||
assertThat(document.getId()).isEqualTo("id-1");
|
||||
assertThat(document.getName()).isEqualTo("widget");
|
||||
assertThat(document.getQuantity()).isEqualTo(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toDomainCopiesEveryField() {
|
||||
ExampleMongoDocument document = new ExampleMongoDocument();
|
||||
document.setId("id-2");
|
||||
document.setName("gadget");
|
||||
document.setQuantity(3);
|
||||
|
||||
ExampleRecord record = ExampleMongoMapper.toDomain(document);
|
||||
|
||||
assertThat(record).isEqualTo(new ExampleRecord("id-2", "gadget", 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripThroughDocumentPreservesTheDomainValue() {
|
||||
ExampleRecord original = new ExampleRecord("id-3", "sprocket", 42);
|
||||
|
||||
ExampleRecord roundTripped =
|
||||
ExampleMongoMapper.toDomain(ExampleMongoMapper.toDocument(original));
|
||||
|
||||
assertThat(roundTripped).isEqualTo(original);
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package dev.caskeleton.adapter.outbound.mongo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
|
||||
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
/**
|
||||
* Real MongoDB round-trip for {@link ExampleMongoRepositoryAdapter} against a Testcontainers
|
||||
* MongoDB. The Spring Data repository proxy is built directly with {@link MongoRepositoryFactory}
|
||||
* over a {@link MongoTemplate} — no full Spring context needed, mirroring how the object-storage
|
||||
* MinIO IT constructs its adapter by hand. Skipped automatically when Docker is unavailable ({@code
|
||||
* disabledWithoutDocker = true}); the pure mapping is covered separately by {@link
|
||||
* ExampleMongoMapperTest}.
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class ExampleMongoRepositoryIT {
|
||||
|
||||
private static final int MONGO_PORT = 27017;
|
||||
private static final String DATABASE = "ca_skeleton_it";
|
||||
|
||||
@Container
|
||||
@SuppressWarnings("resource")
|
||||
static final GenericContainer<?> MONGO =
|
||||
new GenericContainer<>(DockerImageName.parse("mongo:7.0"))
|
||||
.withExposedPorts(MONGO_PORT)
|
||||
.waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1));
|
||||
|
||||
private MongoClient client;
|
||||
private ExampleMongoRepositoryAdapter adapter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
String uri = "mongodb://" + MONGO.getHost() + ":" + MONGO.getMappedPort(MONGO_PORT);
|
||||
client = MongoClients.create(uri);
|
||||
MongoTemplate template =
|
||||
new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DATABASE));
|
||||
ExampleMongoRepository repository =
|
||||
new MongoRepositoryFactory(template).getRepository(ExampleMongoRepository.class);
|
||||
repository.deleteAll();
|
||||
adapter = new ExampleMongoRepositoryAdapter(repository);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void savesAndReadsBackThroughTheDomainBoundary() {
|
||||
ExampleRecord saved = adapter.save(new ExampleRecord("it-1", "widget", 5));
|
||||
assertThat(saved).isEqualTo(new ExampleRecord("it-1", "widget", 5));
|
||||
|
||||
Optional<ExampleRecord> found = adapter.findById("it-1");
|
||||
assertThat(found).contains(new ExampleRecord("it-1", "widget", 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByNameUsesTheDerivedQueryCaseInsensitively() {
|
||||
adapter.save(new ExampleRecord("it-2", "Gadget", 1));
|
||||
adapter.save(new ExampleRecord("it-3", "gadget", 2));
|
||||
|
||||
assertThat(adapter.findByName("GADGET"))
|
||||
.extracting(ExampleRecord::id)
|
||||
.containsExactlyInAnyOrder("it-2", "it-3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByIdIsEmptyForAnAbsentId() {
|
||||
assertThat(adapter.findById("absent")).isEmpty();
|
||||
assertThat(adapter.count()).isZero();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user