Table of Contents

1.0 Repository

1.0.0 Repository Interface

Repository Interface

The Repository Interface sets a contract for CRUD operations, and aggregations. This is a shortened list of all available methods, please check the source for a complete list.

Parameters

All repositories must implement this method, which builds all parameters.

JsonObject buildParameters(Map<String, List<String>> queryMap,
                           Field[] fields, Method[] methods,
                           JsonObject errors,
                           Map<String, List<FilterParameter>> params, int[] limit,
                           Queue<OrderByParameter> orderByQueue,
                           String[] indexName);

Create

default void create(E record, Handler<AsyncResult<CreateResult<E>>> resultHandler) { ... }
default Future<CreateResult<E>> create(E record) { ... }
default void batchCreate(List<E> records, Handler<AsyncResult<List<CreateResult<E>>>> resultHandler) { ... }
default Future<List<CreateResult<E>>> batchCreate(List<E> records) { ... }

Read

Nearly all read operations use a JsonObject parameter called identifiers. This has a structure that is defined by the particular implementation you are using. e.g DynamoDBRepository generally looks something like this:

{
    "hash" : "string",
    "range" : "string"
}

This is up to the implementation and will be defined by them. In addition you can query for multiple ids on index endpoints like this:

{
    "multiple" : true,
    "ids" : ["string"]
}

Show

void read(JsonObject identifiers, Handler<AsyncResult<ItemResult<E>>> resultHandler);
default Future<ItemResult<E>> read(JsonObject identifiers) { ... }
void read(JsonObject identifiers, String[] projections, Handler<AsyncResult<ItemResult<E>>> resultHandler);
default void batchRead(Set<JsonObject> identifiers, String[] projections, Handler<AsyncResult<List<ItemResult<E>>>> resultHandler) { ... }
default void batchRead(Set<JsonObject> identifiers, Handler<AsyncResult<List<ItemResult<E>>>> resultHandler) { ... }
default Future<List<ItemResult<E>>> batchRead(List<JsonObject> identifiers) { ... }
default Future<List<ItemResult<E>>> batchRead(List<JsonObject> identifiers, String[] projections) { ... }

Some methods are intended for use with NoSQL stores and will ask you for consistency (supports depends on implementation):

default void read(JsonObject identifiers, boolean consistent, Handler<AsyncResult<ItemResult<E>>> resultHandler) { ... }
void read(JsonObject identifiers, boolean consistent, String[] projections, Handler<AsyncResult<ItemResult<E>>> resultHandler);

Index

Paginated:

default void readAll(String pageToken, Handler<AsyncResult<ItemListResult<E>>> resultHandler) { ... }
default Future<ItemListResult<E>> readAll(String pageToken) { ... }
default void readAll(JsonObject identifiers, QueryPack queryPack, Handler<AsyncResult<ItemListResult<E>>> resultHandler) { ... }
default Future<ItemListResult<E>> readAll(JsonObject identifiers, QueryPack queryPack) { ... }
void readAll(JsonObject identifiers, String pageToken, QueryPack queryPack, String[] projections, Handler<AsyncResult<ItemListResult<E>>> resultHandler);
default Future<ItemListResult<E>> readAll(JsonObject identifiers, String pageToken, QueryPack queryPack, String[] projections) { ... }
void readAll(String pageToken, QueryPack queryPack, String[] projections, Handler<AsyncResult<ItemListResult<E>>> resultHandler);
default Future<ItemListResult<E>> readAll(String pageToken, QueryPack queryPack, String[] projections) { ... }

Nonpaginated:

void readAll(Handler<AsyncResult<List<E>>> resultHandler);
default Future<List<E>> readAll() { ... }
void readAll(JsonObject identifiers, Map<String, List<FilterParameter>> filterParameterMap, Handler<AsyncResult<List<E>>> resultHandler);
default Future<List<E>> readAll(JsonObject identifiers, Map<String, List<FilterParameter>> filterParamterMap) { ... }
void readAllWithoutPagination(String identifier, Handler<AsyncResult<List<E>>> resultHandler);
default Future<List<E>> readAllWithoutPagination(String identifier) { ... }
void readAllWithoutPagination(String identifier, QueryPack queryPack, Handler<AsyncResult<List<E>>> resultHandler);
default Future<List<E>> readAllWithoutPagination(String identifier, QueryPack queryPack) { ... }
void readAllWithoutPagination(String identifier, QueryPack queryPack, String[] projections, Handler<AsyncResult<List<E>>> resultHandler);
default Future<List<E>> readAllWithoutPagination(String identifier, QueryPack queryPack, String[] projections) { ... }

