Skip to content

Commit 4314528

Browse files
authored
refactor: move validation into MongoOperation via TemplatePayload contract (#6)
Replace MongoOperationValidator monolith with per-concern validators invoked through MongoOperation.validate(), aligning with Flamingock's TemplatePayload interface for early load-time validation. - Decompose MongoOperationValidator into focused validators: TypeValidator, CollectionValidator, and per-operation parameter validators (InsertParameters, UpdateParameters, DeleteParameters, CreateIndexParameters, DropIndexParameters, CreateViewParameters, RenameCollectionParameters) - Implement MongoOperation.validate() so the framework validates both apply and rollback payloads at load time, before any change executes - Delete MongoOperationValidator, ValidationError, and MongoTemplateValidationException (no longer needed) - Migrate validator tests to MongoOperationValidateTest targeting the new API
1 parent a69214e commit 4314528

27 files changed

Lines changed: 892 additions & 678 deletions

.claude/settings.local.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
"Bash(javap:*)",
1414
"Bash(TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock ./gradlew test:*)",
1515
"Bash(docker stop:*)",
16-
"Bash(./gradlew clean:*)"
16+
"Bash(./gradlew clean:*)",
17+
"Bash(./gradlew :core:flamingock-core:test:*)",
18+
"Bash(./gradlew :platform-plugins:flamingock-springboot-integration:test:*)",
19+
"WebFetch(domain:flamingock.io)",
20+
"WebFetch(domain:docs.flamingock.io)",
21+
"Bash(wc:*)",
22+
"Bash(./gradlew test:*)",
23+
"Bash(./gradlew spotlessCheck:*)"
1724
]
1825
}
1926
}

ANALYSIS.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ YAML -> MongoOperation (deserialized) -> MongoOperationValidator -> MongoOperati
2828

2929
## 2. Top 10 Issues (Ranked by Severity)
3030

31-
### #1 - CRITICAL: Rollback path skips validation entirely
32-
**File:** `MongoChangeTemplate.java:101-105`
33-
**Impact:** A malformed rollback YAML (wrong type, missing collection, injection via `$` in collection name) executes without any validation check. The apply path validates via `MongoOperationValidator.validate()` at line 93, but the rollback method directly calls `rollbackPayload.getOperator(db).apply(clientSession)` without any validation.
34-
**Risk:** A rollback triggered during a production failure will amplify damage if the rollback YAML itself is malformed. The exact scenario where you need rollback to work perfectly is the scenario where it's least tested.
35-
**Fix:** Call `MongoOperationValidator.validate(rollbackPayload, changeId)` in the `rollback()` method before executing.
31+
### #1 - ~~CRITICAL: Rollback path skips validation entirely~~ RESOLVED
32+
**Status:** Fixed by validation refactoring.
33+
**Original issue:** `MongoChangeTemplate.rollback()` did not call `MongoOperationValidator.validate()` before executing, so malformed rollback YAML ran unchecked.
34+
**Resolution:** Structural validation was moved into `MongoOperation.validate()` (implementing the `TemplatePayload` interface). The Flamingock framework now calls `validate()` on both apply and rollback payloads at load time via `AbstractTemplateLoadedChange.getValidationErrors()`, which invokes `validateApplyPayload()` and `validateRollbackPayload()` before any change executes. The separate `MongoOperationValidator`, `ValidationError`, and `MongoTemplateValidationException` classes were deleted as part of this refactoring. Validation is no longer a responsibility of the template's `apply()`/`rollback()` methods — the framework handles it uniformly for all payloads.
3635

3736
### #2 - HIGH: InsertOperator silently swallows null/empty documents, bypassing validation
3837
**File:** `InsertOperator.java:37-39`

