Skip to content

izzoukeab/spring-mongodb-toolkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸƒ spring-mongodb-toolkit

A lightweight, annotation-driven toolkit that adds search, pagination, auditing, and soft-delete superpowers to any Spring Boot + MongoDB project.

Java Spring Boot License CI


πŸ“¦ Features

Module What it does
πŸ” Search Annotation-driven SearchCriteria β†’ MongoDB Query translation with type-safe operators
πŸ“„ Pagination Safe Pageable factory with clamped bounds + serializable MongoPageResult<T> DTO
πŸ•΅οΈ Auditing Spring Security–backed AuditorAware with configurable fallback
πŸ—‘οΈ Soft Delete Base document with softDelete() / restore() helpers

βš™οΈ Requirements

  • β˜• Java 21
  • 🐘 Maven (or use the included ./mvnw wrapper)
  • πŸƒ Spring Boot 3.x project with spring-boot-starter-data-mongodb

πŸš€ Quick Start

1 β€” Run tests

./mvnw test

2 β€” Full build with coverage report

./mvnw verify

Open the coverage report at target/site/jacoco/index.html.


πŸ—οΈ Module Guide

πŸ“„ Document Base Classes

BaseDocument<ID>

Extend this to get automatic auditing and optimistic locking on every document.

@Document("products")
public class Product extends BaseDocument<String> {
    private String name;
    private double price;
}
Field Type Description
id ID Document identifier (@Id)
createdAt Instant Set automatically on insert
updatedAt Instant Updated automatically on every save
createdBy String Principal who created the document
updatedBy String Principal who last modified the document
version Long Optimistic lock version (auto-incremented)

SoftDeleteDocument<ID>

Extends BaseDocument and adds logical deletion β€” documents are never physically removed.

@Document("orders")
public class Order extends SoftDeleteDocument<String> {
    private String customerId;
}
order.softDelete();              // marks deleted, records timestamp
order.softDelete("admin");      // also records who deleted it
order.restore();                // clears all soft-delete fields
Field Type Description
deleted boolean true when logically deleted (indexed)
deletedAt Instant Timestamp of deletion
deletedBy String Principal who deleted the document

πŸ•΅οΈ Auditing

The toolkit ships a SpringSecurityAuditor that implements Spring Data's AuditorAware<String>. It reads the current principal from SecurityContextHolder and falls back to a configurable name when no user is authenticated.

Auto-configuration

The auditor is registered automatically via Spring Boot auto-configuration β€” no @Bean needed.

Configuration

javloom:
  mongodb:
    audit:
      fallback-auditor: system   # default; used for anonymous / unauthenticated requests

πŸ” Search

The search module lets you translate a list of SearchCriteria objects into a MongoDB Query using a simple annotation-driven approach.

Step 1 β€” Annotate your document

Mark each searchable field with the appropriate @Searchable* annotation.

@Document("products")
public class Product extends BaseDocument<String> {

    @SearchableString                          // default operator: EQ
    private String name;

    @SearchableString(StringOperators.LIKE)    // default operator: LIKE
    private String description;

    @SearchableNumber
    private double price;

    @SearchableBoolean
    private boolean active;

    @SearchableDate
    private Instant launchedAt;

    @SearchableEnum
    private Category category;

    @SearchableCollection
    private List<String> tags;
}

All annotations accept an optional alias to expose a different name in search criteria:

@SearchableString(alias = "q")
private String description;   // search by "q" instead of "description"

Step 2 β€” Build and execute the query with SearchQuery

SearchQuery is the recommended fluent API. It resolves field types from your annotations at build time, so mismatched operators (e.g. calling .like() on a number field) throw an IllegalArgumentException immediately β€” no silent wrong queries.

Query query = SearchQuery.on(Product.class)
    .where("name").like("monitor")
    .and("price").between(100.0, 500.0)
    .and("active").isTrue()
    .build();

List<Product> results = mongoTemplate.find(query, Product.class);
With pagination
Query query = SearchQuery.on(Product.class)
    .where("name").like("monitor")
    .and("price").gt(50.0)
    .pageable(MongoPageRequest.of(0, 20, "price", Sort.Direction.ASC))
    .build();
AND / OR logic

Chain .and(field) for $and conditions (the default) and .or(field) for $or conditions. Both can be mixed freely.

Query query = SearchQuery.on(Product.class)
    .where("category").eq("ELECTRONICS")   // AND
    .and("price").lte(999.0)               // AND
    .or("name").like("monitor")            // OR
    .or("name").like("display")            // OR
    .build();
Full FieldCondition method reference
Method Applicable types Description
.eq(value) All Exact match
.like(value) STRING Case-insensitive contains
.startsWith(value) STRING Case-insensitive prefix match
.endsWith(value) STRING Case-insensitive suffix match
.gt(value) NUMBER Greater than
.gte(value) NUMBER Greater than or equal
.lt(value) NUMBER Less than
.lte(value) NUMBER Less than or equal
.between(from, to) NUMBER, DATE Inclusive range
.after(value) DATE After the given instant
.before(value) DATE Before the given instant
.isTrue() BOOLEAN Equals true
.isFalse() BOOLEAN Equals false
.in(v1, v2, ...) STRING, ENUM, COLLECTION Value in the given set
.nin(v1, v2, ...) STRING, ENUM, COLLECTION Value not in the given set
.exists() All Field is present in document
.notExists() All Field is absent from document