Read Everything nonpaginated:

default void readAllWithoutPagination(QueryPack queryPack, Handler<AsyncResult<List<E>>> resultHandler) { ... }
default Future<List<E>> readAllWithoutPagination(QueryPack queryPack) { ... }
void readAllWithoutPagination(QueryPack queryPack, String[] projections, Handler<AsyncResult<List<E>>> resultHandler);
default Future<List<E>> readAllWithoutPagination(QueryPack queryPack, String[] projections) { ... }

Aggregations

default void aggregation(JsonObject identifiers, QueryPack queryPack, Handler<AsyncResult<String>> resultHandler);
default Future<String> aggregation(JsonObject identifiers, QueryPack queryPack);
void aggregation(JsonObject identifiers, QueryPack queryPack, String[] projections, Handler<AsyncResult<String>> resultHandler);
default Future<String> aggregation(JsonObject identifiers, QueryPack queryPack, String[] projections) { ... }

The string returned from Aggregation is the JSON representation, it has a similar format to this:

{
  "count": "<integer>",
  "results": [
      "groupByKey":"<valueOfGroupByKeyElement>",
      "<aggregateFunction>":"<aggregateValue>"
    ]
} 

Update

SQL and NoSQL

The repository is set up to deal with both. For those implementations that operate on optimistic locking, use this method to set an update Function which will be used when fetching a newer version when optimistically locking:

default void update(E record, Function<E, E> updateLogic, Handler<AsyncResult<UpdateResult<E>>> resultHandler) { .. }
default Future<UpdateResult<E>> update(E record, Function<E, E> updateLogic) { ... }

If the implementation you are using supports direct updates, like SQL stores you can use these methods:

default void update(E record, Handler<AsyncResult<UpdateResult<E>>> resultHandler) { ... }
default Future<UpdateResult<E>> update(E record) { ... }

For batch operations:

default void batchUpdate(Map<E, Function<E, E>> records, Handler<AsyncResult<List<UpdateResult<E>>>> resultHandler) { ... }
default Future<List<UpdateResult<E>>> batchUpdate(Map<E, Function<E, E>> records) { ... }
default void batchUpdate(List<E> records, Handler<AsyncResult<List<UpdateResult<E>>>> resultHandler) { ... }
default Future<List<UpdateResult<E>>> batchUpdate(List<E> records) { ... }

Delete

default void delete(JsonObject identifiers, Handler<AsyncResult<DeleteResult<E>>> resultHandler) { ... }
default Future<DeleteResult<E>> delete(JsonObject identifiers) { ... }
default void batchDelete(List<JsonObject> identifiers, Handler<AsyncResult<List<DeleteResult<E>>>> resultHandler) { ... }
default Future<List<DeleteResult<E>>> batchDelete(List<JsonObject> identifiers) { ... }

1.0.1 ETagManager

ETagManager

The ETagManager interface is used to manage etags. This can be passed into certain implementations of the Repository interface for automatically updating etags on mutable operations or it can be used standalone. It only accepts objects which implement both Method and ETagable interfaces.

The interface looks like this:

public interface ETagManager<E extends Model & ETagable> {
    void removeProjectionsEtags(int hash, Handler<AsyncResult<Boolean>> resultHandler);
    void destroyEtags(int hash, Handler<AsyncResult<Boolean>> resultHandler);
    void replaceAggregationEtag(String etagItemListHashKey, String etagKey, String newEtag,
                                Handler<AsyncResult<Boolean>> resultHandler);

    void setSingleRecordEtag(Map<String, String> etagMap, Handler<AsyncResult<Boolean>> resultHandler);
    void setProjectionEtags(String[] projections, int hash, E item);
    void setItemListEtags(String etagItemListHashKey, String etagKey, ItemList<E> itemList, Future<Boolean> itemListEtagFuture);

