diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 818e932..2d78d50 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -16,7 +16,7 @@ ENV HOME="/root"
# --------------------------------------
# Need to add the devcontainer workspace folder as a safe directory to enable git
# version control system to be enabled in the containers file system.
-RUN git config --global --add safe.directory "/workspaces/plugin-template"
+RUN git config --global --add safe.directory "/workspaces/plugin-documentdb"
# --------------------------------------
# --------------------------------------
@@ -53,7 +53,7 @@ ENV PATH="$PATH:$JAVA_HOME/bin"
# Will load a custom configuration file for Micronaut
ENV MICRONAUT_ENVIRONMENTS=local,override
# Sets the path where you save plugins as Jar and is loaded during the startup process
-ENV KESTRA_PLUGINS_PATH="/workspaces/plugin-template/local/plugins"
+ENV KESTRA_PLUGINS_PATH="/workspaces/plugin-documentdb/local/plugins"
# --------------------------------------
# --------------------------------------
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index b5184d6..7d9a650 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,10 +1,10 @@
{
- "name": "plugin-template",
+ "name": "plugin-documentdb",
"build": {
"context": ".",
"dockerfile": "Dockerfile"
},
- "workspaceFolder": "/workspaces/plugin-template",
+ "workspaceFolder": "/workspaces/plugin-documentdb",
"forwardPorts": [8080],
"customizations": {
"vscode": {
diff --git a/.github/setup-unit.sh b/.github/setup-unit.sh
new file mode 100755
index 0000000..0dad29e
--- /dev/null
+++ b/.github/setup-unit.sh
@@ -0,0 +1,130 @@
+#!/bin/bash
+
+# Setup script for Kestra DocumentDB Plugin Unit Tests
+# This script sets up a local DocumentDB instance for testing both locally and in CI
+
+set -e
+
+echo "๐ณ Setting up DocumentDB for unit tests..."
+
+# Check if Docker and Docker Compose are available
+if ! command -v docker &> /dev/null; then
+ echo "โ Docker is not installed or not in PATH"
+ exit 1
+fi
+
+# Check for Docker Compose (both v1 and v2)
+if ! command -v docker-compose &> /dev/null && ! command -v docker compose &> /dev/null; then
+ echo "โ Docker Compose is not installed or not in PATH"
+ exit 1
+fi
+
+# Use docker compose (v2) if available, otherwise fall back to docker-compose (v1)
+if command -v docker compose &> /dev/null; then
+ DC_CMD="docker compose"
+ COMPOSE_FILE="docker-compose-ci.yml"
+else
+ DC_CMD="docker-compose"
+ COMPOSE_FILE="docker-compose-ci.yml"
+fi
+
+# Stop and remove any existing containers
+echo "๐งน Cleaning up existing containers..."
+$DC_CMD -f $COMPOSE_FILE down -v --remove-orphans || true
+
+# Start MongoDB and DocumentDB API containers
+echo "๐ Starting MongoDB and DocumentDB API containers..."
+$DC_CMD -f $COMPOSE_FILE up -d --build
+
+# Wait for containers to start
+echo "โณ Waiting for containers to start..."
+timeout=120
+elapsed=0
+while ! $DC_CMD -f $COMPOSE_FILE ps | grep -q "mongodb.*Up"; do
+ if [ $elapsed -ge $timeout ]; then
+ echo "โ MongoDB container failed to start within ${timeout} seconds"
+ $DC_CMD -f $COMPOSE_FILE logs mongodb
+ exit 1
+ fi
+ sleep 5
+ elapsed=$((elapsed + 5))
+ echo "โณ Still waiting for MongoDB container... (${elapsed}/${timeout}s)"
+done
+
+echo "โ
MongoDB container is running"
+
+# Wait for DocumentDB API service to be ready
+echo "โณ Waiting for DocumentDB API service to respond..."
+timeout=120
+elapsed=0
+while ! curl -f -s http://localhost:10260/health &> /dev/null; do
+ if [ $elapsed -ge $timeout ]; then
+ echo "โ DocumentDB API service failed to respond within ${timeout} seconds"
+ echo "API Server logs:"
+ $DC_CMD -f $COMPOSE_FILE logs documentdb-api
+ exit 1
+ fi
+ sleep 5
+ elapsed=$((elapsed + 5))
+ echo "โณ Still waiting for API service... (${elapsed}/${timeout}s)"
+done
+
+echo "โ
DocumentDB API service is ready"
+
+# Create a test database and collection
+echo "๐๏ธ Creating test database and collection..."
+
+# Test database creation by inserting a test document
+response=$(curl -s -X POST \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Basic $(echo -n 'testuser:testpass' | base64)" \
+ -d '{
+ "database": "test_db",
+ "collection": "test_collection",
+ "document": {"_id": "test_doc", "message": "DocumentDB is ready for testing", "timestamp": "'$(date -Iseconds)'"}
+ }' \
+ http://localhost:10260/data/v1/action/insertOne)
+
+echo "Insert response: $response"
+
+# Verify we can read from the database
+echo "๐ Verifying database connectivity..."
+read_response=$(curl -s -X POST \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Basic $(echo -n 'testuser:testpass' | base64)" \
+ -d '{
+ "database": "test_db",
+ "collection": "test_collection",
+ "filter": {"_id": "test_doc"}
+ }' \
+ http://localhost:10260/data/v1/action/find)
+
+echo "Read response: $read_response"
+
+if echo "$read_response" | grep -q "DocumentDB is ready"; then
+ echo "โ
Test database and collection created successfully"
+else
+ echo "โ ๏ธ Database setup completed, but verification response unclear"
+ echo "This is normal - tests will handle actual connectivity"
+fi
+
+# Show status
+echo "๐ Container status:"
+$DC_CMD -f $COMPOSE_FILE ps
+
+echo ""
+echo "๐ Setup complete!"
+echo ""
+echo "๐ Connection details:"
+echo " URL: http://localhost:10260"
+echo " Username: testuser"
+echo " Password: testpass"
+echo " Test Database: test_db"
+echo " Test Collection: test_collection"
+echo ""
+echo "๐งช You can now run tests with:"
+echo " ./gradlew test"
+echo " DOCUMENTDB_INTEGRATION_TESTS=true ./gradlew test"
+echo ""
+echo "๐ To stop the services:"
+echo " $DC_CMD -f $COMPOSE_FILE down"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4be6e5d..976b3c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ Thumbs.db
build/
target/
out/
+bin/
.idea
.vscode
*.iml
@@ -12,5 +13,6 @@ out/
.project
.settings
.classpath
+.factorypath
.attach*
src/test/resources/application-test.yml
diff --git a/README.md b/README.md
index 3da6d1f..23f2296 100644
--- a/README.md
+++ b/README.md
@@ -33,39 +33,229 @@
Get started with Kestra in 4 minutes.
-# Kestra Plugin Template
+# Kestra DocumentDB Plugin
-> A template for creating Kestra plugins
+> Integrate with DocumentDB - Microsoft's open-source, MongoDB-compatible document database
-This repository serves as a general template for creating a new [Kestra](https://github.com/kestra-io/kestra) plugin. It should take only a few minutes! Use this repository as a scaffold to ensure that you've set up the plugin correctly, including unit tests and CI/CD workflows.
+This plugin provides integration with [DocumentDB](https://documentdb.io), Microsoft's open-source document database built on PostgreSQL. DocumentDB is now part of the Linux Foundation and offers MongoDB compatibility with the reliability and ecosystem of PostgreSQL.
+
+## Features
+
+- **Document Operations**: Insert single or multiple documents with automatic ID generation
+- **Advanced Querying**: Find documents with MongoDB-style filters and pagination
+- **Aggregation Pipelines**: Execute complex aggregation operations for data analysis
+- **MongoDB Compatibility**: Use familiar MongoDB query syntax and operators
+- **Flexible Output**: Support for FETCH, FETCH_ONE, STORE, and NONE output types
+- **PostgreSQL Backend**: Built on the reliability and performance of PostgreSQL
+- **Open Source**: Fully MIT-licensed with no vendor lock-in
+
+## Supported Operations
+
+| Operation | Description | Required Parameters |
+|-----------|-------------|-------------------|
+| `Insert` | Insert single or multiple documents | `host`, `database`, `collection`, `username`, `password`, `document` or `documents` |
+| `Read` | Find documents with filtering and aggregation | `host`, `database`, `collection`, `username`, `password`, optional: `filter`, `aggregationPipeline`, `limit`, `skip` |

-## Running the project in local
+## Quick Start
+
+### Basic Configuration
+
+All tasks require these basic connection parameters:
+
+```yaml
+tasks:
+ - id: documentdb_task
+ type: io.kestra.plugin.documentdb.Insert
+ host: "https://my-documentdb-instance.com" # DocumentDB HTTP endpoint
+ database: "myapp" # Database name
+ collection: "users" # Collection name
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}" # Username
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}" # Password
+```
+
+### Example: Insert Single Document
+
+```yaml
+id: insert_user
+namespace: company.documentdb
+
+tasks:
+ - id: create_user
+ type: io.kestra.plugin.documentdb.Insert
+ host: "https://my-documentdb-instance.com"
+ database: "myapp"
+ collection: "users"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ document:
+ name: "John Doe"
+ email: "john.doe@example.com"
+ age: 30
+ created_at: "{{ now() }}"
+ roles: ["user", "editor"]
+```
+
+### Example: Insert Multiple Documents
+
+```yaml
+id: insert_products
+namespace: company.documentdb
+
+tasks:
+ - id: create_products
+ type: io.kestra.plugin.documentdb.Insert
+ host: "https://my-documentdb-instance.com"
+ database: "inventory"
+ collection: "products"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ documents:
+ - name: "Laptop"
+ price: 999.99
+ category: "Electronics"
+ in_stock: true
+ - name: "Mouse"
+ price: 29.99
+ category: "Electronics"
+ in_stock: false
+ - name: "Desk"
+ price: 299.99
+ category: "Furniture"
+ in_stock: true
+```
+
+### Example: Find Documents with Filters
+
+```yaml
+id: find_active_users
+namespace: company.documentdb
+
+tasks:
+ - id: query_users
+ type: io.kestra.plugin.documentdb.Read
+ host: "https://my-documentdb-instance.com"
+ database: "myapp"
+ collection: "users"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ filter:
+ status: "active"
+ age:
+ $gte: 18
+ roles:
+ $in: ["editor", "admin"]
+ limit: 100
+ fetchType: FETCH
+```
+
+### Example: Aggregation Pipeline
+
+```yaml
+id: user_statistics
+namespace: company.documentdb
+
+tasks:
+ - id: aggregate_users
+ type: io.kestra.plugin.documentdb.Read
+ host: "https://my-documentdb-instance.com"
+ database: "myapp"
+ collection: "users"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ aggregationPipeline:
+ - $match:
+ status: "active"
+ - $group:
+ _id: "$department"
+ count: { $sum: 1 }
+ avgAge: { $avg: "$age" }
+ - $sort:
+ count: -1
+ fetchType: FETCH
+```
+
+### Example: Get Single Document
+
+```yaml
+id: get_user
+namespace: company.documentdb
+
+tasks:
+ - id: find_user
+ type: io.kestra.plugin.documentdb.Read
+ host: "https://my-documentdb-instance.com"
+ database: "myapp"
+ collection: "users"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ filter:
+ email: "john.doe@example.com"
+ fetchType: FETCH_ONE
+```
+
+## Installation
+
+Add this plugin to your Kestra instance:
+
+```bash
+./kestra plugins install io.kestra.plugin:plugin-documentdb:LATEST
+```
+
+## Development
+
### Prerequisites
- Java 21
- Docker
### Running tests
+
+#### Integration Tests with Mock DocumentDB Server
+
+This plugin includes a test mock server (`api-server.py`) that simulates DocumentDB's REST API for testing purposes. The server bridges HTTP requests to MongoDB operations, providing a realistic testing environment without requiring a real DocumentDB instance.
+
+**Automatic Setup (Recommended):**
+```bash
+# Setup test environment and run tests
+./.github/setup-unit.sh
+./gradlew test
```
+
+**Manual Setup:**
+```bash
+# Start DocumentDB mock server and MongoDB
+docker-compose -f docker-compose-ci.yml up -d
+
+# Run tests
./gradlew check --parallel
+
+# Cleanup
+docker-compose -f docker-compose-ci.yml down
```
-### Development
+The mock server (`api-server.py`) provides:
+- **DocumentDB REST API simulation**: Endpoints matching real DocumentDB HTTP API
+- **MongoDB backend**: Uses MongoDB as the underlying database (DocumentDB-compatible)
+- **Test authentication**: Uses `testuser:testpass` credentials for testing
+- **Local endpoint**: Available at `http://localhost:10260`
-`VSCode`:
+**Test Environment Details:**
+- **Mock API Server**: `http://localhost:10260` (simulates DocumentDB REST API)
+- **MongoDB Instance**: `localhost:27017` (backend storage)
+- **Test Credentials**: Username: `testuser`, Password: `testpass`
+- **Test Database**: `test_db`
-Follow the README.md within the `.devcontainer` folder for a quick and easy way to get up and running with developing plugins if you are using VSCode.
+### Local Development
-`Other IDEs`:
+**VSCode**: Follow the README.md within the `.devcontainer` folder for development setup.
+**Other IDEs**:
+```bash
+./gradlew shadowJar && docker build -t kestra-documentdb . && docker run --rm -p 8080:8080 kestra-documentdb server local
```
-./gradlew shadowJar && docker build -t kestra-custom . && docker run --rm -p 8080:8080 kestra-custom server local
-```
-> [!NOTE]
-> You need to relaunch this whole command everytime you make a change to your plugin
-go to http://localhost:8080, your plugin will be available to use
+Visit http://localhost:8080 to test your plugin.
## Documentation
* Full documentation can be found under: [kestra.io/docs](https://kestra.io/docs)
diff --git a/api-server.py b/api-server.py
new file mode 100644
index 0000000..24a0a53
--- /dev/null
+++ b/api-server.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python3
+"""
+Simple HTTP API server that simulates DocumentDB REST API endpoints
+This bridges HTTP requests to MongoDB operations for testing
+"""
+
+import os
+import json
+import base64
+from flask import Flask, request, jsonify
+from pymongo import MongoClient
+from pymongo.errors import PyMongoError
+import logging
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = Flask(__name__)
+
+# MongoDB connection
+MONGODB_URI = os.getenv('MONGODB_URI', 'mongodb://testuser:testpass@mongodb:27017/test_db?authSource=admin')
+client = MongoClient(MONGODB_URI)
+
+def authenticate_request():
+ """Simple authentication check"""
+ auth_header = request.headers.get('Authorization', '')
+ if not auth_header.startswith('Basic '):
+ return False
+
+ try:
+ credentials = base64.b64decode(auth_header[6:]).decode('utf-8')
+ username, password = credentials.split(':', 1)
+ return username == 'testuser' and password == 'testpass'
+ except:
+ return False
+
+def get_collection(database_name, collection_name):
+ """Get MongoDB collection"""
+ db = client[database_name]
+ return db[collection_name]
+
+@app.route('/data/v1/action/insertOne', methods=['POST'])
+def insert_one():
+ """Insert a single document"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ document = data['document']
+
+ collection = get_collection(database, collection_name)
+ result = collection.insert_one(document)
+
+ return jsonify({
+ 'insertedId': str(result.inserted_id),
+ 'insertedCount': 1
+ })
+
+ except Exception as e:
+ logger.error(f"Insert error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/insertMany', methods=['POST'])
+def insert_many():
+ """Insert multiple documents"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ documents = data['documents']
+
+ collection = get_collection(database, collection_name)
+ result = collection.insert_many(documents)
+
+ return jsonify({
+ 'insertedIds': [str(id) for id in result.inserted_ids],
+ 'insertedCount': len(result.inserted_ids)
+ })
+
+ except Exception as e:
+ logger.error(f"Insert many error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/find', methods=['POST'])
+def find():
+ """Find documents"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ filter_doc = data.get('filter', {})
+ limit = data.get('limit')
+ skip = data.get('skip', 0)
+
+ collection = get_collection(database, collection_name)
+ cursor = collection.find(filter_doc).skip(skip)
+
+ if limit:
+ cursor = cursor.limit(limit)
+
+ documents = []
+ for doc in cursor:
+ # Convert ObjectId to string for JSON serialization
+ if '_id' in doc:
+ doc['_id'] = str(doc['_id'])
+ documents.append(doc)
+
+ return jsonify({
+ 'documents': documents
+ })
+
+ except Exception as e:
+ logger.error(f"Find error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/aggregate', methods=['POST'])
+def aggregate():
+ """Execute aggregation pipeline"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ pipeline = data['pipeline']
+
+ collection = get_collection(database, collection_name)
+ cursor = collection.aggregate(pipeline)
+
+ documents = []
+ for doc in cursor:
+ # Convert ObjectId to string for JSON serialization
+ if '_id' in doc:
+ doc['_id'] = str(doc['_id'])
+ documents.append(doc)
+
+ return jsonify({
+ 'documents': documents
+ })
+
+ except Exception as e:
+ logger.error(f"Aggregate error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/updateOne', methods=['POST'])
+def update_one():
+ """Update a single document"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ filter_criteria = data.get('filter', {})
+ update_doc = data['update']
+
+ collection = get_collection(database, collection_name)
+ result = collection.update_one(filter_criteria, update_doc)
+
+ return jsonify({
+ 'matchedCount': result.matched_count,
+ 'modifiedCount': result.modified_count,
+ 'upsertedId': str(result.upserted_id) if result.upserted_id else None
+ })
+
+ except Exception as e:
+ logger.error(f"Update one error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/updateMany', methods=['POST'])
+def update_many():
+ """Update multiple documents"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ filter_criteria = data.get('filter', {})
+ update_doc = data['update']
+
+ collection = get_collection(database, collection_name)
+ result = collection.update_many(filter_criteria, update_doc)
+
+ return jsonify({
+ 'matchedCount': result.matched_count,
+ 'modifiedCount': result.modified_count
+ })
+
+ except Exception as e:
+ logger.error(f"Update many error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/deleteOne', methods=['POST'])
+def delete_one():
+ """Delete a single document"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ filter_criteria = data.get('filter', {})
+
+ collection = get_collection(database, collection_name)
+ result = collection.delete_one(filter_criteria)
+
+ return jsonify({
+ 'deletedCount': result.deleted_count
+ })
+
+ except Exception as e:
+ logger.error(f"Delete one error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/data/v1/action/deleteMany', methods=['POST'])
+def delete_many():
+ """Delete multiple documents"""
+ if not authenticate_request():
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ try:
+ data = request.get_json()
+ database = data['database']
+ collection_name = data['collection']
+ filter_criteria = data.get('filter', {})
+
+ collection = get_collection(database, collection_name)
+ result = collection.delete_many(filter_criteria)
+
+ return jsonify({
+ 'deletedCount': result.deleted_count
+ })
+
+ except Exception as e:
+ logger.error(f"Delete many error: {e}")
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/health', methods=['GET'])
+def health():
+ """Health check endpoint"""
+ try:
+ client.admin.command('ping')
+ return jsonify({'status': 'healthy', 'message': 'DocumentDB API server is running'})
+ except Exception as e:
+ return jsonify({'status': 'unhealthy', 'error': str(e)}), 500
+
+if __name__ == '__main__':
+ logger.info("Starting DocumentDB API server...")
+ logger.info(f"MongoDB URI: {MONGODB_URI}")
+ app.run(host='0.0.0.0', port=10260, debug=True)
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index eadd000..5c9a5b3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,7 @@ java {
}
group = "io.kestra.plugin"
-description = 'Plugin template for Kestra'
+description = 'DocumentDB plugin for Kestra'
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
@@ -130,6 +130,9 @@ test {
jacocoTestReport {
dependsOn test
+ reports {
+ xml.required.set(true)
+ }
}
/**********************************************************************************************************************\
@@ -172,8 +175,8 @@ jar {
manifest {
attributes(
"X-Kestra-Name": project.name,
- "X-Kestra-Title": "Template",
- "X-Kestra-Group": project.group + ".templates",
+ "X-Kestra-Title": "DocumentDB",
+ "X-Kestra-Group": project.group + ".documentdb",
"X-Kestra-Description": project.description,
"X-Kestra-Version": project.version
)
diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml
new file mode 100644
index 0000000..d4229e0
--- /dev/null
+++ b/docker-compose-ci.yml
@@ -0,0 +1,46 @@
+version: '3.8'
+services:
+ # Using MongoDB with HTTP interface since DocumentDB is MongoDB-compatible
+ mongodb:
+ image: mongo:7.0
+ ports:
+ - "127.0.0.1:27017:27017"
+ environment:
+ - MONGO_INITDB_ROOT_USERNAME=testuser
+ - MONGO_INITDB_ROOT_PASSWORD=testpass
+ - MONGO_INITDB_DATABASE=test_db
+ volumes:
+ - mongodb-data:/data/db
+ healthcheck:
+ test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 30s
+
+ # DocumentDB HTTP API Gateway (simulates DocumentDB REST API)
+ documentdb-api:
+ image: python:3.11-slim
+ depends_on:
+ mongodb:
+ condition: service_healthy
+ ports:
+ - "0.0.0.0:10260:10260"
+ volumes:
+ - ./api-server.py:/app/api-server.py:ro
+ working_dir: /app
+ command: >
+ sh -c "
+ pip install flask pymongo &&
+ python api-server.py
+ "
+ environment:
+ - MONGODB_URI=mongodb://testuser:testpass@mongodb:27017/test_db?authSource=admin
+
+volumes:
+ mongodb-data:
+ driver: local
+
+networks:
+ default:
+ name: documentdb-network
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index aaa347f..8107386 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-rootProject.name = 'plugin-template'
+rootProject.name = 'plugin-documentdb'
\ No newline at end of file
diff --git a/src/main/java/io/kestra/plugin/documentdb/AbstractDocumentDBTask.java b/src/main/java/io/kestra/plugin/documentdb/AbstractDocumentDBTask.java
new file mode 100644
index 0000000..8a0ebf8
--- /dev/null
+++ b/src/main/java/io/kestra/plugin/documentdb/AbstractDocumentDBTask.java
@@ -0,0 +1,58 @@
+package io.kestra.plugin.documentdb;
+
+import io.kestra.core.models.property.Property;
+import io.kestra.core.models.tasks.Task;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * Abstract base class for DocumentDB tasks.
+ * Provides common connection properties shared across all DocumentDB operations.
+ */
+@SuperBuilder
+@ToString
+@EqualsAndHashCode
+@Getter
+@NoArgsConstructor
+public abstract class AbstractDocumentDBTask extends Task {
+
+ @Schema(
+ title = "DocumentDB host",
+ description = "The HTTP endpoint URL of your DocumentDB instance"
+ )
+ @NotNull
+ protected Property host;
+
+ @Schema(
+ title = "Database name",
+ description = "The name of the database"
+ )
+ @NotNull
+ protected Property database;
+
+ @Schema(
+ title = "Collection name",
+ description = "The name of the collection"
+ )
+ @NotNull
+ protected Property collection;
+
+ @Schema(
+ title = "Username",
+ description = "DocumentDB username for authentication"
+ )
+ @NotNull
+ protected Property username;
+
+ @Schema(
+ title = "Password",
+ description = "DocumentDB password for authentication"
+ )
+ @NotNull
+ protected Property password;
+}
diff --git a/src/main/java/io/kestra/plugin/documentdb/Delete.java b/src/main/java/io/kestra/plugin/documentdb/Delete.java
new file mode 100644
index 0000000..dde06b0
--- /dev/null
+++ b/src/main/java/io/kestra/plugin/documentdb/Delete.java
@@ -0,0 +1,169 @@
+package io.kestra.plugin.documentdb;
+
+import io.kestra.core.models.annotations.Example;
+import io.kestra.core.models.annotations.Plugin;
+import io.kestra.core.models.property.Property;
+import io.kestra.core.models.tasks.RunnableTask;
+import io.kestra.core.models.tasks.Task;
+import io.kestra.core.runners.RunContext;
+import io.kestra.plugin.documentdb.models.DeleteResult;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+import org.slf4j.Logger;
+
+import java.util.Map;
+
+@SuperBuilder
+@ToString
+@EqualsAndHashCode
+@Getter
+@NoArgsConstructor
+@Schema(
+ title = "Delete documents from a DocumentDB collection",
+ description = "Delete one or more documents from a DocumentDB collection that match the filter criteria."
+)
+@Plugin(
+ examples = {
+ @Example(
+ title = "Delete a single user document",
+ full = true,
+ code = """
+ id: delete_documentdb_user
+ namespace: company.documentdb
+
+ tasks:
+ - id: delete_user
+ type: io.kestra.plugin.documentdb.Delete
+ host: "https://my-documentdb-instance.com"
+ database: "myapp"
+ collection: "users"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ filter:
+ email: "user.to.delete@example.com"
+ deleteMany: false
+ """
+ ),
+ @Example(
+ title = "Delete multiple inactive documents",
+ full = true,
+ code = """
+ id: cleanup_inactive_users
+ namespace: company.documentdb
+
+ tasks:
+ - id: delete_inactive_users
+ type: io.kestra.plugin.documentdb.Delete
+ host: "https://my-documentdb-instance.com"
+ database: "myapp"
+ collection: "users"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ filter:
+ status: "inactive"
+ last_login:
+ $lt: "2022-01-01"
+ deleteMany: true
+ """
+ ),
+ @Example(
+ title = "Delete documents by age criteria",
+ full = true,
+ code = """
+ id: delete_old_logs
+ namespace: company.documentdb
+
+ tasks:
+ - id: cleanup_old_logs
+ type: io.kestra.plugin.documentdb.Delete
+ host: "https://my-documentdb-instance.com"
+ database: "logging"
+ collection: "application_logs"
+ username: "{{ secret('DOCUMENTDB_USERNAME') }}"
+ password: "{{ secret('DOCUMENTDB_PASSWORD') }}"
+ filter:
+ created_at:
+ $lt: "{{ now() | dateAdd(-30, 'DAYS') }}"
+ level:
+ $in: ["DEBUG", "INFO"]
+ deleteMany: true
+ """
+ )
+ }
+)
+public class Delete extends AbstractDocumentDBTask implements RunnableTask {
+
+ @Schema(
+ title = "Filter criteria",
+ description = "MongoDB-style filter to select which documents to delete. Example: {\"status\": \"inactive\", \"age\": {\"$gte\": 18}}"
+ )
+ private Property