CLAUDE.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**flamingock-java-template-mongodb** is a Flamingock template library that provides declarative, YAML-based MongoDB operations. It enables "no-code" system evolution by allowing developers to define MongoDB changes in YAML files instead of writing Java classes.
8+
9+
This is a **standalone repository** — it is not a module of `flamingock-java`. It depends on Flamingock core artifacts published to Maven Local or Maven Central.
10+
11+
### What is Flamingock?
12+
13+
Flamingock is a Change-as-Code (CaC) platform for audited, synchronized evolution of distributed systems. It applies versioned, auditable changes to external systems (databases, queues, configs, schemas, etc.) at application startup, in lockstep with the application lifecycle.
14+
15+
- **Not** a database migration tool (like Flyway or Liquibase)
16+
- **Not** a CI/CD tool or infra-as-code (like Terraform)
17+
- Changes are called "changes", never "migrations"
18+
19+
### How This Template Fits
20+
21+
Flamingock supports two authoring modes:
22+
1. **Programmatic** — Java classes annotated with `@Change`, `@Apply`, `@Rollback`
23+
2. **Declarative (Templates)** — YAML files processed by template implementations
24+
25+
This module implements option 2 for MongoDB. Users write YAML like:
26+
27+
```yaml
28+
type: insert
29+
collection: users
30+
parameters:
31+
documents:
32+
- { name: "Alice", role: "admin" }
33+
```
34+
35+
The template parses it into `MongoOperation` POJOs, validates them, and executes against a `MongoDatabase`.
36+
37+
## Build System
38+
39+
Standalone Gradle project with Kotlin DSL. Java 8 target.
40+
41+
### Common Commands
42+
43+
```bash
44+
# Build
45+
./gradlew build
46+
47+
# Run all tests (requires Docker for TestContainers MongoDB)
48+
./gradlew test
49+
50+
# Check license headers
51+
./gradlew spotlessCheck
52+
53+
# Fix license headers
54+
./gradlew spotlessApply
55+
56+
# Publish to local Maven repo (for flamingock-java to consume)
57+
./gradlew publishToMavenLocal
58+
```
59+
60+
### Dependencies
61+
62+
- **Flamingock core artifacts** (`flamingock-core-commons`, `flamingock-processor`, etc.) at version defined by `flamingockVersion` in `build.gradle.kts`
63+
- **MongoDB driver** `mongodb-driver-sync:4.0.0` — `compileOnly` (user provides at runtime)
64+
- **TestContainers** for integration tests (requires Docker)
65+
- Resolves from `mavenLocal()` first, then `mavenCentral()`
66+
67+
### Flamingock Core Dependency
68+
69+
When working on this project alongside `flamingock-java`, you must first publish core artifacts locally:
70+
71+
```bash
72+
cd /path/to/flamingock-java
73+
./gradlew publishToMavenLocal
74+
```
75+
76+
Then this project picks them up via `mavenLocal()`. Keep versions in sync between both projects.
77+
78+
## Architecture
79+
80+
### Execution Flow
81+
82+
```
83+
YAML → MongoOperation (deserialized) → MongoOperationValidator → MongoOperationType (enum factory) → MongoOperator subclass → MongoDB Driver
84+
```
85+
86+
### Key Classes
87+
88+
| Class | Role |
89+
|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
90+
| `MongoChangeTemplate` | Entry point. Extends `AbstractChangeTemplate<Void, MongoOperation, MongoOperation>`. Annotated `@ChangeTemplate(name = "mongodb-sync-template", multiStep = true)` |
91+
| `MongoOperation` | POJO model deserialized from YAML. Fields: `type`, `collection`, `parameters` |
92+
| `MongoOperationType` | Enum with 11 operations + factory pattern via `BiFunction` |
93+
| `MongoOperationValidator` | Pre-execution validation. Collects all errors before throwing |
94+
| `MongoOperator` | Abstract base for operators. Template method: `apply()` → `applyInternal()` |
95+
| `ValidationError` | Structured error with entityId/entityType/message |
96+
97+
### Supported Operations (11)
98+
99+
| Operation | Transactional | Key Parameters |
100+
|--------------------|:-------------:|----------------------------------------------------|
101+
| `createCollection` | No | collection name only |
102+
| `dropCollection` | No | collection name only |
103+
| `renameCollection` | No | `target` |
104+
| `modifyCollection` | No | `validator`, `validationLevel`, `validationAction` |
105+
| `createIndex` | No | `keys`, `options` |
106+
| `dropIndex` | No | `indexName` or `keys` |
107+
| `insert` | Yes | `documents`, `options` |
108+
| `update` | Yes | `filter`, `update`, `multi`, `options` |
109+
| `delete` | Yes | `filter` |
110+
| `createView` | No | `viewOn`, `pipeline` |
111+
| `dropView` | No | collection name only |
112+
113+
### Package Structure
114+
115+
```
116+
io.flamingock.template.mongodb
117+
├── MongoChangeTemplate.java # Template entry point
118+
├── mapper/ # YAML Map → MongoDB Options conversion
119+
│ ├── IndexOptionsMapper
120+
│ ├── InsertOptionsMapper
121+
│ ├── UpdateOptionsMapper
122+
│ ├── CreateViewOptionsMapper
123+
│ ├── RenameCollectionOptionsMapper
124+
│ └── MapperUtil # Type-safe extraction utilities
125+
├── model/
126+
│ ├── MongoOperation # YAML payload POJO
127+
│ ├── MongoOperationType # Enum factory (11 types)
128+
│ └── operator/ # 11 MongoOperator subclasses
129+
└── validation/
130+
├── MongoOperationValidator # Comprehensive validation
131+
├── ValidationError # Error model
132+
└── MongoTemplateValidationException # Exception wrapper
133+
```
134+
135+
### TemplatePayload and Two-Phase Validation
136+
137+
`TemplatePayload` is the contract that all APPLY and ROLLBACK generic types in `ChangeTemplate<CONFIG, APPLY, ROLLBACK>` must implement. It has a single method: `validate()` returning `List<TemplatePayloadValidationError>`.
138+
139+
**Motivation — early validation at load time:**
140+
141+
Flamingock's pipeline lifecycle has two distinct phases: **load time** (YAML parsed → typed objects built) and **execution time** (template methods run against real systems). `TemplatePayload.validate()` enables structural validation at load time, so a malformed YAML change at step 50 is caught before steps 1-49 execute.
142+
143+
The framework calls `validate()` on every payload during the loaded-change validation phase (`SimpleTemplateLoadedChange.validateApplyPayload()` / `MultiStepTemplateLoadedChange.validateApplyPayload()`), collecting all errors before any change runs.
144+
145+
**How it works in this project:**
146+
147+
`MongoOperation implements TemplatePayload`. Its `validate()` method is the hook for the framework to validate each operation's structure (type, collection, parameters) at load time.
148+
149+
Currently `validate()` returns an empty list — the existing validation lives in `MongoOperationValidator` and runs at execution time inside `MongoChangeTemplate.apply()`. The next task is to **move structural validation into `MongoOperation.validate()`** so the framework catches errors early. `MongoOperationValidator` may still handle execution-time concerns, but the structural checks (missing type, invalid collection name, missing required parameters) belong in `validate()`.
150+
151+
**Type bound:** `ChangeTemplate` enforces `APPLY_FIELD extends TemplatePayload` at the generic level — you cannot create a template with payload types that don't implement it.
152+
153+
**Built-in implementations in flamingock-java:**
154+
- `TemplateString` — wraps `String` payloads (used by `SqlTemplate`), validates non-null/non-blank
155+
- Test-only stubs that return empty error lists
156+
157+
### SPI Registration
158+
159+
Discovered automatically via Java ServiceLoader:
160+
```
161+
META-INF/services/io.flamingock.api.template.ChangeTemplate
162+
→ io.flamingock.template.mongodb.MongoChangeTemplate
163+
```
164+
165+
## Testing
166+
167+
### Test Categories
168+
169+
| Category | Location | Requires Docker |
170+
|----------|----------|:---:|
171+
| Integration (full pipeline) | `MongoChangeTemplateTest` | Yes |
172+
| Operator tests (1 per operation) | `operations/` | Yes |
173+
| Multi-operation tests | `operations/MultipleOperationsTest` | Yes |
174+
| Validator unit tests (~38) | `validation/MongoOperationValidatorTest` | No |
175+
| Mapper unit tests | `mapper/*Test` | No |
176+
177+
### Test Resources
178+
179+
- `src/test/resources/flamingock/pipeline.yaml` — test pipeline configuration
180+
- Test change YAML files in `src/test/java/io/flamingock/template/mongodb/changes/`
181+
182+
### Running Tests
183+
184+
All integration tests use TestContainers with MongoDB, so **Docker must be running**.
185+
186+
```bash
187+
# All tests
188+
./gradlew test
189+
190+
# Only unit tests (no Docker needed)
191+
./gradlew test --tests "io.flamingock.template.mongodb.validation.*" --tests "io.flamingock.template.mongodb.mapper.*"
192+
193+
# Specific operator test
194+
./gradlew test --tests "io.flamingock.template.mongodb.operations.InsertOperatorTest"
195+
```
196+
197+
## Known Issues
198+
199+
See `ANALYSIS.md` for a detailed quality assessment. Key issues:
200+
201+
1. **Rollback path skips validation**`MongoChangeTemplate.rollback()` doesn't call `MongoOperationValidator.validate()` before executing
202+
2. **InsertOperator silently swallows null/empty documents** — redundant guard bypasses validation
203+
3. **modifyCollection has no parameter validation** — validator returns empty error list
204+
4. **Type-unsafe parameter extraction**`@SuppressWarnings("unchecked")` raw casts in `MongoOperation` getters
205+
5. **CreateIndexOperator claims transactional but ignores session** — misleading behavior
206+
6. **Collation mapping broken for YAML input**`MapperUtil.getCollation()` expects `Collation` object, gets `Map`
207+
208+
## Terminology
209+
210+
- Use "**changes**", not "migrations"
211+
- Use "**system evolution**", not "database migration"
212+
- The template name is `mongodb-sync-template` (as declared in `@ChangeTemplate` annotation)
213+
214+
## License
215+
216+
Apache License 2.0. All source files must include the Flamingock license header. Use `./gradlew spotlessApply` to add missing headers.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ All changes use the `steps` format — a list of operations, each with an `apply
7272
```yaml
7373
id: create-users-collection
7474
transactional: false
75-
template: MongoChangeTemplate
75+
template: mongodb-sync-template
7676
targetSystem:
7777
id: "mongodb"
7878
steps:
@@ -91,7 +91,7 @@ steps:
9191
```yaml
9292
id: seed-users
9393
transactional: true
94-
template: MongoChangeTemplate
94+
template: mongodb-sync-template
9595
targetSystem:
9696
id: "mongodb"
9797
steps:
@@ -122,7 +122,7 @@ Group multiple operations in a single change. If a step fails, the framework aut
122122
```yaml
123123
id: setup-products
124124
transactional: false
125-
template: MongoChangeTemplate
125+
template: mongodb-sync-template
126126
targetSystem:
127127
id: "mongodb"
128128
@@ -184,7 +184,7 @@ author: developer-name
184184
transactional: true
185185
186186
# Required: Template to use
187-
template: MongoChangeTemplate
187+
template: mongodb-sync-template
188188
189189
# Required: Target system configuration
190190
targetSystem:
@@ -333,7 +333,7 @@ A full change file with multiple steps and paired rollbacks:
333333
```yaml
334334
id: setup-orders
335335
transactional: false
336-
template: MongoChangeTemplate
336+
template: mongodb-sync-template
337337
targetSystem:
338338
id: "mongodb"
339339

build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ plugins {
55
}
66

77

8-
val flamingockVersion = "1.1.0-rc.2"
9-
108
group = "io.flamingock"
11-
version = "1.0.0-rc.1"
9+
version = "1.2.0-SNAPSHOT"
10+
11+
val flamingockVersion = "1.2.0-SNAPSHOT"
1212

1313
repositories {
1414
mavenLocal()

src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@
2323
import io.flamingock.api.annotations.Rollback;
2424
import io.flamingock.api.template.AbstractChangeTemplate;
2525
import io.flamingock.template.mongodb.model.MongoOperation;
26-
import io.flamingock.template.mongodb.validation.MongoOperationValidator;
27-
import io.flamingock.template.mongodb.validation.MongoTemplateValidationException;
28-
import io.flamingock.template.mongodb.validation.ValidationError;
2926
import org.slf4j.Logger;
3027
import org.slf4j.LoggerFactory;
3128

32-
import java.util.List;
33-
3429
/**
3530
* MongoDB Change Template for executing declarative MongoDB operations defined in YAML.
3631
*
@@ -45,7 +40,7 @@
4540
* <pre>{@code
4641
* id: create-orders-collection
4742
* transactional: false
48-
* template: MongoChangeTemplate
43+
* template: mongodb-sync-template
4944
* targetSystem:
5045
* id: "mongodb"
5146
* steps:
@@ -89,12 +84,6 @@ public MongoChangeTemplate() {
8984
@Apply
9085
public void apply(MongoDatabase db, @Nullable ClientSession clientSession) {
9186
validateSession(clientSession);
92-
93-
List<ValidationError> errors = MongoOperationValidator.validate(applyPayload, changeId);
94-
if (!errors.isEmpty()) {
95-
throw new MongoTemplateValidationException(errors);
96-
}
97-
9887
applyPayload.getOperator(db).apply(clientSession);
9988
}
10089

0 commit comments

Comments
 (0)