    void checkItemEtag(String etagKeyBase, String key, String requestEtag, Handler<AsyncResult<Boolean>> resultHandler);
    void checkItemListEtag(String etagItemListHashKey, String etagKey, String etag, Handler<AsyncResult<Boolean>> resultHandler);
    void checkAggregationEtag(String etagItemListHashKey, String etagKey, String etag, Handler<AsyncResult<Boolean>> resultHandler);
}

Implementations

nannoq-repository comes with two premade ETagManagers, InMemoryEtagManagerImpl and RedisETagManagerImpl.

InMemoryEtagManagerImpl

This leverages the sharedData structure of Vertx. It uses a LocalMap if vertx is not clustered and a distributed AsyncMap if vert is clustered. This means multiple objects will access the same maps.

RedisETagManagerImpl

This leverages an external Redis store.

1.0.2 CacheManager

CacheManager Interface

The CacheManager is used to manage caches for nannoq-repository, and has multiple implementations. Like the ETagManager it is designed to be used by a repository implementation, but can be used standalone. This is the interface definition:

public interface CacheManager<E extends Cacheable & Model> {
    void initializeCache(Handler<AsyncResult<Boolean>> resultHandler);
    void checkObjectCache(String cacheId, Handler<AsyncResult<E>> resultHandler);
    void checkItemListCache(String cacheId, String[] projections, Handler<AsyncResult<ItemList<E>>> resultHandler);
    void checkAggregationCache(String cacheKey, Handler<AsyncResult<String>> resultHandler);

    void replaceCache(Future<Boolean> writeFuture, List<E> records,
                      Function<E, String> shortCacheIdSupplier,
                      Function<E, String> cacheIdSupplier);

    void replaceObjectCache(String cacheId, E item, Future<E> future, String[] projections);
    void replaceItemListCache(String content, Supplier<String> cacheIdSupplier,
                              Handler<AsyncResult<Boolean>> resultHandler);
    void replaceAggregationCache(String content, Supplier<String> cacheIdSupplier,
                                 Handler<AsyncResult<Boolean>> resultHandler);

    void purgeCache(Future<Boolean> future, List<E> records, Function<E, String> cacheIdSupplier);

    Boolean isObjectCacheAvailable();
    Boolean isItemListCacheAvailable();
    Boolean isAggregationCacheAvailable();
}

Implementations

nannoq-repository comes with two premade CacheManagers, ClusterCacheManagerImpl and LocalCacheManagerImpl.

ClusterCacheManagerImpl

This leverages JCache to establish a distibuted cache on the vertx cluster. It will look for a configuration for the following three caches:

If they are not found (in cluster.xml for hazelcast f.ex) it will create caches with default settings corresponding to those names.

LocalCacheManagerImpl

This leverages a LocalMap extracted from vertx’s sharedData for all three caches.

1.0.3 RedisUtils

RedisUtils

By passing in a JsonObject with a “redis_host” field you can get a RedisClient.

public static RedisClient getRedisClient(Vertx vertx, JsonObject config) { ... }

You can also supply an optional “redis_port”.

1.1 Data Model

1.1.0 Model

Model Interface

These are the basic methods of the Model interface that must be implemented by all models. There is no magic here, relatively standard model definition.

public interface Model {
    Model setModifiables(Model newObject);

    @Fluent
    Model sanitize();
    List<ValidationError> validateCreate();
    List<ValidationError> validateUpdate();

    Model setIdentifiers(JsonObject identifiers);

    Date getCreatedAt();
    Model setCreatedAt(Date date);
    Date getUpdatedAt();
    Model setUpdatedAt(Date date);

    @Fluent
    Model setInitialValues(Model record);

    JsonObject toJsonFormat(@Nonnull String[] projections);

    ...
}

1.1.1 ETagable

ETagable Interface

This interface asks for a setter and getter for etags, and is not directly concerned with how you store and/or generate it.

String getEtag();
ETagable setEtag(String etag);

It also has a default implementation to construct the etag of the object, which ends up in a map which also includes and subobjects that are also ETagable. This can be overriden if you want a hardcoded version of this.

