You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a typed entity history API on Registry that wraps ChaincodeStub.getHistoryForKey with entity deserialization, primary-key resolution, and a small immutable record. Purely additive on top of main: no changes to HypernateContext, middlewares, annotations, or StubMiddleware API.
Motivation
ChaincodeStub.getHistoryForKey returns QueryResultsIterator<KeyModification> keyed by raw composite key strings, and each KeyModification carries raw byte[]. Today every contract that needs audit/history has to:
Rebuild the composite key from entity metadata by hand.
Iterate QueryResultsIterator<KeyModification>.
UTF-8 + JSON-deserialize each non-deleted payload into a DTO.
Track isDeleted() and unwrap Instant / txId manually.
This is the same shape as mustRead/tryRead/readAll — but history is the one CRUD-adjacent operation that has no entity-aware wrapper. Logging (#34/#105) already covers observability of getHistoryForKey; what is missing is a developer-facing API.
Proposal
New public method on Registry:
public <T> List<EntityHistory<T>> history(Class<T> clazz, Object... keyParts);
New record in hu.bme.mit.ftsrg.hypernate.registry:
List<EntityHistory<Asset>> history = registry.history(Asset.class, assetId);
for (EntityHistory<Asset> entry : history) {
if (entry.deleted()) {
log.info("{} deleted at {} by tx {}", assetId, entry.timestamp(), entry.txId());
} else {
log.info("{} modified at {}: {}", assetId, entry.timestamp(), entry.value());
}
}
Behavior
Situation
Result
clazz not annotated with @PrimaryKey
MissingPrimaryKeysException
keyParts.length != primaryKeyCount
IllegalArgumentException
Key has never been written
Empty List (not an error)
Write modification
EntityHistory(txId, ts, false, deserialized)
Delete modification
EntityHistory(txId, ts, true, null)
Malformed JSON on a historical payload
SerializationException propagates
A single flat method is proposed instead of mustHistory / tryHistory because, unlike read, an empty history is a valid, non-exceptional state — a fresh key, or a key that never existed, returns an empty iterator from Fabric. This matches the shape of readAll.
Design
Package layout
lib/src/main/java/hu/bme/mit/ftsrg/hypernate/registry/
├── EntityHistory.java # NEW
└── Registry.java # +1 public method, +1 private helper in EntityUtil
lib/src/test/java/hu/bme/mit/ftsrg/hypernate/
└── RegistryTest.java # +1 assertion in the existing "do anything" test, +1 @Nested class
Everything stays under registry/, consistent with the existing exception siblings (EntityNotFoundException, EntityExistsException, SerializationException).
Reuse
Key generation, type resolution, and JSON deserialization reuse the existing private EntityUtil:
EntityUtil.getType(clazz) — same as mustRead.
EntityUtil.mapKeyPartsToString(clazz, keyParts) — same as mustRead.
EntityUtil.fromBuffer(buffer, clazz) — same as mustRead and readAll.
A small new helper maps KeyModification to EntityHistory<T>:
The iteration follows the same StreamSupport + Collectors.toList() shape as readAll.
Errors
Errors reuse the existing hierarchy rooted at DataAccessException / HypernateException. No new exception types are introduced — this feature does not add a genuinely new failure mode beyond what mustRead already distinguishes.
Logging
Registry is already @Loggable(Loggable.DEBUG) at the class level — the new method is covered.
Summary
Add a typed entity history API on
Registrythat wraps ChaincodeStub.getHistoryForKey with entity deserialization, primary-key resolution, and a small immutable record. Purely additive on top ofmain: no changes toHypernateContext, middlewares, annotations, orStubMiddlewareAPI.Motivation
ChaincodeStub.getHistoryForKey returns
QueryResultsIterator<KeyModification>keyed by raw composite key strings, and eachKeyModificationcarries rawbyte[]. Today every contract that needs audit/history has to:QueryResultsIterator<KeyModification>.isDeleted()and unwrapInstant/txIdmanually.This is the same shape as
mustRead/tryRead/readAll— but history is the one CRUD-adjacent operation that has no entity-aware wrapper. Logging (#34/#105) already covers observability of getHistoryForKey; what is missing is a developer-facing API.Proposal
New public method on
Registry:New record in
hu.bme.mit.ftsrg.hypernate.registry:Developer experience:
Behavior
clazznot annotated with@PrimaryKeyMissingPrimaryKeysExceptionkeyParts.length != primaryKeyCountIllegalArgumentExceptionList(not an error)EntityHistory(txId, ts, false, deserialized)EntityHistory(txId, ts, true, null)SerializationExceptionpropagatesA single flat method is proposed instead of
mustHistory/tryHistorybecause, unlikeread, an empty history is a valid, non-exceptional state — a fresh key, or a key that never existed, returns an empty iterator from Fabric. This matches the shape ofreadAll.Design
Package layout
Everything stays under
registry/, consistent with the existing exception siblings (EntityNotFoundException,EntityExistsException,SerializationException).Reuse
Key generation, type resolution, and JSON deserialization reuse the existing private
EntityUtil:EntityUtil.getType(clazz)— same asmustRead.EntityUtil.mapKeyPartsToString(clazz, keyParts)— same asmustRead.EntityUtil.fromBuffer(buffer, clazz)— same asmustReadandreadAll.A small new helper maps
KeyModificationtoEntityHistory<T>:The iteration follows the same
StreamSupport+Collectors.toList()shape asreadAll.Errors
Errors reuse the existing hierarchy rooted at
DataAccessException/HypernateException. No new exception types are introduced — this feature does not add a genuinely new failure mode beyond whatmustReadalready distinguishes.Logging
Registryis already@Loggable(Loggable.DEBUG)at the class level — the new method is covered.No new logging plumbing required.
Middleware interaction
None required:
CachingStubMiddleware(GeneralizeWriteBackCachedChaincodeStubMiddleware#35 redesign) already treats getHistoryForKey as pass-through — history reflects committed block history, not in-flight cache state.Compatibility
build.gradle.HypernateContext,HypernateContract,StubMiddleware,util.JSON, or any existing exception.EntityHistory) + one new public method onRegistry.Tests
Follows the existing
RegistryTestconventions (@DisplayNameGeneration(ReplaceUnderscores.class), nestedwhen_<method>classes, BDDMockitogiven/then):given_entity_without_primary_keys_when_doing_anything_then_throws_exceptionwith ahistoryassertion.@Nested class when_history:given_insufficient_key_parts_then_throw_illegal_argumentgiven_empty_history_then_return_empty_listgiven_write_modification_then_return_populated_entrygiven_delete_modification_then_value_is_null_and_deleted_truegiven_malformed_payload_then_throw_serializationSummary of changes
registry/EntityHistory.java(record:txId,timestamp,deleted,value)history(Class, Object...)andEntityUtil#toEntityHistorytest/.../RegistryTest.java— extend the "do anything" test and addwhen_historynested class