From c2f101b44c3d2c2ebf4b01d9af35dc54e0f18658 Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Fri, 20 Feb 2026 11:30:53 +0000 Subject: [PATCH 1/3] refactor: template name in annotation --- .../template/mongodb/MongoChangeTemplate.java | 2 +- .../mongodb/MongoChangeTemplateTest.java | 20 +++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java b/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java index 97b6229..0538690 100644 --- a/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java +++ b/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java @@ -77,7 +77,7 @@ * * @see MongoOperation */ -@ChangeTemplate(multiStep = true) +@ChangeTemplate( name = "mongodb-sync-template", multiStep = true) public class MongoChangeTemplate extends AbstractChangeTemplate { private static final Logger log = LoggerFactory.getLogger(MongoChangeTemplate.class); diff --git a/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java b/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java index 3bd39af..6c2b51d 100644 --- a/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java +++ b/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java @@ -21,16 +21,12 @@ import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; import io.flamingock.api.annotations.EnableFlamingock; -import io.flamingock.store.mongodb.sync.MongoDBSyncAuditStore; import io.flamingock.internal.common.core.audit.AuditEntry; import io.flamingock.internal.core.builder.FlamingockFactory; +import io.flamingock.store.mongodb.sync.MongoDBSyncAuditStore; import io.flamingock.targetsystem.mongodb.sync.MongoDBSyncTargetSystem; import io.flamingock.template.mongodb.model.MongoOperation; import org.bson.Document; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -41,28 +37,26 @@ import org.testcontainers.utility.DockerImageName; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -@EnableFlamingock(configFile = "flamingock/pipeline.yaml", strictTemplateValidation = false) +@EnableFlamingock(configFile = "flamingock/pipeline.yaml") @Testcontainers class MongoChangeTemplateTest { + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); private static final String DB_NAME = "test"; - - private static MongoClient mongoClient; - private static MongoDatabase mongoDatabase; - - @Container - public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); - @BeforeAll static void beforeAll() { mongoClient = MongoClients.create(MongoClientSettings From 65f64e103111328b9dbe6609d5419a4732d0e4c4 Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Fri, 27 Feb 2026 13:41:12 +0000 Subject: [PATCH 2/3] refactor: validation mongooperation --- .claude/settings.local.json | 9 +- CLAUDE.md | 216 +++++++++ README.md | 10 +- build.gradle.kts | 6 +- .../template/mongodb/MongoChangeTemplate.java | 13 +- .../mongodb/model/MongoOperation.java | 30 +- .../mongodb/model/MongoOperationType.java | 49 +- .../validation/CollectionValidator.java | 46 ++ .../CreateIndexParametersValidator.java | 58 +++ .../CreateViewParametersValidator.java | 55 +++ .../validation/DeleteParametersValidator.java | 39 ++ .../DropIndexParametersValidator.java | 48 ++ .../validation/InsertParametersValidator.java | 66 +++ .../validation/MongoOperationValidator.java | 421 ------------------ .../MongoTemplateValidationException.java | 60 --- .../validation/OperationValidator.java | 30 ++ .../RenameCollectionParametersValidator.java | 46 ++ .../mongodb/validation/TypeValidator.java | 43 ++ .../validation/UpdateParametersValidator.java | 54 +++ .../mongodb/validation/ValidationError.java | 59 --- .../_0001__create_users_collections.yaml | 2 +- .../mongodb/changes/_0002__seed_users.yaml | 2 +- .../changes/_0003__multiple_operations.yaml | 2 +- .../changes/_0004__apply_and_rollback.yaml | 2 +- .../changes/_0005__step_based_change.yaml | 2 +- .../MongoOperationValidateTest.java} | 193 ++++---- 26 files changed, 888 insertions(+), 673 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/CollectionValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/DeleteParametersValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/DropIndexParametersValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java delete mode 100644 src/main/java/io/flamingock/template/mongodb/validation/MongoOperationValidator.java delete mode 100644 src/main/java/io/flamingock/template/mongodb/validation/MongoTemplateValidationException.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/TypeValidator.java create mode 100644 src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java delete mode 100644 src/main/java/io/flamingock/template/mongodb/validation/ValidationError.java rename src/test/java/io/flamingock/template/mongodb/{validation/MongoOperationValidatorTest.java => model/MongoOperationValidateTest.java} (78%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4420e32..ccb3d4e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,14 @@ "Bash(javap:*)", "Bash(TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock ./gradlew test:*)", "Bash(docker stop:*)", - "Bash(./gradlew clean:*)" + "Bash(./gradlew clean:*)", + "Bash(./gradlew :core:flamingock-core:test:*)", + "Bash(./gradlew :platform-plugins:flamingock-springboot-integration:test:*)", + "WebFetch(domain:flamingock.io)", + "WebFetch(domain:docs.flamingock.io)", + "Bash(wc:*)", + "Bash(./gradlew test:*)", + "Bash(./gradlew spotlessCheck:*)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7edbc4f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,216 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**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. + +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. + +### What is Flamingock? + +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. + +- **Not** a database migration tool (like Flyway or Liquibase) +- **Not** a CI/CD tool or infra-as-code (like Terraform) +- Changes are called "changes", never "migrations" + +### How This Template Fits + +Flamingock supports two authoring modes: +1. **Programmatic** — Java classes annotated with `@Change`, `@Apply`, `@Rollback` +2. **Declarative (Templates)** — YAML files processed by template implementations + +This module implements option 2 for MongoDB. Users write YAML like: + +```yaml +type: insert +collection: users +parameters: + documents: + - { name: "Alice", role: "admin" } +``` + +The template parses it into `MongoOperation` POJOs, validates them, and executes against a `MongoDatabase`. + +## Build System + +Standalone Gradle project with Kotlin DSL. Java 8 target. + +### Common Commands + +```bash +# Build +./gradlew build + +# Run all tests (requires Docker for TestContainers MongoDB) +./gradlew test + +# Check license headers +./gradlew spotlessCheck + +# Fix license headers +./gradlew spotlessApply + +# Publish to local Maven repo (for flamingock-java to consume) +./gradlew publishToMavenLocal +``` + +### Dependencies + +- **Flamingock core artifacts** (`flamingock-core-commons`, `flamingock-processor`, etc.) at version defined by `flamingockVersion` in `build.gradle.kts` +- **MongoDB driver** `mongodb-driver-sync:4.0.0` — `compileOnly` (user provides at runtime) +- **TestContainers** for integration tests (requires Docker) +- Resolves from `mavenLocal()` first, then `mavenCentral()` + +### Flamingock Core Dependency + +When working on this project alongside `flamingock-java`, you must first publish core artifacts locally: + +```bash +cd /path/to/flamingock-java +./gradlew publishToMavenLocal +``` + +Then this project picks them up via `mavenLocal()`. Keep versions in sync between both projects. + +## Architecture + +### Execution Flow + +``` +YAML → MongoOperation (deserialized) → MongoOperationValidator → MongoOperationType (enum factory) → MongoOperator subclass → MongoDB Driver +``` + +### Key Classes + +| Class | Role | +|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `MongoChangeTemplate` | Entry point. Extends `AbstractChangeTemplate`. Annotated `@ChangeTemplate(name = "mongodb-sync-template", multiStep = true)` | +| `MongoOperation` | POJO model deserialized from YAML. Fields: `type`, `collection`, `parameters` | +| `MongoOperationType` | Enum with 11 operations + factory pattern via `BiFunction` | +| `MongoOperationValidator` | Pre-execution validation. Collects all errors before throwing | +| `MongoOperator` | Abstract base for operators. Template method: `apply()` → `applyInternal()` | +| `ValidationError` | Structured error with entityId/entityType/message | + +### Supported Operations (11) + +| Operation | Transactional | Key Parameters | +|--------------------|:-------------:|----------------------------------------------------| +| `createCollection` | No | collection name only | +| `dropCollection` | No | collection name only | +| `renameCollection` | No | `target` | +| `modifyCollection` | No | `validator`, `validationLevel`, `validationAction` | +| `createIndex` | No | `keys`, `options` | +| `dropIndex` | No | `indexName` or `keys` | +| `insert` | Yes | `documents`, `options` | +| `update` | Yes | `filter`, `update`, `multi`, `options` | +| `delete` | Yes | `filter` | +| `createView` | No | `viewOn`, `pipeline` | +| `dropView` | No | collection name only | + +### Package Structure + +``` +io.flamingock.template.mongodb +├── MongoChangeTemplate.java # Template entry point +├── mapper/ # YAML Map → MongoDB Options conversion +│ ├── IndexOptionsMapper +│ ├── InsertOptionsMapper +│ ├── UpdateOptionsMapper +│ ├── CreateViewOptionsMapper +│ ├── RenameCollectionOptionsMapper +│ └── MapperUtil # Type-safe extraction utilities +├── model/ +│ ├── MongoOperation # YAML payload POJO +│ ├── MongoOperationType # Enum factory (11 types) +│ └── operator/ # 11 MongoOperator subclasses +└── validation/ + ├── MongoOperationValidator # Comprehensive validation + ├── ValidationError # Error model + └── MongoTemplateValidationException # Exception wrapper +``` + +### TemplatePayload and Two-Phase Validation + +`TemplatePayload` is the contract that all APPLY and ROLLBACK generic types in `ChangeTemplate` must implement. It has a single method: `validate()` returning `List`. + +**Motivation — early validation at load time:** + +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. + +The framework calls `validate()` on every payload during the loaded-change validation phase (`SimpleTemplateLoadedChange.validateApplyPayload()` / `MultiStepTemplateLoadedChange.validateApplyPayload()`), collecting all errors before any change runs. + +**How it works in this project:** + +`MongoOperation implements TemplatePayload`. Its `validate()` method is the hook for the framework to validate each operation's structure (type, collection, parameters) at load time. + +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()`. + +**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. + +**Built-in implementations in flamingock-java:** +- `TemplateString` — wraps `String` payloads (used by `SqlTemplate`), validates non-null/non-blank +- Test-only stubs that return empty error lists + +### SPI Registration + +Discovered automatically via Java ServiceLoader: +``` +META-INF/services/io.flamingock.api.template.ChangeTemplate +→ io.flamingock.template.mongodb.MongoChangeTemplate +``` + +## Testing + +### Test Categories + +| Category | Location | Requires Docker | +|----------|----------|:---:| +| Integration (full pipeline) | `MongoChangeTemplateTest` | Yes | +| Operator tests (1 per operation) | `operations/` | Yes | +| Multi-operation tests | `operations/MultipleOperationsTest` | Yes | +| Validator unit tests (~38) | `validation/MongoOperationValidatorTest` | No | +| Mapper unit tests | `mapper/*Test` | No | + +### Test Resources + +- `src/test/resources/flamingock/pipeline.yaml` — test pipeline configuration +- Test change YAML files in `src/test/java/io/flamingock/template/mongodb/changes/` + +### Running Tests + +All integration tests use TestContainers with MongoDB, so **Docker must be running**. + +```bash +# All tests +./gradlew test + +# Only unit tests (no Docker needed) +./gradlew test --tests "io.flamingock.template.mongodb.validation.*" --tests "io.flamingock.template.mongodb.mapper.*" + +# Specific operator test +./gradlew test --tests "io.flamingock.template.mongodb.operations.InsertOperatorTest" +``` + +## Known Issues + +See `ANALYSIS.md` for a detailed quality assessment. Key issues: + +1. **Rollback path skips validation** — `MongoChangeTemplate.rollback()` doesn't call `MongoOperationValidator.validate()` before executing +2. **InsertOperator silently swallows null/empty documents** — redundant guard bypasses validation +3. **modifyCollection has no parameter validation** — validator returns empty error list +4. **Type-unsafe parameter extraction** — `@SuppressWarnings("unchecked")` raw casts in `MongoOperation` getters +5. **CreateIndexOperator claims transactional but ignores session** — misleading behavior +6. **Collation mapping broken for YAML input** — `MapperUtil.getCollation()` expects `Collation` object, gets `Map` + +## Terminology + +- Use "**changes**", not "migrations" +- Use "**system evolution**", not "database migration" +- The template name is `mongodb-sync-template` (as declared in `@ChangeTemplate` annotation) + +## License + +Apache License 2.0. All source files must include the Flamingock license header. Use `./gradlew spotlessApply` to add missing headers. diff --git a/README.md b/README.md index d5e0bde..805f8f8 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ All changes use the `steps` format — a list of operations, each with an `apply ```yaml id: create-users-collection transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" steps: @@ -91,7 +91,7 @@ steps: ```yaml id: seed-users transactional: true -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" steps: @@ -122,7 +122,7 @@ Group multiple operations in a single change. If a step fails, the framework aut ```yaml id: setup-products transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" @@ -184,7 +184,7 @@ author: developer-name transactional: true # Required: Template to use -template: MongoChangeTemplate +template: mongodb-sync-template # Required: Target system configuration targetSystem: @@ -333,7 +333,7 @@ A full change file with multiple steps and paired rollbacks: ```yaml id: setup-orders transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" diff --git a/build.gradle.kts b/build.gradle.kts index e9ad94b..91f06b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,10 +5,10 @@ plugins { } -val flamingockVersion = "1.1.0-rc.2" - group = "io.flamingock" -version = "1.0.0-rc.1" +version = "1.2.0-SNAPSHOT" + +val flamingockVersion = "1.2.0-SNAPSHOT" repositories { mavenLocal() diff --git a/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java b/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java index 0538690..e8fa6d1 100644 --- a/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java +++ b/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java @@ -23,14 +23,9 @@ import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.template.mongodb.model.MongoOperation; -import io.flamingock.template.mongodb.validation.MongoOperationValidator; -import io.flamingock.template.mongodb.validation.MongoTemplateValidationException; -import io.flamingock.template.mongodb.validation.ValidationError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - /** * MongoDB Change Template for executing declarative MongoDB operations defined in YAML. * @@ -45,7 +40,7 @@ *
{@code
  * id: create-orders-collection
  * transactional: false
- * template: MongoChangeTemplate
+ * template: mongodb-sync-template
  * targetSystem:
  *   id: "mongodb"
  * steps:
@@ -89,12 +84,6 @@ public MongoChangeTemplate() {
     @Apply
     public void apply(MongoDatabase db, @Nullable ClientSession clientSession) {
         validateSession(clientSession);
-
-        List errors = MongoOperationValidator.validate(applyPayload, changeId);
-        if (!errors.isEmpty()) {
-            throw new MongoTemplateValidationException(errors);
-        }
-
         applyPayload.getOperator(db).apply(clientSession);
     }
 
diff --git a/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java b/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java
index 89ad87e..07e25b1 100644
--- a/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java
+++ b/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java
@@ -18,15 +18,20 @@
 import com.mongodb.client.MongoDatabase;
 import io.flamingock.api.annotations.NonLockGuarded;
 import io.flamingock.api.NonLockGuardedType;
+import io.flamingock.api.template.TemplatePayload;
+import io.flamingock.api.template.TemplatePayloadValidationError;
 import io.flamingock.template.mongodb.model.operator.MongoOperator;
+import io.flamingock.template.mongodb.validation.CollectionValidator;
+import io.flamingock.template.mongodb.validation.TypeValidator;
 import org.bson.Document;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
 @NonLockGuarded(NonLockGuardedType.NONE)
-public class MongoOperation {
+public class MongoOperation implements TemplatePayload {
     private String type;
     private String collection;
     private Map parameters;
@@ -115,7 +120,7 @@ public boolean isMulti() {
     }
 
     public MongoOperator getOperator(MongoDatabase db) {
-        return MongoOperationType.getFromValue(getType()).getOperator(db, this);
+        return MongoOperationType.findByTypeOrThrow(getType()).getOperator(db, this);
     }
 
     @Override
@@ -127,4 +132,25 @@ public String toString() {
         sb.append('}');
         return sb.toString();
     }
+
+    private static final TypeValidator TYPE_VALIDATOR = new TypeValidator();
+    private static final CollectionValidator COLLECTION_VALIDATOR = new CollectionValidator();
+
+    @Override
+    public List validate() {
+
+        List typeErrors = TYPE_VALIDATOR.validate(this);
+        List errors = new ArrayList<>(typeErrors);
+        if (!typeErrors.isEmpty()) {
+            return errors;
+        }
+
+        //we know it won't throw any exception because we validate the type through TYPE_VALIDATOR
+        MongoOperationType operationType = MongoOperationType.findByTypeOrThrow(type);
+
+        errors.addAll(COLLECTION_VALIDATOR.validate(this));
+        errors.addAll(operationType.getOperationValidator().validate(this));
+
+        return errors;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java b/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java
index 76ded4a..bdaa314 100644
--- a/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java
+++ b/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java
@@ -28,43 +28,66 @@
 import io.flamingock.template.mongodb.model.operator.MongoOperator;
 import io.flamingock.template.mongodb.model.operator.RenameCollectionOperator;
 import io.flamingock.template.mongodb.model.operator.UpdateOperator;
+import io.flamingock.template.mongodb.validation.CreateIndexParametersValidator;
+import io.flamingock.template.mongodb.validation.CreateViewParametersValidator;
+import io.flamingock.template.mongodb.validation.DeleteParametersValidator;
+import io.flamingock.template.mongodb.validation.DropIndexParametersValidator;
+import io.flamingock.template.mongodb.validation.InsertParametersValidator;
+import io.flamingock.template.mongodb.validation.OperationValidator;
+import io.flamingock.template.mongodb.validation.RenameCollectionParametersValidator;
+import io.flamingock.template.mongodb.validation.UpdateParametersValidator;
 
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.function.BiFunction;
 
 public enum MongoOperationType {
 
-    CREATE_COLLECTION("createCollection", CreateCollectionOperator::new),
-    CREATE_INDEX("createIndex", CreateIndexOperator::new),
-    INSERT("insert", InsertOperator::new),
-    UPDATE("update", UpdateOperator::new),
-    DELETE("delete", DeleteOperator::new),
-    DROP_COLLECTION("dropCollection", DropCollectionOperator::new),
-    DROP_INDEX("dropIndex", DropIndexOperator::new),
-    RENAME_COLLECTION("renameCollection", RenameCollectionOperator::new),
-    MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new),
-    CREATE_VIEW("createView", CreateViewOperator::new),
-    DROP_VIEW("dropView", DropViewOperator::new);
+    CREATE_COLLECTION("createCollection", CreateCollectionOperator::new, OperationValidator.NO_OP),
+    CREATE_INDEX("createIndex", CreateIndexOperator::new, new CreateIndexParametersValidator()),
+    INSERT("insert", InsertOperator::new, new InsertParametersValidator()),
+    UPDATE("update", UpdateOperator::new, new UpdateParametersValidator()),
+    DELETE("delete", DeleteOperator::new, new DeleteParametersValidator()),
+    DROP_COLLECTION("dropCollection", DropCollectionOperator::new, OperationValidator.NO_OP),
+    DROP_INDEX("dropIndex", DropIndexOperator::new, new DropIndexParametersValidator()),
+    RENAME_COLLECTION("renameCollection", RenameCollectionOperator::new, new RenameCollectionParametersValidator()),
+    MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new, OperationValidator.NO_OP),
+    CREATE_VIEW("createView", CreateViewOperator::new, new CreateViewParametersValidator()),
+    DROP_VIEW("dropView", DropViewOperator::new, OperationValidator.NO_OP);
 
     private final String value;
     private final BiFunction createOperatorFunction;
+    private final OperationValidator operationValidator;
 
-    MongoOperationType(String value, BiFunction createOperatorFunction) {
+    MongoOperationType(String value,
+                       BiFunction createOperatorFunction,
+                       OperationValidator operationValidator) {
         this.value = value;
         this.createOperatorFunction = createOperatorFunction;
+        this.operationValidator = operationValidator;
     }
 
-    public static MongoOperationType getFromValue(String typeValue) {
+    public static MongoOperationType findByTypeOrThrow(String typeValue) {
         return Arrays.stream(MongoOperationType.values())
                 .filter(type -> type.matches(typeValue))
                 .findFirst()
                 .orElseThrow(() -> new IllegalArgumentException("MongoOperation not supported: " + typeValue));
     }
 
+    public static Optional findByType(String typeValue) {
+        return Arrays.stream(MongoOperationType.values())
+                .filter(type -> type.matches(typeValue))
+                .findFirst();
+    }
+
     public MongoOperator getOperator(MongoDatabase mongoDatabase, MongoOperation operation) {
         return createOperatorFunction.apply(mongoDatabase, operation);
     }
 
+    public OperationValidator getOperationValidator() {
+        return operationValidator;
+    }
+
     private boolean matches(String operationType) {
         return this.value.equals(operationType);
     }
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/CollectionValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/CollectionValidator.java
new file mode 100644
index 0000000..912058e
--- /dev/null
+++ b/src/main/java/io/flamingock/template/mongodb/validation/CollectionValidator.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.template.mongodb.validation;
+
+import io.flamingock.api.template.TemplatePayloadValidationError;
+import io.flamingock.template.mongodb.model.MongoOperation;
+
+import java.util.Collections;
+import java.util.List;
+
+public class CollectionValidator implements OperationValidator {
+
+    @Override
+    public List validate(MongoOperation operation) {
+        String collection = operation.getCollection();
+
+        if (collection == null) {
+            return Collections.singletonList(
+                    new TemplatePayloadValidationError("collection", "Collection name is required"));
+        } else if (collection.trim().isEmpty()) {
+            return Collections.singletonList(
+                    new TemplatePayloadValidationError("collection", "Collection name cannot be empty"));
+        } else if (collection.contains("$")) {
+            return Collections.singletonList(
+                    new TemplatePayloadValidationError("collection", "Collection name cannot contain '$': " + collection));
+        } else if (collection.contains("\0")) {
+            return Collections.singletonList(
+                    new TemplatePayloadValidationError("collection", "Collection name cannot contain null character"));
+        }
+
+        return Collections.emptyList();
+    }
+}
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java
new file mode 100644
index 0000000..0f986a6
--- /dev/null
+++ b/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.template.mongodb.validation;
+
+import io.flamingock.api.template.TemplatePayloadValidationError;
+import io.flamingock.template.mongodb.model.MongoOperation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class CreateIndexParametersValidator implements OperationValidator {
+
+    @Override
+    public List validate(MongoOperation operation) {
+        List errors = new ArrayList<>();
+        Map params = operation.getParameters();
+
+        if (params == null) {
+            errors.add(new TemplatePayloadValidationError("parameters",
+                    "CreateIndex operation requires 'parameters' with 'keys'"));
+            return errors;
+        }
+
+        Object keys = params.get("keys");
+        if (keys == null) {
+            errors.add(new TemplatePayloadValidationError("parameters.keys",
+                    "CreateIndex operation requires 'keys' parameter"));
+            return errors;
+        }
+
+        if (!(keys instanceof Map)) {
+            errors.add(new TemplatePayloadValidationError("parameters.keys",
+                    "'keys' must be a map"));
+            return errors;
+        }
+
+        if (((Map) keys).isEmpty()) {
+            errors.add(new TemplatePayloadValidationError("parameters.keys",
+                    "'keys' cannot be empty"));
+        }
+
+        return errors;
+    }
+}
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java
new file mode 100644
index 0000000..26855c3
--- /dev/null
+++ b/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.template.mongodb.validation;
+
+import io.flamingock.api.template.TemplatePayloadValidationError;
+import io.flamingock.template.mongodb.model.MongoOperation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class CreateViewParametersValidator implements OperationValidator {
+
+    @Override
+    public List validate(MongoOperation operation) {
+        List errors = new ArrayList<>();
+        Map params = operation.getParameters();
+
+        if (params == null) {
+            errors.add(new TemplatePayloadValidationError("parameters",
+                    "CreateView operation requires 'parameters' with 'viewOn' and 'pipeline'"));
+            return errors;
+        }
+
+        Object viewOn = params.get("viewOn");
+        if (viewOn == null || (viewOn instanceof String && ((String) viewOn).trim().isEmpty())) {
+            errors.add(new TemplatePayloadValidationError("parameters.viewOn",
+                    "CreateView operation requires 'viewOn' parameter"));
+        }
+
+        Object pipeline = params.get("pipeline");
+        if (pipeline == null) {
+            errors.add(new TemplatePayloadValidationError("parameters.pipeline",
+                    "CreateView operation requires 'pipeline' parameter"));
+        } else if (!(pipeline instanceof List)) {
+            errors.add(new TemplatePayloadValidationError("parameters.pipeline",
+                    "'pipeline' must be a list"));
+        }
+
+        return errors;
+    }
+}
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/DeleteParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/DeleteParametersValidator.java
new file mode 100644
index 0000000..1a45e7a
--- /dev/null
+++ b/src/main/java/io/flamingock/template/mongodb/validation/DeleteParametersValidator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.template.mongodb.validation;
+
+import io.flamingock.api.template.TemplatePayloadValidationError;
+import io.flamingock.template.mongodb.model.MongoOperation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class DeleteParametersValidator implements OperationValidator {
+
+    @Override
+    public List validate(MongoOperation operation) {
+        List errors = new ArrayList<>();
+        Map params = operation.getParameters();
+
+        if (params == null || !params.containsKey("filter")) {
+            errors.add(new TemplatePayloadValidationError("parameters.filter",
+                    "Delete operation requires 'filter' parameter"));
+        }
+
+        return errors;
+    }
+}
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/DropIndexParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/DropIndexParametersValidator.java
new file mode 100644
index 0000000..11442e0
--- /dev/null
+++ b/src/main/java/io/flamingock/template/mongodb/validation/DropIndexParametersValidator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.template.mongodb.validation;
+
+import io.flamingock.api.template.TemplatePayloadValidationError;
+import io.flamingock.template.mongodb.model.MongoOperation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class DropIndexParametersValidator implements OperationValidator {
+
+    @Override
+    public List validate(MongoOperation operation) {
+        List errors = new ArrayList<>();
+        Map params = operation.getParameters();
+
+        if (params == null) {
+            errors.add(new TemplatePayloadValidationError("parameters",
+                    "DropIndex operation requires 'parameters' with 'indexName' or 'keys'"));
+            return errors;
+        }
+
+        Object indexName = params.get("indexName");
+        Object keys = params.get("keys");
+
+        if (indexName == null && keys == null) {
+            errors.add(new TemplatePayloadValidationError("parameters",
+                    "DropIndex operation requires either 'indexName' or 'keys' parameter"));
+        }
+
+        return errors;
+    }
+}
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java
new file mode 100644
index 0000000..aceb282
--- /dev/null
+++ b/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.template.mongodb.validation;
+
+import io.flamingock.api.template.TemplatePayloadValidationError;
+import io.flamingock.template.mongodb.model.MongoOperation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class InsertParametersValidator implements OperationValidator {
+
+    @Override
+    public List validate(MongoOperation operation) {
+        List errors = new ArrayList<>();
+        Map params = operation.getParameters();
+
+        if (params == null) {
+            errors.add(new TemplatePayloadValidationError("parameters",
+                    "Insert operation requires 'parameters' with 'documents'"));
+            return errors;
+        }
+
+        Object docs = params.get("documents");
+        if (docs == null) {
+            errors.add(new TemplatePayloadValidationError("parameters.documents",
+                    "Insert operation requires 'documents' parameter"));
+            return errors;
+        }
+
+        if (!(docs instanceof List)) {
+            errors.add(new TemplatePayloadValidationError("parameters.documents",
+                    "'documents' must be a list"));
+            return errors;
+        }
+
+        List docList = (List) docs;
+        if (docList.isEmpty()) {
+            errors.add(new TemplatePayloadValidationError("parameters.documents",
+                    "'documents' cannot be empty"));
+        }
+
+        for (int i = 0; i < docList.size(); i++) {
+            if (docList.get(i) == null) {
+                errors.add(new TemplatePayloadValidationError("parameters.documents[" + i + "]",
+                        "Document at index " + i + " is null"));
+            }
+        }
+
+        return errors;
+    }
+}
diff --git a/src/main/java/io/flamingock/template/mongodb/validation/MongoOperationValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/MongoOperationValidator.java
deleted file mode 100644
index 81599d5..0000000
--- a/src/main/java/io/flamingock/template/mongodb/validation/MongoOperationValidator.java
+++ /dev/null
@@ -1,421 +0,0 @@
-/*
- * Copyright 2025 Flamingock (https://www.flamingock.io)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.flamingock.template.mongodb.validation;
-
-import io.flamingock.template.mongodb.model.MongoOperation;
-import io.flamingock.template.mongodb.model.MongoOperationType;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Validates MongoDB template operations before execution.
- *
- * 

This validator checks for required fields, valid parameter types, - * and operation-specific constraints. All errors are collected and returned - * together rather than failing on the first error.

- * - *

Common Validations (All Operations)

- *
    - *
  • type - Required, must be a known operation type from {@link MongoOperationType}
  • - *
  • collection - Required, cannot be empty, cannot contain '$' or null character
  • - *
- * - *

Operation-Specific Validations

- * - *

createCollection

- *

Only requires common validations (collection name).

- *
{@code
- * - type: createCollection
- *   collection: users
- * }
- * - *

dropCollection

- *

Only requires common validations (collection name).

- *
{@code
- * - type: dropCollection
- *   collection: users
- * }
- * - *

insert

- *

Requires {@code parameters.documents} as a non-empty list with no null elements.

- *
{@code
- * - type: insert
- *   collection: users
- *   parameters:
- *     documents:
- *       - name: "John"
- *         email: "john@example.com"
- * }
- * - *

update

- *

Requires {@code parameters.filter} and {@code parameters.update}. Optionally supports {@code parameters.multi} - * (boolean, default false) to update multiple documents.

- *
{@code
- * - type: update
- *   collection: users
- *   parameters:
- *     filter:
- *       status: "inactive"
- *     update:
- *       $set:
- *         status: "archived"
- *     multi: true
- * }
- * - *

delete

- *

Requires {@code parameters.filter}. Filter can be empty ({@code {}}) to delete all documents.

- *
{@code
- * - type: delete
- *   collection: users
- *   parameters:
- *     filter:
- *       status: "inactive"
- * }
- * - *

createIndex

- *

Requires {@code parameters.keys} as a non-empty map defining the index fields.

- *
{@code
- * - type: createIndex
- *   collection: users
- *   parameters:
- *     keys:
- *       email: 1
- *     options:
- *       unique: true
- * }
- * - *

dropIndex

- *

Requires either {@code parameters.indexName} OR {@code parameters.keys} (at least one).

- *
{@code
- * - type: dropIndex
- *   collection: users
- *   parameters:
- *     indexName: "email_1"
- * }
- * - *

renameCollection

- *

Requires {@code parameters.target} as a non-empty string for the new collection name.

- *
{@code
- * - type: renameCollection
- *   collection: old_users
- *   parameters:
- *     target: users_archive
- * }
- * - *

createView

- *

Requires {@code parameters.viewOn} (source collection) and {@code parameters.pipeline} (aggregation pipeline as list).

- *
{@code
- * - type: createView
- *   collection: active_users_view
- *   parameters:
- *     viewOn: users
- *     pipeline:
- *       - $match:
- *           status: "active"
- * }
- * - *

dropView

- *

Only requires common validations (collection/view name).

- *
{@code
- * - type: dropView
- *   collection: active_users_view
- * }
- * - *

modifyCollection

- *

Only requires common validations. Used for modifying collection options like validation rules.

- *
{@code
- * - type: modifyCollection
- *   collection: users
- *   parameters:
- *     validator:
- *       $jsonSchema:
- *         required: ["email"]
- * }
- * - * @see MongoOperation - * @see MongoOperationType - * @see ValidationError - */ -public final class MongoOperationValidator { - - private static final String MONGO_OPERATION = "MongoOperation"; - - private MongoOperationValidator() { - } - - /** - * Validates a MongoDB operation and returns all validation errors. - * - * @param operation the operation to validate - * @param entityId the identifier for error reporting (e.g., "changeId.operations[0]") - * @return list of validation errors (empty if valid) - */ - public static List validate(MongoOperation operation, String entityId) { - List errors = new ArrayList<>(); - - if (operation == null) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, "Operation cannot be null")); - return errors; - } - - // Operation type - String typeValue = operation.getType(); - if (typeValue == null || typeValue.trim().isEmpty()) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, "Operation type is required")); - return errors; - } - - MongoOperationType type; - try { - type = MongoOperationType.getFromValue(typeValue); - } catch (IllegalArgumentException e) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, - "Unknown operation type: " + typeValue)); - return errors; // Can't continue with unknown type - } - - // 2. Collection name - errors.addAll(validateCollectionName(operation.getCollection(), entityId)); - - // 3. Type-specific - errors.addAll(validateByType(type, operation, entityId)); - - return errors; - } - - private static List validateCollectionName(String collection, String entityId) { - List errors = new ArrayList<>(); - - if (collection == null) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, - "Collection name is required")); - } else if (collection.trim().isEmpty()) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, - "Collection name cannot be empty")); - } else if (collection.contains("$")) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, - "Collection name cannot contain '$': " + collection)); - } else if (collection.contains("\0")) { - errors.add(new ValidationError(entityId, MONGO_OPERATION, - "Collection name cannot contain null character")); - } - - return errors; - } - - private static List validateByType(MongoOperationType type, - MongoOperation op, - String entityId) { - switch (type) { - case INSERT: - return validateInsert(op, entityId); - case UPDATE: - return validateUpdate(op, entityId); - case DELETE: - return validateDelete(op, entityId); - case CREATE_INDEX: - return validateCreateIndex(op, entityId); - case DROP_INDEX: - return validateDropIndex(op, entityId); - case RENAME_COLLECTION: - return validateRenameCollection(op, entityId); - case CREATE_VIEW: - return validateCreateView(op, entityId); - default: - return new ArrayList<>(); - } - } - - private static List validateInsert(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null) { - errors.add(new ValidationError(entityId, "InsertOperation", - "Insert operation requires 'parameters' with 'documents'")); - return errors; - } - - Object docs = params.get("documents"); - if (docs == null) { - errors.add(new ValidationError(entityId, "InsertOperation", - "Insert operation requires 'documents' parameter")); - return errors; - } - - if (!(docs instanceof List)) { - errors.add(new ValidationError(entityId, "InsertOperation", - "'documents' must be a list")); - return errors; - } - - List docList = (List) docs; - if (docList.isEmpty()) { - errors.add(new ValidationError(entityId, "InsertOperation", - "'documents' cannot be empty")); - } - - for (int i = 0; i < docList.size(); i++) { - if (docList.get(i) == null) { - errors.add(new ValidationError(entityId, "InsertOperation", - "Document at index " + i + " is null")); - } - } - - return errors; - } - - private static List validateUpdate(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null) { - errors.add(new ValidationError(entityId, "UpdateOperation", - "Update operation requires 'parameters' with 'filter' and 'update'")); - return errors; - } - - if (!params.containsKey("filter")) { - errors.add(new ValidationError(entityId, "UpdateOperation", - "Update operation requires 'filter' parameter")); - } - - Object update = params.get("update"); - if (update == null) { - errors.add(new ValidationError(entityId, "UpdateOperation", - "Update operation requires 'update' parameter")); - } else if (!(update instanceof Map)) { - errors.add(new ValidationError(entityId, "UpdateOperation", - "'update' must be a document")); - } - - return errors; - } - - private static List validateDelete(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null || !params.containsKey("filter")) { - errors.add(new ValidationError(entityId, "DeleteOperation", - "Delete operation requires 'filter' parameter")); - } - - return errors; - } - - private static List validateCreateIndex(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null) { - errors.add(new ValidationError(entityId, "CreateIndexOperation", - "CreateIndex operation requires 'parameters' with 'keys'")); - return errors; - } - - Object keys = params.get("keys"); - if (keys == null) { - errors.add(new ValidationError(entityId, "CreateIndexOperation", - "CreateIndex operation requires 'keys' parameter")); - return errors; - } - - if (!(keys instanceof Map)) { - errors.add(new ValidationError(entityId, "CreateIndexOperation", - "'keys' must be a map")); - return errors; - } - - if (((Map) keys).isEmpty()) { - errors.add(new ValidationError(entityId, "CreateIndexOperation", - "'keys' cannot be empty")); - } - - return errors; - } - - private static List validateDropIndex(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null) { - errors.add(new ValidationError(entityId, "DropIndexOperation", - "DropIndex operation requires 'parameters' with 'indexName' or 'keys'")); - return errors; - } - - Object indexName = params.get("indexName"); - Object keys = params.get("keys"); - - if (indexName == null && keys == null) { - errors.add(new ValidationError(entityId, "DropIndexOperation", - "DropIndex operation requires either 'indexName' or 'keys' parameter")); - } - - return errors; - } - - private static List validateRenameCollection(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null || !params.containsKey("target")) { - errors.add(new ValidationError(entityId, "RenameCollectionOperation", - "RenameCollection operation requires 'target' parameter")); - return errors; - } - - Object target = params.get("target"); - if (target == null || (target instanceof String && ((String) target).trim().isEmpty())) { - errors.add(new ValidationError(entityId, "RenameCollectionOperation", - "'target' cannot be null or empty")); - } - - return errors; - } - - private static List validateCreateView(MongoOperation op, String entityId) { - List errors = new ArrayList<>(); - Map params = op.getParameters(); - - if (params == null) { - errors.add(new ValidationError(entityId, "CreateViewOperation", - "CreateView operation requires 'parameters' with 'viewOn' and 'pipeline'")); - return errors; - } - - Object viewOn = params.get("viewOn"); - if (viewOn == null || (viewOn instanceof String && ((String) viewOn).trim().isEmpty())) { - errors.add(new ValidationError(entityId, "CreateViewOperation", - "CreateView operation requires 'viewOn' parameter")); - } - - Object pipeline = params.get("pipeline"); - if (pipeline == null) { - errors.add(new ValidationError(entityId, "CreateViewOperation", - "CreateView operation requires 'pipeline' parameter")); - } else if (!(pipeline instanceof List)) { - errors.add(new ValidationError(entityId, "CreateViewOperation", - "'pipeline' must be a list")); - } - - return errors; - } - -} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/MongoTemplateValidationException.java b/src/main/java/io/flamingock/template/mongodb/validation/MongoTemplateValidationException.java deleted file mode 100644 index 8dd04ff..0000000 --- a/src/main/java/io/flamingock/template/mongodb/validation/MongoTemplateValidationException.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.template.mongodb.validation; - -import java.util.Collections; -import java.util.List; - -/** - * Exception thrown when MongoDB template validation fails. - * - *

This exception collects all validation errors found during validation - * and provides a formatted message listing all issues.

- */ -public class MongoTemplateValidationException extends RuntimeException { - - private final List errors; - - /** - * Creates a new validation exception with the given errors. - * - * @param errors the list of validation errors - */ - public MongoTemplateValidationException(List errors) { - super(formatMessage(errors)); - this.errors = Collections.unmodifiableList(errors); - } - - /** - * Returns the list of validation errors. - * - * @return unmodifiable list of validation errors - */ - public List getErrors() { - return errors; - } - - private static String formatMessage(List errors) { - StringBuilder sb = new StringBuilder("MongoDB template validation failed with ") - .append(errors.size()) - .append(" error(s):\n"); - for (ValidationError error : errors) { - sb.append(" - [").append(error.getEntityId()) - .append("] ").append(error.getMessage()).append("\n"); - } - return sb.toString(); - } -} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java new file mode 100644 index 0000000..a8acffe --- /dev/null +++ b/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.mongodb.validation; + +import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.model.MongoOperation; + +import java.util.Collections; +import java.util.List; + +@FunctionalInterface +public interface OperationValidator { + + OperationValidator NO_OP = operation -> Collections.emptyList(); + + List validate(MongoOperation operation); +} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java new file mode 100644 index 0000000..fc72483 --- /dev/null +++ b/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.mongodb.validation; + +import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.model.MongoOperation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RenameCollectionParametersValidator implements OperationValidator { + + @Override + public List validate(MongoOperation operation) { + List errors = new ArrayList<>(); + Map params = operation.getParameters(); + + if (params == null || !params.containsKey("target")) { + errors.add(new TemplatePayloadValidationError("parameters.target", + "RenameCollection operation requires 'target' parameter")); + return errors; + } + + Object target = params.get("target"); + if (target == null || (target instanceof String && ((String) target).trim().isEmpty())) { + errors.add(new TemplatePayloadValidationError("parameters.target", + "'target' cannot be null or empty")); + } + + return errors; + } +} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/TypeValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/TypeValidator.java new file mode 100644 index 0000000..196f602 --- /dev/null +++ b/src/main/java/io/flamingock/template/mongodb/validation/TypeValidator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.mongodb.validation; + +import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.MongoOperationType; + +import java.util.Collections; +import java.util.List; + +public class TypeValidator implements OperationValidator { + + @Override + public List validate(MongoOperation operation) { + String type = operation.getType(); + + if (type == null || type.trim().isEmpty()) { + return Collections.singletonList( + new TemplatePayloadValidationError("type", "Operation type is required")); + } + + if (!MongoOperationType.findByType(type).isPresent()) { + return Collections.singletonList( + new TemplatePayloadValidationError("type", "Unknown operation type: " + type)); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java new file mode 100644 index 0000000..5d5a6c1 --- /dev/null +++ b/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.mongodb.validation; + +import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.model.MongoOperation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class UpdateParametersValidator implements OperationValidator { + + @Override + public List validate(MongoOperation operation) { + List errors = new ArrayList<>(); + Map params = operation.getParameters(); + + if (params == null) { + errors.add(new TemplatePayloadValidationError("parameters", + "Update operation requires 'parameters' with 'filter' and 'update'")); + return errors; + } + + if (!params.containsKey("filter")) { + errors.add(new TemplatePayloadValidationError("parameters.filter", + "Update operation requires 'filter' parameter")); + } + + Object update = params.get("update"); + if (update == null) { + errors.add(new TemplatePayloadValidationError("parameters.update", + "Update operation requires 'update' parameter")); + } else if (!(update instanceof Map)) { + errors.add(new TemplatePayloadValidationError("parameters.update", + "'update' must be a document")); + } + + return errors; + } +} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/ValidationError.java b/src/main/java/io/flamingock/template/mongodb/validation/ValidationError.java deleted file mode 100644 index a04a3b3..0000000 --- a/src/main/java/io/flamingock/template/mongodb/validation/ValidationError.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.template.mongodb.validation; - -/** - * Represents a validation error for MongoDB template operations. - * - *

Each error contains context about where the validation failed - * (entityId, entityType) and a human-readable error message.

- */ -public class ValidationError { - - private final String entityId; - private final String entityType; - private final String message; - - /** - * Creates a new validation error. - * - * @param entityId the identifier of the entity that failed validation (e.g., "changeId.operations[0]") - * @param entityType the type of entity (e.g., "MongoOperation", "InsertOperation") - * @param message the human-readable error message - */ - public ValidationError(String entityId, String entityType, String message) { - this.entityId = entityId; - this.entityType = entityType; - this.message = message; - } - - public String getEntityId() { - return entityId; - } - - public String getEntityType() { - return entityType; - } - - public String getMessage() { - return message; - } - - @Override - public String toString() { - return String.format("[%s] %s: %s", entityId, entityType, message); - } -} diff --git a/src/test/java/io/flamingock/template/mongodb/changes/_0001__create_users_collections.yaml b/src/test/java/io/flamingock/template/mongodb/changes/_0001__create_users_collections.yaml index cba0016..4789a8f 100644 --- a/src/test/java/io/flamingock/template/mongodb/changes/_0001__create_users_collections.yaml +++ b/src/test/java/io/flamingock/template/mongodb/changes/_0001__create_users_collections.yaml @@ -1,6 +1,6 @@ id: create-users-collection-with-index transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" steps: diff --git a/src/test/java/io/flamingock/template/mongodb/changes/_0002__seed_users.yaml b/src/test/java/io/flamingock/template/mongodb/changes/_0002__seed_users.yaml index 09807d3..7413d3f 100644 --- a/src/test/java/io/flamingock/template/mongodb/changes/_0002__seed_users.yaml +++ b/src/test/java/io/flamingock/template/mongodb/changes/_0002__seed_users.yaml @@ -1,6 +1,6 @@ id: seed-users transactional: true -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" steps: diff --git a/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml b/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml index 33cac6c..ee07965 100644 --- a/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml +++ b/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml @@ -1,6 +1,6 @@ id: multiple-operations-change transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" diff --git a/src/test/java/io/flamingock/template/mongodb/changes/_0004__apply_and_rollback.yaml b/src/test/java/io/flamingock/template/mongodb/changes/_0004__apply_and_rollback.yaml index 7d6675f..48a15a3 100644 --- a/src/test/java/io/flamingock/template/mongodb/changes/_0004__apply_and_rollback.yaml +++ b/src/test/java/io/flamingock/template/mongodb/changes/_0004__apply_and_rollback.yaml @@ -1,6 +1,6 @@ id: apply-and-rollback-test transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" diff --git a/src/test/java/io/flamingock/template/mongodb/changes/_0005__step_based_change.yaml b/src/test/java/io/flamingock/template/mongodb/changes/_0005__step_based_change.yaml index 1ee1814..1b04342 100644 --- a/src/test/java/io/flamingock/template/mongodb/changes/_0005__step_based_change.yaml +++ b/src/test/java/io/flamingock/template/mongodb/changes/_0005__step_based_change.yaml @@ -1,6 +1,6 @@ id: step-based-change transactional: false -template: MongoChangeTemplate +template: mongodb-sync-template targetSystem: id: "mongodb" diff --git a/src/test/java/io/flamingock/template/mongodb/validation/MongoOperationValidatorTest.java b/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java similarity index 78% rename from src/test/java/io/flamingock/template/mongodb/validation/MongoOperationValidatorTest.java rename to src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java index fcdb890..17f0703 100644 --- a/src/test/java/io/flamingock/template/mongodb/validation/MongoOperationValidatorTest.java +++ b/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.template.mongodb.validation; +package io.flamingock.template.mongodb.model; -import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.api.template.TemplatePayloadValidationError; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -25,23 +25,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class MongoOperationValidatorTest { - - private static final String ENTITY_ID = "test-change"; +class MongoOperationValidateTest { @Nested @DisplayName("Common Validation Tests") class CommonValidationTests { - @Test - @DisplayName("WHEN operation is null THEN validation fails") - void nullOperationTest() { - List errors = MongoOperationValidator.validate(null, ENTITY_ID); - - assertEquals(1, errors.size()); - assertTrue(errors.get(0).getMessage().contains("cannot be null")); - } - @Test @DisplayName("WHEN operation type is null THEN validation fails") void nullOperationTypeTest() { @@ -49,9 +38,10 @@ void nullOperationTypeTest() { op.setType(null); op.setCollection("test"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("type", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("type is required")); } @@ -62,9 +52,10 @@ void emptyOperationTypeTest() { op.setType(""); op.setCollection("test"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("type", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("type is required")); } @@ -75,9 +66,10 @@ void unknownOperationTypeTest() { op.setType("unknownType"); op.setCollection("test"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("type", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("Unknown operation type")); } @@ -88,9 +80,10 @@ void nullCollectionTest() { op.setType("createCollection"); op.setCollection(null); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("collection", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("Collection name is required")); } @@ -101,9 +94,10 @@ void emptyCollectionTest() { op.setType("createCollection"); op.setCollection(""); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("collection", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("cannot be empty")); } @@ -114,9 +108,10 @@ void collectionWithDollarSignTest() { op.setType("createCollection"); op.setCollection("test$collection"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("collection", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("cannot contain '$'")); } @@ -127,16 +122,17 @@ void collectionWithNullCharTest() { op.setType("createCollection"); op.setCollection("test\0collection"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("collection", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("null character")); } } @Nested - @DisplayName("Insert Operation Tests") - class InsertOperationTests { + @DisplayName("Insert Validation Tests") + class InsertValidationTests { @Test @DisplayName("WHEN insert missing parameters THEN validation fails") @@ -146,9 +142,10 @@ void insertMissingParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'parameters'")); } @@ -160,9 +157,10 @@ void insertMissingDocumentsTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.documents", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'documents'")); } @@ -176,9 +174,10 @@ void insertEmptyDocumentsTest() { params.put("documents", new ArrayList<>()); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.documents", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("cannot be empty")); } @@ -194,9 +193,10 @@ void insertNullDocumentElementTest() { params.put("documents", docs); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertTrue(errors.get(0).getField().contains("parameters.documents")); assertTrue(errors.get(0).getMessage().contains("index 0 is null")); } @@ -210,9 +210,10 @@ void insertDocumentsWrongTypeTest() { params.put("documents", "not a list"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.documents", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("must be a list")); } @@ -230,15 +231,15 @@ void insertValidTest() { params.put("documents", docs); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("Update Operation Tests") - class UpdateOperationTests { + @DisplayName("Update Validation Tests") + class UpdateValidationTests { @Test @DisplayName("WHEN update missing parameters THEN validation fails") @@ -248,9 +249,10 @@ void updateMissingParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'parameters'")); } @@ -266,9 +268,10 @@ void updateMissingFilterTest() { params.put("update", update); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.filter", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'filter'")); } @@ -282,9 +285,10 @@ void updateMissingUpdateParamTest() { params.put("filter", new HashMap<>()); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.update", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'update'")); } @@ -299,9 +303,10 @@ void updateParamWrongTypeTest() { params.put("update", "not a document"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.update", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("must be a document")); } @@ -313,7 +318,7 @@ void updateMissingBothTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(2, errors.size()); } @@ -333,34 +338,15 @@ void updateValidTest() { params.put("update", update); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); - - assertTrue(errors.isEmpty()); - } - - @Test - @DisplayName("WHEN update with multi option is valid THEN validation passes") - void updateWithMultiValidTest() { - MongoOperation op = new MongoOperation(); - op.setType("update"); - op.setCollection("test"); - Map params = new HashMap<>(); - params.put("filter", new HashMap<>()); - Map update = new HashMap<>(); - update.put("$set", new HashMap<>()); - params.put("update", update); - params.put("multi", true); - op.setParameters(params); - - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("Delete Operation Tests") - class DeleteOperationTests { + @DisplayName("Delete Validation Tests") + class DeleteValidationTests { @Test @DisplayName("WHEN delete missing filter THEN validation fails") @@ -370,9 +356,10 @@ void deleteMissingFilterTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.filter", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'filter'")); } @@ -386,7 +373,7 @@ void deleteEmptyFilterTest() { params.put("filter", new HashMap<>()); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } @@ -403,15 +390,15 @@ void deleteValidTest() { params.put("filter", filter); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("CreateIndex Operation Tests") - class CreateIndexOperationTests { + @DisplayName("CreateIndex Validation Tests") + class CreateIndexValidationTests { @Test @DisplayName("WHEN createIndex missing parameters THEN validation fails") @@ -421,9 +408,10 @@ void createIndexMissingParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'parameters'")); } @@ -435,9 +423,10 @@ void createIndexMissingKeysTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.keys", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'keys'")); } @@ -451,9 +440,10 @@ void createIndexEmptyKeysTest() { params.put("keys", new HashMap<>()); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.keys", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("cannot be empty")); } @@ -467,9 +457,10 @@ void createIndexKeysWrongTypeTest() { params.put("keys", "not a map"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.keys", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("must be a map")); } @@ -485,15 +476,15 @@ void createIndexValidTest() { params.put("keys", keys); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("DropIndex Operation Tests") - class DropIndexOperationTests { + @DisplayName("DropIndex Validation Tests") + class DropIndexValidationTests { @Test @DisplayName("WHEN dropIndex missing both indexName and keys THEN validation fails") @@ -503,7 +494,7 @@ void dropIndexMissingBothTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); assertTrue(errors.get(0).getMessage().contains("'indexName' or 'keys'")); @@ -519,7 +510,7 @@ void dropIndexWithIndexNameTest() { params.put("indexName", "email_index"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } @@ -536,15 +527,15 @@ void dropIndexWithKeysTest() { params.put("keys", keys); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("RenameCollection Operation Tests") - class RenameCollectionOperationTests { + @DisplayName("RenameCollection Validation Tests") + class RenameCollectionValidationTests { @Test @DisplayName("WHEN renameCollection missing target THEN validation fails") @@ -554,9 +545,10 @@ void renameCollectionMissingTargetTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.target", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'target'")); } @@ -570,9 +562,10 @@ void renameCollectionEmptyTargetTest() { params.put("target", ""); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.target", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("cannot be null or empty")); } @@ -586,15 +579,15 @@ void renameCollectionValidTest() { params.put("target", "newName"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("CreateView Operation Tests") - class CreateViewOperationTests { + @DisplayName("CreateView Validation Tests") + class CreateViewValidationTests { @Test @DisplayName("WHEN createView missing parameters THEN validation fails") @@ -604,9 +597,10 @@ void createViewMissingParametersTest() { op.setCollection("testView"); op.setParameters(null); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'parameters'")); } @@ -620,9 +614,10 @@ void createViewMissingViewOnTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.viewOn", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'viewOn'")); } @@ -636,9 +631,10 @@ void createViewMissingPipelineTest() { params.put("viewOn", "sourceCollection"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.pipeline", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("requires 'pipeline'")); } @@ -653,9 +649,10 @@ void createViewPipelineWrongTypeTest() { params.put("pipeline", "not a list"); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertEquals(1, errors.size()); + assertEquals("parameters.pipeline", errors.get(0).getField()); assertTrue(errors.get(0).getMessage().contains("must be a list")); } @@ -670,15 +667,15 @@ void createViewValidTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } } @Nested - @DisplayName("Simple Operation Tests") - class SimpleOperationTests { + @DisplayName("Simple Operation Validation Tests") + class SimpleOperationValidationTests { @Test @DisplayName("WHEN createCollection is valid THEN validation passes") @@ -687,7 +684,7 @@ void createCollectionValidTest() { op.setType("createCollection"); op.setCollection("test"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } @@ -699,7 +696,7 @@ void dropCollectionValidTest() { op.setType("dropCollection"); op.setCollection("test"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); assertTrue(errors.isEmpty()); } @@ -711,7 +708,19 @@ void dropViewValidTest() { op.setType("dropView"); op.setCollection("testView"); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); + + assertTrue(errors.isEmpty()); + } + + @Test + @DisplayName("WHEN modifyCollection is valid THEN validation passes") + void modifyCollectionValidTest() { + MongoOperation op = new MongoOperation(); + op.setType("modifyCollection"); + op.setCollection("test"); + + List errors = op.validate(); assertTrue(errors.isEmpty()); } @@ -731,9 +740,9 @@ void multipleErrorsTest() { params.put("documents", new ArrayList<>()); op.setParameters(params); - List errors = MongoOperationValidator.validate(op, ENTITY_ID); + List errors = op.validate(); - assertEquals(2, errors.size()); // collection contains $ and documents is empty + assertEquals(2, errors.size()); } } } From cb1cca85ba98994bd70ca12bf20ff4632334ff62 Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Fri, 27 Feb 2026 13:46:42 +0000 Subject: [PATCH 3/3] fix: issue 1 --- ANALYSIS.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ANALYSIS.md b/ANALYSIS.md index 12171b9..41cd861 100644 --- a/ANALYSIS.md +++ b/ANALYSIS.md @@ -28,11 +28,10 @@ YAML -> MongoOperation (deserialized) -> MongoOperationValidator -> MongoOperati ## 2. Top 10 Issues (Ranked by Severity) -### #1 - CRITICAL: Rollback path skips validation entirely -**File:** `MongoChangeTemplate.java:101-105` -**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. -**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. -**Fix:** Call `MongoOperationValidator.validate(rollbackPayload, changeId)` in the `rollback()` method before executing. +### #1 - ~~CRITICAL: Rollback path skips validation entirely~~ RESOLVED +**Status:** Fixed by validation refactoring. +**Original issue:** `MongoChangeTemplate.rollback()` did not call `MongoOperationValidator.validate()` before executing, so malformed rollback YAML ran unchecked. +**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. ### #2 - HIGH: InsertOperator silently swallows null/empty documents, bypassing validation **File:** `InsertOperator.java:37-39`