default Map<String, String> generateAndSetEtag(Map<String, String> map) { ... }

The last method to override is for generating the unique etag identifier for a particular object:

String generateEtagKeyIdentifier();

1.1.2 Cacheable

Cacheable Interface

This is a non-implement interface that creates a cache identifier, that is overrideable if you have a class define two separate logical components. i.e. a Comment class that doubles as a Reply.

public interface Cacheable {
    @DynamoDBIgnore
    @JsonIgnore
    default String getCachePartitionKey() {
        return getClass().getSimpleName();
    }
}

1.1.3 ModelUtils

ModelUtils

This is a simple class, currently for creating etags.

public static String hashString(String stringToHash) throws NoSuchAlgorithmException { ... }
public static String returnNewEtag(long tag) { ... }

1.2 Implementations

1.2.1 DynamoDBRepository

1.2.1.0 Implementation

Implementation

Initializing a dynamodbrepository for a chosen Type is easy:

DynamoDBRepository<TestModel> repo = new DynamoDBRepository<>(TestModel.class, new JsonObject());

The JsonObject is a configuration object, that expects the following values:

{
    "dynamodb_endpoint" : "<string>"
}

Simple, right? This is the default though. If you want to leverage the S3 capabilities of the repository you will have to add some additional things:

{
    "dynamo_endpoint" : "<string>",
    "dynamo_db_iam_id" : "<string>",
    "dynamo_db_iam_key" : "<string>",
    "content_bucket" : "<string>"
}

This intializes the dynamodbrepo for S3 operations. This id and key should ofc correspond to an AWS IAM account that has correct access privileges to the specified bucket.

S3 specific logic is found in these methods:

public DynamoDBMapper getDynamoDbMapper() {
    return DYNAMO_DB_MAPPER;
}

public static S3Link createS3Link(DynamoDBMapper dynamoDBMapper, String path) {
    return dynamoDBMapper.createS3Link(Region.EU_Ireland, S3BucketName, path);
}

public static String createSignedUrl(DynamoDBMapper dynamoDBMapper, S3Link file) {
    return createSignedUrl(dynamoDBMapper, 7, file);
}

public static String createSignedUrl(DynamoDBMapper dynamoDBMapper, int days, S3Link file) { ... }

If you have not added a serializer/deserializer for the S3Link class you can use this method to prepare that, calling it once is enough for all dynamodbrepos.

public static void initializeDynamoDb(JsonObject appConfig, Map<String, Class> collectionMap, Handler<AsyncResult<Void>> resultHandler) {

This will initialize the Jackson module for S3Link and you can use the collectionMap parameter to create any tables you need by settings the tableNames, matched with the class you are creating the table for, e.g:

collectionMap.put("feedItems", FeedItem.class);

ETagManager

If you would like to leverage redis for etags you should include the following to the config:

{
    "redis_host" : "<string>",
    "redis_port" : "<integer>" //optional
}

Custom Managers

If you want to use custom implementations of the cluster manager or the etag manager you can add those in the constructor:

public DynamoDBRepository(Class<E> type, JsonObject appConfig, @Nullable CacheManager<E> cacheManager) { ... }
public DynamoDBRepository(Class<E> type, JsonObject appConfig, @Nullable ETagManager<E> eTagManager) { ... }
public DynamoDBRepository(Class<E> type, JsonObject appConfig, @Nullable CacheManager<E> cacheManager, @Nullable ETagManager<E> eTagManager) { ... }

Constructors

If you are not on a Vert.x context when constructing the repos you can pass in vertx as the first parameter.

Queries

On paged queries the DynamoDBRepository expects a Local Secondary Index called PAGINATION_INDEX.

You can also do queries on Global Secondary Indexes with these implementation specific methods:

public void readAll(JsonObject identifiers, QueryPack queryPack, String GSI, Handler<AsyncResult<ItemListResult<E>>> asyncResultHandler) { ... }
public void readAll(JsonObject identifiers, String pageToken, QueryPack queryPack, String[] projections, String GSI, Handler<AsyncResult<ItemListResult<E>>> asyncResultHandler) { ... }
public void aggregation(JsonObject identifiers, QueryPack queryPack, String GSI, Handler<AsyncResult<String>> resultHandler) { ... }
public void aggregation(JsonObject identifiers, QueryPack queryPack, String[] projections, String GSI, Handler<AsyncResult<String>> resultHandler) { ... }

1.2.1.1 DynamoDBModel

DynamoDBModel Interface

This is a simple contract requirement for operationg with the DynamoDBRepository, to return hash and range values for an object. If the model does not have a range key, this will be identified by DynamoDBRepository in which case the getRange() method is redundant, and can just return null.

public interface DynamoDBModel {
    @JsonIgnore
    @DynamoDBIgnore
    String getHash();
    @JsonIgnore
    @DynamoDBIgnore
    String getRange();

    @Fluent
    DynamoDBModel setHash(String hash);

    @Fluent
    DynamoDBModel setRange(String range);
}

The following is an example of a fully implemented TestModel class for use with DynamoDBRepository, and a embeddable class which is a DynamoDBDocument.

@DynamoDBTable(tableName="testModels")
@DataObject(generateConverter = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TestModel implements DynamoDBModel, Model, ETagable, Cacheable {
    private String etag;
    private String someStringOne;
    private String someStringTwo;
    private String someStringThree;
    private String someStringFour;
    private Date someDate;
    private Date someDateTwo;
    private Long someLong;
    private Long someLongTwo;
    private Integer someInteger;
    private Integer someIntegerTwo;
    private Boolean someBoolean;
    private Boolean someBooleanTwo;
    private List<TestDocument> documents;
    private Date createdAt;
    private Date updatedAt;
    private Long version;

    public TestModel() {

    }

    public TestModel(JsonObject jsonObject) {
        fromJson(jsonObject, this);

        someDate = jsonObject.getLong("someDate") == null ? null : new Date(jsonObject.getLong("someDate"));
        someDateTwo = jsonObject.getLong("someDateTwo") == null ? null : new Date(jsonObject.getLong("someDateTwo"));
        createdAt = jsonObject.getLong("createdAt") == null ? null : new Date(jsonObject.getLong("createdAt"));
        updatedAt = jsonObject.getLong("updatedAt") == null ? null : new Date(jsonObject.getLong("updatedAt"));
    }

    public JsonObject toJson() {
        return JsonObject.mapFrom(this);
    }

    @Override
    public TestModel setIdentifiers(JsonObject identifiers) {
        setHash(identifiers.getString("hash"));
        setRange(identifiers.getString("range"));

        return this;
    }

    @DynamoDBHashKey
    public String getSomeStringOne() {
        return someStringOne;
    }

    @Fluent
    public TestModel setSomeStringOne(String someStringOne) {
        this.someStringOne = someStringOne;

        return this;
    }

    @DynamoDBRangeKey
    public String getSomeStringTwo() {
        return someStringTwo;
    }

    @Fluent
    public TestModel setSomeStringTwo(String someStringTwo) {
        this.someStringTwo = someStringTwo;

        return this;
    }

    @DynamoDBIndexHashKey(globalSecondaryIndexName = "TEST_GSI")
    public String getSomeStringThree() {
        return someStringThree;
    }

    @Fluent
    public TestModel setSomeStringThree(String someStringThree) {
        this.someStringThree = someStringThree;

        return this;
    }

    public String getSomeStringFour() {
        return someStringFour;
    }

    @Fluent
    public TestModel setSomeStringFour(String someStringFour) {
        this.someStringFour = someStringFour;

        return this;
    }

    @DynamoDBIndexRangeKey(localSecondaryIndexName = PAGINATION_INDEX)
    public Date getSomeDate() {
        return someDate;
    }

    @Fluent
    public TestModel setSomeDate(Date someDate) {
        this.someDate = someDate;

        return this;
    }

    @DynamoDBIndexRangeKey(globalSecondaryIndexName = "TEST_GSI")
    public Date getSomeDateTwo() {
        return someDateTwo;
    }

    @Fluent
    public TestModel setSomeDateTwo(Date someDateTwo) {
        this.someDateTwo = someDateTwo;

        return this;
    }

    public Long getSomeLong() {
        return someLong != null ? someLong : 0L;
    }

    @Fluent
    public TestModel setSomeLong(Long someLong) {
        this.someLong = someLong;

        return this;
    }

    public Long getSomeLongTwo() {
        return someLongTwo != null ? someLongTwo : 0L;
    }

    @Fluent
    public TestModel setSomeLongTwo(Long someLongTwo) {
        this.someLongTwo = someLongTwo;

        return this;
    }

    public Integer getSomeInteger() {
        return someInteger != null ? someInteger : 0;
    }

    @Fluent
    public TestModel setSomeInteger(Integer someInteger) {
        this.someInteger = someInteger;

        return this;
    }

    public Integer getSomeIntegerTwo() {
        return someIntegerTwo != null ? someIntegerTwo : 0;
    }

    @Fluent
    public TestModel setSomeIntegerTwo(Integer someIntegerTwo) {
        this.someIntegerTwo = someIntegerTwo;

        return this;
    }

    public Boolean getSomeBoolean() {
        return someBoolean != null ? someBoolean : Boolean.FALSE;
    }

    @Fluent
    public TestModel setSomeBoolean(Boolean someBoolean) {
        this.someBoolean = someBoolean;

        return this;
    }

    public Boolean getSomeBooleanTwo() {
        return someBooleanTwo != null ? someBooleanTwo : Boolean.FALSE;
    }

    @Fluent
    public TestModel setSomeBooleanTwo(Boolean someBooleanTwo) {
        this.someBooleanTwo = someBooleanTwo;

        return this;
    }

    public List<TestDocument> getDocuments() {
        return documents;
    }

    @Fluent
    public TestModel setDocuments(List<TestDocument> documents) {
        this.documents = documents;

        return this;
    }

    @DynamoDBVersionAttribute
    public Long getVersion() {
        return version;
    }

    @Fluent
    public TestModel setVersion(Long version) {
        this.version = version;

        return this;
    }

    @Override
    public String getHash() {
        return someStringOne;
    }

    @Override
    public String getRange() {
        return someStringTwo;
    }

    @Override
    @Fluent
    public TestModel setHash(String hash) {
        someStringOne = hash;

        return this;
    }

    @Override
    @Fluent
    public TestModel setRange(String range) {
        someStringTwo = range;

        return this;
    }

    @Override
    public String getEtag() {
        return etag;
    }

    @Fluent
    @Override
    public TestModel setEtag(String etag) {
        this.etag = etag;

        return this;
    }

    @Override
    public String generateEtagKeyIdentifier() {
        return getSomeStringOne() != null && getSomeStringTwo() != null ?
                "data_api_testModel_etag_" + getSomeStringOne() + "_" + getSomeStringTwo() :
                "NoTestModelEtag";
    }

    @Override
    public TestModel setModifiables(Model newObject) {
        return this;
    }

    @Override
    public TestModel sanitize() {
        return this;
    }

    @Override
    public List<ValidationError> validateCreate() {
        return Collections.emptyList();
    }

    @Override
    public List<ValidationError> validateUpdate() {
        return Collections.emptyList();
    }

    @Override
    public Date getCreatedAt() {
        return createdAt != null ? createdAt : new Date();
    }

    @Override
    @Fluent
    public TestModel setCreatedAt(Date date) {
        createdAt = date;

        return this;
    }

    @Override
    public Date getUpdatedAt() {
        return updatedAt != null ? updatedAt : new Date();
    }

    @Override
    @Fluent
    public TestModel setUpdatedAt(Date date) {
        updatedAt = date;

        return this;
    }

    @Override
    @Fluent
    public TestModel setInitialValues(Model record) {
        return this;
    }

    @Override
    public JsonObject toJsonFormat(@Nonnull String[] projections) {
        return new JsonObject(Json.encode(this));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestModel testModel = (TestModel) o;

        return Objects.equals(getSomeStringOne(), testModel.getSomeStringOne()) &&
                Objects.equals(getSomeStringTwo(), testModel.getSomeStringTwo()) &&
                Objects.equals(getSomeStringThree(), testModel.getSomeStringThree()) &&
                Objects.equals(getSomeStringFour(), testModel.getSomeStringFour()) &&
                Objects.equals(getSomeDate(), testModel.getSomeDate()) &&
                Objects.equals(getSomeDateTwo(), testModel.getSomeDateTwo()) &&
                Objects.equals(getSomeLong(), testModel.getSomeLong()) &&
                Objects.equals(getSomeLongTwo(), testModel.getSomeLongTwo()) &&
                Objects.equals(getSomeInteger(), testModel.getSomeInteger()) &&
                Objects.equals(getSomeIntegerTwo(), testModel.getSomeIntegerTwo()) &&
                Objects.equals(getSomeBoolean(), testModel.getSomeBoolean()) &&
                Objects.equals(getSomeBooleanTwo(), testModel.getSomeBooleanTwo()) &&
                Objects.equals(getDocuments(), testModel.getDocuments()) &&
                Objects.equals(getCreatedAt(), testModel.getCreatedAt()) &&
                Objects.equals(getUpdatedAt(), testModel.getUpdatedAt()) &&
                Objects.equals(getVersion(), testModel.getVersion());
    }

    @Override
    public int hashCode() {
        return Objects.hash(someStringOne, someStringTwo, someStringThree, someStringFour, someDate, someDateTwo,
                someLong, someLongTwo, someInteger, someIntegerTwo, someBoolean, someBooleanTwo, documents, createdAt,
                updatedAt, version);
    }
}

TestDocument:

@DynamoDBDocument
@DataObject(generateConverter = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TestDocument implements ETagable {
    private String etag;
    private String someStringOne;
    private String someStringTwo;
    private String someStringThree;
    private String someStringFour;
    private Long version;

    public TestDocument() {

    }

    public TestDocument(JsonObject jsonObject) {
        fromJson(jsonObject, this);
    }

    public JsonObject toJson() {
        return JsonObject.mapFrom(this);
    }

    public String getSomeStringOne() {
        return someStringOne;
    }

    public void setSomeStringOne(String someStringOne) {
        this.someStringOne = someStringOne;
    }

    public String getSomeStringTwo() {
        return someStringTwo;
    }

    public void setSomeStringTwo(String someStringTwo) {
        this.someStringTwo = someStringTwo;
    }

    public String getSomeStringThree() {
        return someStringThree;
    }

    public void setSomeStringThree(String someStringThree) {
        this.someStringThree = someStringThree;
    }

    public String getSomeStringFour() {
        return someStringFour;
    }

    public void setSomeStringFour(String someStringFour) {
        this.someStringFour = someStringFour;
    }

    @DynamoDBVersionAttribute
    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

    @Override
    public String getEtag() {
        return etag;
    }

    @Override
    public TestDocument setEtag(String etag) {
        this.etag = etag;

        return this;
    }

    @Override
    public String generateEtagKeyIdentifier() {
        return getSomeStringOne() != null ? "data_api_testDocument_etag_" + getSomeStringOne() : "NoDocumentTag";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestDocument that = (TestDocument) o;

        return Objects.equals(getSomeStringOne(), that.getSomeStringOne()) &&
                Objects.equals(getSomeStringTwo(), that.getSomeStringTwo()) &&
                Objects.equals(getSomeStringThree(), that.getSomeStringThree()) &&
                Objects.equals(getSomeStringFour(), that.getSomeStringFour()) &&
                Objects.equals(getVersion(), that.getVersion());
    }

    @Override
    public int hashCode() {
        return Objects.hash(someStringOne, someStringTwo, someStringThree, someStringFour, version);
    }
}

1.2.1.2 ImageUploader

ImageUploader Interface

This interface provides a helper for converting images to JPG with 1.0 compression and storing it to a specific S3Link class, i.e. storing it in a S3 bucket.

File upload has three options:

default void doUpload(Vertx vertx, File file, Supplier<S3Link> s3LinkSupplier, Future<Boolean> fut) { ... }
default void doUpload(Vertx vertx, FileUpload file, Supplier<S3Link> s3LinkSupplier, Future<Boolean> fut) { ... }
default void doUpload(Vertx vertx, String url, Supplier<S3Link> s3LinkSupplier, Future<Boolean> fut) { ... }

1.2.1.3 CachedContent

CachedContent Interface

This is a helper interface that asks you to implement these two methods:

S3Link getContentLocation();
void setContentLocation(S3Link s3Link);

At that point you can call this method on those objects to store any content accessible by url to the supplied contentlocation S3Link.

default void storeContent(Vertx vertx, String urlToContent, String bucketPath, Handler<AsyncResult<Boolean>> resultHandler) { ... }

1.3 Querying

1.3.0 QueryPack

QueryPack

The QueryPack class is used for doing queries on Repository objects.

It’s primarily concerned with these fields:

...

    private String pageToken;
    private String requestEtag;
    private Queue<OrderByParameter> orderByQueue;
    private Map<String, List<FilterParameter>> params;
    private AggregateFunction aggregateFunction;
    private String[] projections;
    private String indexName;
    private Integer limit;

...

Which are set using its builder interface which can optionally accept a Class. This is useful for setting a unique “route” when you are not accepting a query from an API interface, e.g:

QueryPack queryPack = QueryPack.builder(TYPE)
        .withPageToken(pageToken)
        .withRequestEtag(etag)
        .withOrderByQueue(orderByQueue)
        .withFilterParameters(params)
        .withAggregateFunction(aggregateFunction)
        .withProjections(projections)
        .withIndexName(indexName)
        .withLimit(limit)
        .build();

All QueryPack builder methods are optional, and you can also add FilterParameters directly with the following methods:

public QueryPackBuilder addFilterParameter(String field, FilterParameter param) { ... }
public QueryPackBuilder addFilterParameters(String field, List<FilterParameter> parameters) { ... }

Execution of Query Parameters

The QueryPack operates in order. So if you want to build complex boolean statements you should add the filterparameters in the order they would naturally follow.

For OrderByParameters they too are processed in insertion order.

ETags and Caches

Every specific configuration of a querypack is unique and will produce different etags and caches.


Vert.x Web Integration

The builder has a method called

public QueryPackBuilder withRoutingContext(RoutingContext routingContext) { ... }

This method will automatically set the pageToken based on an available pageToken param in the query and will also set the ETag from the request based on the “If-None-Match” HTTP Header.

1.3.1 FilterParameter

FilterParameter

You can filter on fields with the following parameters:


The FilterParameter class defines a filtering specification for a field. It is easily constructed like this:

FilterParameter.builder("someBoolean")
    .withEq("true")
    .build()

If you want to construct more complex combinations you can set the type of the specification as well. The default is AND. e.g:

someBoolean == true AND someOtherBoolean == false


This would set a filterparameter to OR, and OR on the next parameter in the list supplied to a QueryPack.

FilterParameter.builder("someBoolean")
    .withEq("true")
    .withType("or")
    .build()

1.3.2 OrderByParameter

OrderByParameter

This class is used to define ordering on a field of a model. Available options are:

An OrderByParameter is defined easily like this:

OrderByParameter.builder()
    .withField("someLong")
    .withDirection("asc")
    .build()

Direction is DESC by default.

1.3.3 AggregateFunction

AggregateFunction

The AggregateFunction class is used to define aggregation on a model.

Available functions are the following:

It also supports grouping, normal and ranged. It supports grouping three levels deep.


Constructing the AggregateFunction follows the same builder interface as other parameters:

AggregateFunction aggregateFunction = AggregateFunction.builder()
        .withAggregateFunction(MAX)
        .withField("someLong")
        .build();

You can either set groupings explicitly

AggregateFunction aggregateFunction = AggregateFunction.builder()
        .withAggregateFunction(MAX)
        .withField("someLong")
        .withGroupBy(Collections.singletonList(GroupingConfiguration.builder()
                .withGroupBy("someLong")
                .withGroupByUnit("INTEGER")
                .withGroupByRange(10000)
                .build()))
        .build();

, or add them one by one.

AggregateFunction aggregateFunction = AggregateFunction.builder()
        .withAggregateFunction(MAX)
        .withField("someLong")
        .addGroupBy(GroupingConfiguration.builder()
               .withGroupBy("someLong")
               .withGroupByUnit("INTEGER")
               .withGroupByRange(10000)
               .build())
        .build();

1.3.4 Projection

Projection

Projection is very simple. You pass in a String array containing the fields you want, and those fields will be returned to you. Exact response might differ between implementations.