πŸ’‘ Aliases work transparently. If a field is annotated with alias = "q", use "q" in .where()/.and()/.or() and the correct MongoDB field name is resolved automatically.

Low-level alternative β€” SearchCriteria list

If you need to build criteria programmatically (e.g. from a request DTO), you can pass a List<SearchCriteria> directly to CriteriaBuilder:

List<SearchCriteria> criteria = List.of(
    SearchCriteria.of("name", "Monitor"),          // shorthand EQ

    SearchCriteria.builder()
        .field("price")
        .operator("BETWEEN")
        .value(List.of(100.0, 500.0))
        .build(),

    SearchCriteria.builder()
        .field("active")
        .operator("EQ")
        .value(true)
        .logic(SearchLogic.AND)
        .build()
);

Query query = CriteriaBuilder.buildQuery(Product.class, criteria);
// or with pagination:
Query query = CriteriaBuilder.buildQuery(Product.class, criteria, pageable);

πŸ—‚οΈ Operators Reference

πŸ”€ String (@SearchableString)

Operator Description
EQ Exact match
NEQ Not equal
LIKE Contains substring (case-insensitive regex)
STARTS_WITH Starts with prefix (case-insensitive)
ENDS_WITH Ends with suffix (case-insensitive)
IN Value is in a Collection
NIN Value is not in a Collection
EXISTS Field exists in document
NOT_EXISTS Field does not exist in document

πŸ”’ Number (@SearchableNumber)

Operator Description
EQ Equal
NEQ Not equal
GT Greater than
GTE Greater than or equal
LT Less than
LTE Less than or equal
BETWEEN Inclusive range β€” value must be List.of(lower, upper)
IN Value is in a Collection
NIN Value is not in a Collection
EXISTS Field exists

πŸ“… Date (@SearchableDate)

Operator Description
EQ Equal
GT After
GTE After or equal
LT Before
LTE Before or equal
BETWEEN Inclusive range β€” value must be List.of(from, to)
EXISTS Field exists
NOT_EXISTS Field does not exist

βœ… Boolean (@SearchableBoolean)

Operator Description
EQ Equal
EXISTS Field exists
NOT_EXISTS Field does not exist

🏷️ Enum (@SearchableEnum)

Operator Description
EQ Equal
NEQ Not equal
IN Value in a Collection
NIN Value not in a Collection
EXISTS Field exists
NOT_EXISTS Field does not exist

πŸ“‹ Collection (@SearchableCollection)

Operator Description
IN Array field contains any of the given values
NIN Array field contains none of the given values
SIZE_EQ Array size equals the given int
SIZE_GT Array size is greater than the given value
EXISTS Field exists
NOT_EXISTS Field does not exist

πŸ“„ Pagination

MongoPageRequest β€” safe Pageable factory

Page index is clamped to >= 0. Page size is clamped to [1, 100].

// Basic
Pageable p = MongoPageRequest.of(0, 20);

// With sort
Pageable p = MongoPageRequest.of(0, 20, "createdAt", Sort.Direction.DESC);

// Multiple sort orders
Pageable p = MongoPageRequest.of(0, 20,
    Sort.Order.desc("createdAt"),
    Sort.Order.asc("name")
);

// Defaults: page 0, size 20, sorted by createdAt DESC
Pageable p = MongoPageRequest.defaultPageable();
Constant Value
DEFAULT_PAGE 0
DEFAULT_SIZE 20
MAX_SIZE 100
DEFAULT_SORT "createdAt"

MongoPageResult<T> β€” serializable page DTO

Wraps a Spring Data Page<T> into a JSON-friendly response object (@JsonInclude(NON_NULL)).

Page<Product> page = mongoTemplate.find(query, Product.class);  // or repository method
MongoPageResult<Product> result = MongoPageResult.of(page);
{
  "content": [...],
  "page": 0,
  "size": 20,
  "totalElements": 142,
  "totalPages": 8,
  "first": true,
  "last": false
}

πŸ§ͺ Testing

Unit tests and integration tests are included. Integration tests spin up an embedded MongoDB (Flapdoodle) β€” no external database required.

# Run all tests
./mvnw test

# Run all tests + generate JaCoCo coverage report
./mvnw verify

Coverage report: target/site/jacoco/index.html


πŸ”„ CI

GitHub Actions runs ./mvnw -B verify on every push and pull request.

Workflow definition: .github/workflows/ci.yml


πŸ› οΈ Tech Stack

Technology Version
Java 21
Spring Boot 3.4.1
Spring Data MongoDB (managed by Boot)
Spring Security (optional, managed by Boot)
Lombok 1.18.38
Jackson Databind (managed by Boot)
Flapdoodle Embed Mongo 4.22.0 (test)
JaCoCo 0.8.14

πŸ“ License

This project is licensed under the MIT License.

About

A lightweight, annotation-driven toolkit that adds search, pagination, auditing, and soft-delete superpowers to any Spring Boot + MongoDB project.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages