diff --git a/contracts/platform/iotcontractplatform/ctasset.go b/contracts/platform/iotcontractplatform/ctasset.go index f12aaf8..b61af7e 100644 --- a/contracts/platform/iotcontractplatform/ctasset.go +++ b/contracts/platform/iotcontractplatform/ctasset.go @@ -230,7 +230,7 @@ func (c *AssetClass) DeleteAsset(stub shim.ChaincodeStubInterface, args []string log.Errorf(err.Error()) return nil, err } - err = removeOneAssetFromWorldState(stub, assetKey) + err = arg.removeOneAssetFromWorldState(stub) if err != nil { err := fmt.Errorf("DeleteAsset: removeOneAssetFromWorldState class %s, asset %s, returned error: %s", c.Name, assetKey, err) log.Errorf(err.Error()) @@ -243,7 +243,12 @@ func (c *AssetClass) DeleteAsset(stub shim.ChaincodeStubInterface, args []string func (c *AssetClass) DeleteAllAssets(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) { var filter StateFilter - filter = getUnmarshalledStateFilter(stub, "DeleteAllAssets", args) + filter, err := getUnmarshalledStateFilter(stub, args) + if err != nil { + err = fmt.Errorf("DeleteAllAssets failed to get the filter: %s", err) + log.Error(err) + return nil, err + } iter, err := stub.RangeQueryState(c.Prefix, c.Prefix+"}") if err != nil { err = fmt.Errorf("DeleteAllAssets failed to get a range query iterator: %s", err) @@ -265,8 +270,8 @@ func (c *AssetClass) DeleteAllAssets(stub shim.ChaincodeStubInterface, args []st log.Errorf(err.Error()) return nil, err } - if len(filter.Entries) == 0 || state.Filter(filter) { - err = removeOneAssetFromWorldState(stub, key) + if state.Filter(filter) { + err = state.removeOneAssetFromWorldState(stub) if err != nil { err = fmt.Errorf("DeleteAllAssets removeOneAssetFromWorldState for asset %s failed: %s", key, err) log.Errorf(err.Error()) @@ -410,7 +415,12 @@ func (c AssetClass) ReadAllAssetsUnmarshalled(stub shim.ChaincodeStubInterface, var err error var filter StateFilter - filter = getUnmarshalledStateFilter(stub, "ReadAllAssetsUnmarshalled", args) + filter, err = getUnmarshalledStateFilter(stub, args) + if err != nil { + err = fmt.Errorf("readAllAssetsUnmarshalled failed to get a filter: %s", err) + log.Errorf(err.Error()) + return nil, err + } iter, err := stub.RangeQueryState(c.Prefix, c.Prefix+"}") if err != nil { @@ -433,7 +443,7 @@ func (c AssetClass) ReadAllAssetsUnmarshalled(stub shim.ChaincodeStubInterface, log.Errorf(err.Error()) return nil, err } - if len(filter.Entries) == 0 || state.Filter(filter) { + if state.Filter(filter) { assets = append(assets, *state) } } @@ -447,50 +457,15 @@ func (c AssetClass) ReadAllAssetsUnmarshalled(stub shim.ChaincodeStubInterface, return assets, nil } -// // ReadAssetHistory returns an asset's history from world state as an array -// func ReadAssetHistory(stub shim.ChaincodeStubInterface, args []string, assetName string, caller string) ([]byte, error) { -// argsMap, err := getUnmarshalledArgument(stub, caller, args) -// if err != nil { -// return nil, err -// } -// assetID, err := validateAssetID(caller, assetName, argsMap) -// if err != nil { -// return nil, err -// } -// stateHistory, err := h.ReadStateHistory(stub, assetID) -// if err != nil { -// return nil, err -// } -// // is count present? -// var olen int -// countBytes, found := GetObject(argsMap, "count") -// if found { -// olen = int(countBytes.(float64)) -// } -// if olen <= 0 || olen > len(stateHistory.AssetHistory) { -// olen = len(stateHistory.AssetHistory) -// } -// var hStatesOut = make([]interface{}, 0, olen) -// for i := 0; i < olen; i++ { -// var obj interface{} -// err = json.Unmarshal([]byte(stateHistory.AssetHistory[i]), &obj) -// if err != nil { -// log.Errorf("readAssetHistory JSON unmarshal of entry %d failed [%#v]", i, stateHistory.AssetHistory[i]) -// return nil, err -// } -// hStatesOut = append(hStatesOut, obj) -// } -// assetBytes, err := json.Marshal(hStatesOut) -// if err != nil { -// log.Errorf("readAssetHistory failed to marshal results: %s", err) -// return nil, err -// } - -// return []byte(assetBytes), nil -// } - //********** sort interface for AssetArray func (aa AssetArray) Len() int { return len(aa) } func (aa AssetArray) Swap(i, j int) { aa[i], aa[j] = aa[j], aa[i] } func (aa AssetArray) Less(i, j int) bool { return aa[i].AssetKey < aa[j].AssetKey } + +// ByTimestamp alias for sorting by timestamp +type ByTimestamp AssetArray + +func (aa ByTimestamp) Len() int { return len(aa) } +func (aa ByTimestamp) Swap(i, j int) { aa[i], aa[j] = aa[j], aa[i] } +func (aa ByTimestamp) Less(i, j int) bool { return (*aa[i].TXNTS).Before(*aa[j].TXNTS) } diff --git a/contracts/platform/iotcontractplatform/ctcontractstate.go b/contracts/platform/iotcontractplatform/ctcontractstate.go index 649187f..abc06ae 100644 --- a/contracts/platform/iotcontractplatform/ctcontractstate.go +++ b/contracts/platform/iotcontractplatform/ctcontractstate.go @@ -27,7 +27,7 @@ import ( ) // CONTRACTSTATEKEY is used to store contract state, including version, nickname and activeAssets -const CONTRACTSTATEKEY string = "IOTCP:ContractStateKey" +const CONTRACTSTATEKEY string = "IOTCP:ContractState" // ContractState struct defines contract state. Unlike the main contract maps, structs work fine // for this fixed structure. diff --git a/contracts/platform/iotcontractplatform/ctcrud.go b/contracts/platform/iotcontractplatform/ctcrud.go index e88f94b..75d557c 100644 --- a/contracts/platform/iotcontractplatform/ctcrud.go +++ b/contracts/platform/iotcontractplatform/ctcrud.go @@ -207,36 +207,35 @@ func (a *Asset) putMarshalledState(stub shim.ChaincodeStubInterface) error { return err } - // // add history state - // err = h.UpdateStateHistory(stub, assetID, string(stateJSON)) - // if err != nil { - // err = fmt.Errorf("%s: event %s assetID %s push history failed: %s", caller, eventName, assetID, err) - // log.Errorf(err.Error()) - // return err - // } + err = a.PUTAssetStateHistory(stub) + if err != nil { + err = fmt.Errorf("putMarshalledState failed to put asset %s history: %s", a.AssetKey, err) + log.Error(err) + return err + } return nil } // RemoveOneAssetFromWorldState remove the asset from world state -func removeOneAssetFromWorldState(stub shim.ChaincodeStubInterface, assetKey string) error { - err := stub.DelState(assetKey) +func (a *Asset) removeOneAssetFromWorldState(stub shim.ChaincodeStubInterface) error { + err := stub.DelState(a.AssetKey) + if err != nil { + err = fmt.Errorf("removeOneAssetFromWorldState: asset %s failed", a.AssetKey) + log.Error(err) + return err + } + err = a.RemoveAssetFromRecentStates(stub) if err != nil { - err = fmt.Errorf("removeOneAssetFromWorldState: asset %s failed", assetKey) + err = fmt.Errorf("removeOneAssetFromWorldState: asset %s could not be removed from recent states: %s", a.AssetKey, err) log.Error(err) return err } - err = RemoveAssetFromRecentStates(stub, assetKey) + err = a.DeleteAssetStateHistory(stub) if err != nil { - err = fmt.Errorf("removeOneAssetFromWorldState: asset %s could not be removed from recent states: %s", assetKey, err) + err = fmt.Errorf("putMarshalledState failed to put asset %s history: %s", a.AssetKey, err) log.Error(err) return err } - // err = h.DeleteStateHistory(stub, assetID) - // if err != nil { - // err = fmt.Errorf("%s: %s assetID %s history deletion failed", caller, assetName, assetID) - // log.Error(err) - // return err - // } return nil } diff --git a/contracts/platform/iotcontractplatform/ctfilters.go b/contracts/platform/iotcontractplatform/ctfilters.go index 8c2f87c..196927e 100644 --- a/contracts/platform/iotcontractplatform/ctfilters.go +++ b/contracts/platform/iotcontractplatform/ctfilters.go @@ -30,29 +30,33 @@ import ( type MatchType int32 const ( + // MatchDisabled causes the filter to not execute + MatchDisabled = 0 // MatchAll requires that every property in the filter be present and have // the same value - MatchAll MatchType = 0 + MatchAll MatchType = 1 // MatchAny requires that at least one property in the filter be present and have // the same value - MatchAny MatchType = 1 + MatchAny MatchType = 2 // MatchNone requires that every property in the filter either be present and have // a different value. or not be present - MatchNone MatchType = 2 + MatchNone MatchType = 3 ) // MatchName is a map of ID to name var MatchName = map[int]string{ - 0: "ALL", - 1: "ANY", - 2: "NONE", + 0: "n/a", + 1: "all", + 2: "any", + 3: "none", } // MatchValue is a map of name to ID var MatchValue = map[string]int32{ - "ALL": 0, - "ANY": 1, - "NONE": 2, + "n/a": 0, + "all": 1, + "any": 2, + "none": 3, } func (x MatchType) String() string { @@ -67,22 +71,37 @@ type QPropNV struct { Value string `json:"value"` } -// StateFilter is an array of QPropNV +// FilterGroup is a matchmode with a list of K:V pairs +type FilterGroup struct { + Match string `json:"match"` + Select []QPropNV `json:"select"` +} + +// StateFilter is a complete filter for a state type StateFilter struct { - MatchMode string `json:"match"` - Entries []QPropNV `json:"select"` + Filter FilterGroup `json:"filter"` } -var emptyFilter = StateFilter{"matchall", make([]QPropNV, 0)} +var emptyFilter = StateFilter{ + FilterGroup{ + "n/a", + make([]QPropNV, 0), + }, +} // Filter returns true if the filter's conditions are all met func (a *Asset) Filter(filter StateFilter) bool { - switch filter.MatchMode { - case "ALL": + if len(filter.Filter.Select) == 0 { + return true + } + switch filter.Filter.Match { + case "n/a": + return true + case "all": return matchAll(a, filter) - case "ANY": + case "any": return matchAny(a, filter) - case "NONE": + case "none": return matchNone(a, filter) default: err := fmt.Errorf("filterObject has unknown matchType in filter: %+v", filter) @@ -92,7 +111,7 @@ func (a *Asset) Filter(filter StateFilter) bool { } func matchAll(a *Asset, filter StateFilter) bool { - for _, f := range filter.Entries { + for _, f := range filter.Filter.Select { if !performOneMatch(a.State, f) { // must match all return false @@ -103,7 +122,7 @@ func matchAll(a *Asset, filter StateFilter) bool { } func matchAny(a *Asset, filter StateFilter) bool { - for _, f := range filter.Entries { + for _, f := range filter.Filter.Select { if performOneMatch(a.State, f) { // must match at least one return true @@ -114,7 +133,7 @@ func matchAny(a *Asset, filter StateFilter) bool { } func matchNone(a *Asset, filter StateFilter) bool { - for _, f := range filter.Entries { + for _, f := range filter.Filter.Select { if performOneMatch(a.State, f) { // must not match any return false @@ -164,23 +183,23 @@ func performOneMatch(obj *map[string]interface{}, prop QPropNV) bool { return false } -// Returns a map containing the JSON object represented by args[0] -func getUnmarshalledStateFilter(stub shim.ChaincodeStubInterface, caller string, args []string) StateFilter { +// Returns a filter found in the json object in args[0] +func getUnmarshalledStateFilter(stub shim.ChaincodeStubInterface, args []string) (StateFilter, error) { var filter StateFilter var err error if len(args) != 1 { // perfectly normal to not have a filter - return emptyFilter + return emptyFilter, nil } fBytes := []byte(args[0]) err = json.Unmarshal(fBytes, &filter) if err != nil { - err = fmt.Errorf("%s failed to unmarshal filter: %s error: %s", caller, args[0], err) - log.Errorf(err.Error()) - return emptyFilter + err = fmt.Errorf("getUnmarshalledStateFilter failed to unmarshal %s as filter, error: %s", args[0], err) + log.Error(err) + return emptyFilter, err } - return filter + return filter, nil } diff --git a/contracts/platform/iotcontractplatform/cthistory.go b/contracts/platform/iotcontractplatform/cthistory.go index acc110c..3f6e832 100644 --- a/contracts/platform/iotcontractplatform/cthistory.go +++ b/contracts/platform/iotcontractplatform/cthistory.go @@ -16,11 +16,19 @@ Kim Letkeman - Initial Contribution */ // v0.1 KL -- new iot chaincode platform +// v0.2 KL -- complete rewrite, history will be stored one state at a time so that +// it poses minimal additional burden on state writes, read will use an iterator, +// read will allow start and end time range, all, or last package iotcontractplatform import ( "encoding/json" + "fmt" + + "time" + + "sort" "github.com/hyperledger/fabric/core/chaincode/shim" ) @@ -30,87 +38,168 @@ import ( // STATEHISTORYKEY is used to separate history from current asset state and is prepended // to the assetID -const STATEHISTORYKEY string = "HIST." +const STATEHISTORYKEY string = "IOTCP.HIST." // + assetKey + '.' + txnts -// AssetStateHistory is used to hold the array of states as strings. +// AssetStateHistory is used to hold the output array of strings type AssetStateHistory struct { - AssetHistory []string `json:"assetHistory"` + AssetHistory []Asset `json:"assetHistory"` } -// CreateStateHistory creates a new history entry in the ledger for an asset. -func CreateStateHistory(stub shim.ChaincodeStubInterface, assetID string, stateJSON string) error { +// EmptyDateRange is used to determine that a range does not exist, which works in this +// case because the zero values of time are nonsense +var EmptyDateRange = DateRange{} - var ledgerKey = STATEHISTORYKEY + assetID - var assetStateHistory = AssetStateHistory{make([]string, 1)} - assetStateHistory.AssetHistory[0] = stateJSON +// DateRange allows a function to return states between a begin and end time, inclusive +type DateRange struct { + Range struct { + Begin string `json:"begin"` + End string `json:"end"` + } `json:"range"` +} - assetState, err := json.Marshal(&assetStateHistory) +// PUTAssetStateHistory write an Asset state with history key +func (a *Asset) PUTAssetStateHistory(stub shim.ChaincodeStubInterface) error { + historyKey := STATEHISTORYKEY + a.AssetKey + "." + a.TXNTS.Format(time.RFC3339Nano) + assetBytes, err := json.Marshal(a) if err != nil { + err = fmt.Errorf("Failed to marshal Asset for history: %s", err) + log.Error(err) return err } + err = stub.PutState(historyKey, assetBytes) + if err != nil { + err = fmt.Errorf("Failed to PUT Asset history: %s", err) + log.Error(err) + return err + } + return nil +} - return stub.PutState(ledgerKey, []byte(assetState)) +// DeleteAssetStateHistory deletes all history for an asset +func (a *Asset) DeleteAssetStateHistory(stub shim.ChaincodeStubInterface) error { -} + var historyKey = STATEHISTORYKEY + a.AssetKey + "." -// UpdateStateHistory adds a new state history for an asset. States are stored in the ledger -// in descending order by timestamp. AssetID is expected to by the *internal* -// assetID with a unique asset class prefix. -func UpdateStateHistory(stub shim.ChaincodeStubInterface, assetID string, stateJSON string) error { + iter, err := stub.RangeQueryState(historyKey, historyKey+"}") + if err != nil { + err = fmt.Errorf("DeleteAssetStateHistory failed to get a range query iterator: %s", err) + log.Errorf(err.Error()) + return err + } + defer iter.Close() + for iter.HasNext() { + key, _, err := iter.Next() + if err != nil { + err = fmt.Errorf("DeleteAssetStateHistory iter.Next() failed: %s", err) + log.Errorf(err.Error()) + return err + } + err = stub.DelState(key) + if err != nil { + err = fmt.Errorf("DeleteAssetStateHistory DelState for asset %s failed: %s ", key, err) + log.Errorf(err.Error()) + return err + } + } - var ledgerKey = STATEHISTORYKEY + assetID - var historyBytes []byte - var assetStateHistory AssetStateHistory + return nil +} - historyBytes, err := stub.GetState(ledgerKey) +// ReadAssetStateHistory gets the state history for an asset. +func (c *AssetClass) ReadAssetStateHistory(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) { + var assets AssetArray + var err error + var filter StateFilter + var dr DateRange + var arg = c.NewAsset() + var begin string + var end string + + if err = arg.unmarshallEventIn(stub, args); err != nil { + err := fmt.Errorf("ReadAssetStateHistory for class %s could not unmarshall, err is %s", c.Name, err) + log.Error(err) + return nil, err + } + assetKey, err := arg.getAssetKey() + if err != nil { + err = fmt.Errorf("ReadAssetStateHistory for class %s could not find id at %s, err is %s", c.Name, c.AssetIDPath, err) + log.Error(err) + return nil, err + } + + filter, err = getUnmarshalledStateFilter(stub, args) if err != nil { - // assume that this is a new asset. - return CreateStateHistory(stub, assetID, stateJSON) + err = fmt.Errorf("ReadAssetStateHistory failed while getting filter for %s %s, err is %s", c.Name, assetKey, err) + log.Error(err) + return nil, err } - err = json.Unmarshal(historyBytes, &assetStateHistory) + dr, err = getUnmarshalledDateRange(stub, args) if err != nil { - // assume that history is corrupted, so reset. - return CreateStateHistory(stub, assetID, stateJSON) + err = fmt.Errorf("ReadAssetStateHistory failed while getting daterange for %s %s, err is %s", c.Name, assetKey, err) + log.Error(err) + return nil, err + } + + if dr == EmptyDateRange { + begin = "" + end = "}" + } else { + begin = dr.Range.Begin + end = dr.Range.End + "}" } - var newSlice = make([]string, 0) - newSlice = append(newSlice, stateJSON) - newSlice = append(newSlice, assetStateHistory.AssetHistory...) - assetStateHistory.AssetHistory = newSlice + var historyKey = STATEHISTORYKEY + assetKey + "." - assetState, err := json.Marshal(&assetStateHistory) + iter, err := stub.RangeQueryState(historyKey+begin, historyKey+end) if err != nil { - return err + err = fmt.Errorf("ReadAssetStateHistory failed to get a range query iterator: %s", err) + log.Errorf(err.Error()) + return nil, err + } + defer iter.Close() + for iter.HasNext() { + key, assetBytes, err := iter.Next() + if err != nil { + err = fmt.Errorf("ReadAssetStateHistory iter.Next() failed: %s", err) + log.Errorf(err.Error()) + return nil, err + } + var state = new(Asset) + err = json.Unmarshal(assetBytes, state) + if err != nil { + err = fmt.Errorf("ReadAssetStateHistory unmarshal %s failed: %s", key, err) + log.Errorf(err.Error()) + return nil, err + } + if state.Filter(filter) { + assets = append(assets, *state) + } } - log.Debug("Update state history succedded for asset " + assetID) - return stub.PutState(ledgerKey, []byte(assetState)) -} + // return history, newest first + sort.Sort(sort.Reverse(ByTimestamp(assets))) -// DeleteStateHistory deletes all history for an asset from the ledger. -func DeleteStateHistory(stub shim.ChaincodeStubInterface, assetID string) error { - var ledgerKey = STATEHISTORYKEY + assetID - return stub.DelState(ledgerKey) + return json.Marshal(assets) } -// ReadStateHistory gets the state history for an asset. -func ReadStateHistory(stub shim.ChaincodeStubInterface, assetID string) (AssetStateHistory, error) { +// Returns a date range found in the json object in args[0] +func getUnmarshalledDateRange(stub shim.ChaincodeStubInterface, args []string) (DateRange, error) { + var dr DateRange + var err error - var ledgerKey = STATEHISTORYKEY + assetID - var assetStateHistory AssetStateHistory - var historyBytes []byte - - historyBytes, err := stub.GetState(ledgerKey) - if err != nil { - return AssetStateHistory{}, err + if len(args) == 0 { + // perfectly normal to not have no date range + return DateRange{}, nil } - err = json.Unmarshal(historyBytes, &assetStateHistory) + fBytes := []byte(args[0]) + err = json.Unmarshal(fBytes, &dr) if err != nil { - return AssetStateHistory{}, err + err = fmt.Errorf("getUnmarshalledDateRange failed to unmarshal %s as a date range, error: %s", args[0], err) + log.Error(err) + return EmptyDateRange, err } - return assetStateHistory, nil - + return dr, nil } diff --git a/contracts/platform/iotcontractplatform/ctmaps.go b/contracts/platform/iotcontractplatform/ctmaps.go index dbc1446..9615aa1 100644 --- a/contracts/platform/iotcontractplatform/ctmaps.go +++ b/contracts/platform/iotcontractplatform/ctmaps.go @@ -21,6 +21,7 @@ package iotcontractplatform import ( "encoding/json" "fmt" + "reflect" "sort" "strings" ) @@ -316,62 +317,36 @@ func GetObjectAsInteger(objIn *map[string]interface{}, qname string) (int, bool) return 0, false } -// Contains does its best to assert an array type on the incoming array -// and the matching type on the incoming val, and then searches for val -// in arr. +// Contains checks every element with a deepEqual func Contains(arr interface{}, val interface{}) bool { - switch t := arr.(type) { + switch arr.(type) { + case AlertNameArray: + arr2 := arr.(AlertNameArray) + for _, v := range arr2 { + return reflect.DeepEqual(v, val) + } case []string: arr2 := arr.([]string) for _, v := range arr2 { - if v == val { - return true - } + return reflect.DeepEqual(v, val) } case []int: arr2 := arr.([]int) for _, v := range arr2 { - if v == val { - return true - } + return reflect.DeepEqual(v, val) } case []float64: arr2 := arr.([]float64) for _, v := range arr2 { - if v == val { - return true - } + return reflect.DeepEqual(v, val) } case []interface{}: - //todo: try cast instead of assertion - //todo: use schema to determine if we even call this function or just add the value arr2 := arr.([]interface{}) for _, v := range arr2 { - switch tt := val.(type) { - case string: - if v.(string) == val.(string) { - return true - } - case int: - if v.(int) == val.(int) { - return true - } - case float64: - if v.(float64) == val.(float64) { - return true - } - case interface{}: - if v.(interface{}) == val.(interface{}) { - return true - } - default: - log.Errorf("Contains passed array containing unknown type: %+v\n", tt) - return false - } + return reflect.DeepEqual(v, val) } default: - log.Errorf("Contains passed array of unknown type: %+v\n", t) - return false + return reflect.DeepEqual(arr, val) } return false } diff --git a/contracts/platform/iotcontractplatform/ctrecent.go b/contracts/platform/iotcontractplatform/ctrecent.go index 51d0222..353ba2a 100644 --- a/contracts/platform/iotcontractplatform/ctrecent.go +++ b/contracts/platform/iotcontractplatform/ctrecent.go @@ -39,7 +39,7 @@ import ( // a begin. // RECENTSTATESKEY is used as key for recent states bucket -const RECENTSTATESKEY string = "IOTCP:RecentStates" +const RECENTSTATESKEY string = "IOTCP.RecentStates" // MaxRecentStates is an arbitrary limit on how many asset states we track across the // entire contract @@ -116,7 +116,7 @@ func (a *Asset) PushRecentState(stub shim.ChaincodeStubInterface) error { if assetPosn == -1 { // shift right rstates.States = append(rstates.States, a.AssetKey) - copy(rstates.States[1:MaxRecentStates], rstates.States[0:len(rstates.States)]) + copy(rstates.States[1:], rstates.States[0:]) } else { // shift right to close the gap copy(rstates.States[1:], rstates.States[0:assetPosn]) @@ -128,7 +128,7 @@ func (a *Asset) PushRecentState(stub shim.ChaincodeStubInterface) error { } // RemoveAssetFromRecentStates is called when an asset is deleted -func RemoveAssetFromRecentStates(stub shim.ChaincodeStubInterface, assetID string) error { +func (a *Asset) RemoveAssetFromRecentStates(stub shim.ChaincodeStubInterface) error { var rstates RecentStates var err error @@ -136,7 +136,7 @@ func RemoveAssetFromRecentStates(stub shim.ChaincodeStubInterface, assetID strin if err != nil { return err } - posn := findAssetInRecent(assetID, rstates) + posn := findAssetInRecent(a.AssetKey, rstates) if posn >= 0 { rstates.States = append(rstates.States[:posn], rstates.States[posn+1:]...) } diff --git a/contracts/platform/iotcontractplatform/ctrouter.go b/contracts/platform/iotcontractplatform/ctrouter.go index 2310c7d..0112b8f 100644 --- a/contracts/platform/iotcontractplatform/ctrouter.go +++ b/contracts/platform/iotcontractplatform/ctrouter.go @@ -139,7 +139,7 @@ func Query(stub shim.ChaincodeStubInterface, function string, args []string) ([] // readAllRoutes shows all registered routes var readAllRoutes = func(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) { type RoutesOut struct { - FunctionName string `json:"functionnamr"` + FunctionName string `json:"functionname"` Method string `json:"method"` Class AssetClass `json:"class"` } diff --git a/contracts/platform/iotcontractplatform/schema/IOTCPschema.json b/contracts/platform/iotcontractplatform/schema/IOTCPschema.json new file mode 100644 index 0000000..a3d030d --- /dev/null +++ b/contracts/platform/iotcontractplatform/schema/IOTCPschema.json @@ -0,0 +1,780 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "API": { + "type": "object", + "description": "Contract API", + "properties": { + "initContract": { + "type": "object", + "description": "Sets contract version and nickname", + "properties": { + "method": "deploy", + "function": { + "type": "string", + "enum": [ + "initContract" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "$ref": "#/definitions/version" + }, + "nickname": { + "$ref": "#/definitions/nickname" + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + }, + "createAsset": { + "type": "object", + "description": "Creates a new asset by class", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "createAsset" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asset": { + "$ref": "#/definitions/asset" + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + }, + "updateAssetAsset": { + "type": "object", + "description": "Update an asset's state with one or more property changes", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "updateAsset" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asset": { + "$ref": "#/definitions/asset" + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + }, + "deleteAsset": { + "type": "object", + "description": "Delete an asset from world state, transactions remain on the blockchain", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "deleteAsset" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "assetKey": { + "$ref": "#/definitions/assetKey" + } + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + } + }, + "deletePropertiesFromAsset": { + "type": "object", + "description": "Delete one or more properties from an asset's state", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "deletePropertiesFromAsset" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "assetKey": { + "type": "string", + "description": "an asset's ID" + } + } + }, + "qprops": { + "type": "array", + "description": "Qualified property names, e.g. asset.assetKey", + "items": { + "type": "string" + } + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + }, + "deleteAllAssets": { + "type": "object", + "description": "Delete all assets from world state, supports filters", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "deleteAllAssets" + ] + }, + "args": { + "type": "array", + "items": { + "$ref": "#/definitions/stateFilter" + }, + "minItems": 0, + "maxItems": 1 + } + } + }, + "readAsset": { + "type": "object", + "description": "Returns the state an asset", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readAsset" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "assetKey": { + "type": "string", + "description": "an asset's ID" + } + } + } + } + }, + "minItems": 1, + "maxItems": 1 + }, + "result": { + "$ref": "#/definitions/assetstate" + } + } + }, + "readAllAssets": { + "type": "object", + "description": "Returns the state of all assets, supports filters", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readAllAssets" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "filter": { + "$ref": "#/definitions/stateFilter" + } + } + }, + "minItems": 0, + "maxItems": 1 + }, + "result": { + "$ref": "#/definitions/assetstatearray" + } + } + }, + "readAssetHistory": { + "type": "object", + "description": "Returns the history of an asset", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readAssetHistory" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "assetKey": { + "$ref": "#/definitions/assetKey" + } + }, + "required": [ + "assetKey" + ] + }, + "range": { + "type": "object", + "description": "if specified, dates must fall in between these values, inclusive", + "properties": { + "begin": { + "type": "string", + "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "format": "date-time", + "sample": "yyyy-mm-dd hh:mm:ss" + }, + "end": { + "type": "string", + "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "format": "date-time", + "sample": "yyyy-mm-dd hh:mm:ss" + } + } + }, + "filter": { + "$ref": "#/definitions/stateFilter" + } + }, + "required": [ + "asset" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "result": { + "$ref": "#/definitions/assetstatearray" + } + } + }, + "readAssetSchemas": { + "type": "object", + "description": "Returns the API for this contract for the use of self-configuring applications; is MANDATORY for integration with the Watson IoT Platform", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readAssetSchemas" + ] + }, + "args": { + "type": "array", + "items": {}, + "minItems": 0, + "maxItems": 0 + }, + "result": { + "type": "object", + "properties": {} + } + } + }, + "readAssetSamples": { + "type": "object", + "description": "Returns samples of selected contract objects", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readAssetSamples" + ] + }, + "args": { + "type": "array", + "items": {}, + "minItems": 0, + "maxItems": 0 + }, + "result": { + "type": "object", + "properties": {} + } + } + }, + "readRecentStates": { + "type": "object", + "description": "Returns the state of recently updated assets", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readRecentStates" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "begin": { + "type": "integer", + "description": "zero based beginning of range" + }, + "end": { + "type": "integer", + "description": "zero based end of range, absence means to end" + } + } + }, + "minItems": 0, + "maxItems": 0 + }, + "result": { + "$ref": "#/definitions/assetstatearray" + } + } + }, + "readAllRoutes": { + "type": "object", + "description": "Returns an array of registered API calls by function (debugging)", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readAllRoutes" + ] + }, + "args": { + "type": "array", + "items": {}, + "minItems": 0, + "maxItems": 0 + }, + "result": { + "$ref": "#/definitions/assetstatearray" + } + } + }, + "readContractState": { + "type": "object", + "description": "Returns this contract instance's version and nickname", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readContractState" + ] + }, + "args": { + "type": "array", + "items": {}, + "minItems": 0, + "maxItems": 0 + }, + "result": { + "$ref": "#/definitions/contractState" + } + } + }, + "setLoggingLevel": { + "type": "object", + "description": "Sets the logging level for the contract", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "setLoggingLevel" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "logLevel": { + "type": "string", + "enum": [ + "CRITICAL", + "ERROR", + "WARNING", + "NOTICE", + "INFO", + "DEBUG" + ] + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + }, + "setCreateOnUpdate": { + "type": "object", + "description": "Allow updateAsset to create an asset upon receipt of its first event", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "setCreateOnUpdate" + ] + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "setCreateOnUpdate": { + "type": "object", + "description": "Allows updates to create missing assets on first event", + "properties": { + "createOnUpdate": { + "type": "boolean" + } + } + } + } + }, + "minItems": 1, + "maxItems": 1 + } + } + }, + "readWorldState": { + "type": "object", + "description": "Returns the entire contents of world state", + "properties": { + "method": "query", + "function": { + "type": "string", + "enum": [ + "readWorldState" + ] + }, + "args": { + "type": "array", + "items": {}, + "minItems": 0, + "maxItems": 0 + }, + "result": { + "type": "object", + "properties": {} + } + } + }, + "deleteWorldState": { + "type": "object", + "description": "**** WARNING *** Clears the entire contents of world state, redeploy the contract after using this, in debugging mode, will require a restart", + "properties": { + "method": "invoke", + "function": { + "type": "string", + "enum": [ + "deleteWorldState" + ] + }, + "args": { + "type": "array", + "items": {}, + "minItems": 0, + "maxItems": 0 + } + } + } + } + }, + "version": { + "type": "string", + "description": "The version number of the current contract instance" + }, + "nickname": { + "type": "string", + "default": "CTIORSAMPLE", + "description": "The nickname of the current contract instance" + }, + "assetKey": { + "type": "string", + "description": "A asset's ID" + }, + "alertName": { + "type": "string", + "enum": [ + "" + ], + "description": "An alert name" + }, + "alerts": { + "type": "array", + "description": "A list of alert names", + "items": { + "$ref": "#/definitions/alertName" + } + }, + "geo": { + "description": "A geographical coordinate", + "type": "object", + "properties": { + "latitude": { "type": "number" }, + "longitude": { "type": "number" } + } + }, + "QProps": { + "type": "array", + "items": { + "type": "string", + "description": "A qualified name of a property such as asset.assetKey" + } + }, + "asset": { + "type": "object", + "description": "The changeable properties for an asset, also considered its 'event' as a partial state", + "properties": { + "common": { + "$ref": "#/definitions/ioteventcommon" + }, + "assetKey": { + "$ref": "#/definitions/assetKey" + }, + "temperature": { + "type": "number", + "description": "Temperature of an asset's contents in degrees Celsuis" + }, + "carrier": { + "type": "string", + "description": "The carrier in possession of this asset" + } + }, + "required": [ + "assetKey" + ] + }, + "invokeevent": { + "type": "object", + "description": "An chaincode event emitted by a contract invoke", + "properties": { + "name": { + "type": "string", + "description": "The chaincode event's name" + }, + "payload": { + "type": "object", + "description": "The chaincode event's properties", + "properties": {} + } + } + }, + "ioteventcommon": { + "type": "object", + "description": "Common properties for all assets", + "properties": { + "devicetimestamp": { + "type": "string", + "description": "A timestamp recoded by the device that sent the current event" + }, + "deviceID": { + "type": "string", + "description": "A unique identifier for the device that sent the current event" + }, + "location": { + "$ref": "#/definitions/geo" + }, + "appdata": { + "type": "array", + "description": "Application managed information as an array of key:value pairs", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "K": { + "type": "string" + }, + "V": { + "type": "string" + } + } + } + } + } + }, + "assetstate": { + "type": "object", + "description": "A asset's complete state", + "properties": { + "class": { + "type": "string", + "description": "The asset's asset class" + }, + "prefix": { + "type": "string", + "description": "The asset's asset class prefix in world state" + }, + "assetIDpath": { + "type": "string", + "description": "Qualified property path to the asset's ID, declared in the contract code" + }, + "assetKey": { + "type": "string", + "description": "This asset's world state asset ID" + }, + "state": { + "type": "object", + "description": "Properties that have been received or calculated for this asset", + "properties": { + "asset": { + "$ref": "#/definitions/asset" + } + } + }, + "eventin": { + "type": "object", + "description": "The contract event that created this state, for example updateAsset", + "properties": { + "asset": { + "$ref": "#/definitions/asset" + } + } + }, + "txnts": { + "type": "string", + "description": "Transaction timestamp matching the blockchain" + }, + "txnid": { + "type": "string", + "description": "Transaction UUID matching the blockchain" + }, + "eventout": { + "type": "object", + "description": "The chaincode event emitted on invoke exit, if any", + "properties": { + "asset": { + "$ref": "#/definitions/invokeevent" + } + } + }, + "alerts": { + "$ref": "#/definitions/alerts" + }, + "compliant": { + "type": "boolean", + "description": "This asset has no active alerts" + } + } + }, + "assetstateexternal": { + "type": "object", + "patternProperties": { + "^CON": { + "type": "object", + "description": "The external state of one asset asset, named by its world state ID", + "$ref": "#/definitions/assetstate" + } + } + }, + "stateFilter": { + "type": "object", + "description": "Filter asset states", + "properties": { + "match": { + "type": "string", + "description": "Defines how to match properties, missing property always fails match", + "enum": [ + "n/a", + "all", + "any", + "none" + ], + "default": "n/a" + }, + "select": { + "type": "array", + "description": "Qualified property names and values match", + "items": { + "type": "object", + "properties": { + "qprop": { + "type": "string", + "description": "Qualified property name, e.g. asset.assetKey" + }, + "value": { + "type": "string", + "description": "Match this property value" + } + } + } + } + } + }, + "assetstatearray": { + "type": "array", + "items": { + "$ref": "#/definitions/assetstateexternal" + }, + "minItems": 0, + "description": "Array of asset states, can mix asset classes" + }, + "contractState": { + "type": "object", + "properties": { + "version": { + "$ref": "#/definitions/version" + }, + "nickname": { + "$ref": "#/definitions/nickname" + } + } + } + } +} \ No newline at end of file diff --git a/contracts/platform/iotcontractplatform/scripts/genschema.go b/contracts/platform/iotcontractplatform/scripts/genschema.go index 818376d..f057f13 100644 --- a/contracts/platform/iotcontractplatform/scripts/genschema.go +++ b/contracts/platform/iotcontractplatform/scripts/genschema.go @@ -452,6 +452,9 @@ func main() { var lineOut = 1 var offsets [5000]int + // _, currentFilePath, _, _ := runtime.Caller(0) + // mypath := path.Dir(currentFilePath) + // read the configuration from the json file filename, _ := filepath.Abs("./" + configFile) jsonFile, err := ioutil.ReadFile(filename) diff --git a/contracts/platform/iotcontractplatformsample/assetContainer.go b/contracts/platform/iotcontractplatformsample/assetContainer.go index ab4f053..7a04d48 100644 --- a/contracts/platform/iotcontractplatformsample/assetContainer.go +++ b/contracts/platform/iotcontractplatformsample/assetContainer.go @@ -72,9 +72,9 @@ var readAllAssetsContainer iot.ChaincodeFunc = func(stub shim.ChaincodeStubInter return ContainerClass.ReadAllAssets(stub, args) } -// func (t *SimpleChaincode) readAssetIOTHistory(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) { -// return iot.ReadAssetHistory(stub, args, "iot", "readAssetIOTHistory") -// } +var readAssetHistoryContainer = func(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) { + return ContainerClass.ReadAssetStateHistory(stub, args) +} var overtempAlert iot.AlertName = "OVERTEMP" var overtempRule iot.RuleFunc = func(stub shim.ChaincodeStubInterface, container *iot.Asset) error { @@ -97,5 +97,6 @@ func init() { iot.AddRoute("deleteAllAssetsContainer", "invoke", ContainerClass, deleteAllAssetsContainer) iot.AddRoute("deletePropertiesFromAssetContainer", "invoke", ContainerClass, deletePropertiesFromAssetContainer) iot.AddRoute("readAssetContainer", "query", ContainerClass, readAssetContainer) + iot.AddRoute("readAssetHistoryContainer", "query", ContainerClass, readAssetHistoryContainer) iot.AddRoute("readAllAssetsContainer", "query", ContainerClass, readAllAssetsContainer) } diff --git a/contracts/platform/iotcontractplatformsample/generate.json b/contracts/platform/iotcontractplatformsample/generate.json index d097e0c..06de263 100644 --- a/contracts/platform/iotcontractplatformsample/generate.json +++ b/contracts/platform/iotcontractplatformsample/generate.json @@ -38,7 +38,8 @@ "containerstate", "containerstateexternal", "containerstatearray", - "stateFilter" + "stateFilter", + "API/readAssetHistoryContainer" ] } } \ No newline at end of file diff --git a/contracts/platform/iotcontractplatformsample/payloadschema.json b/contracts/platform/iotcontractplatformsample/payloadschema.json index 2477084..b8e937c 100644 --- a/contracts/platform/iotcontractplatformsample/payloadschema.json +++ b/contracts/platform/iotcontractplatformsample/payloadschema.json @@ -226,7 +226,12 @@ "args": { "type": "array", "items": { - "$ref": "#/definitions/stateFilter" + "type": "object", + "properties": { + "filter": { + "$ref": "#/definitions/stateFilter" + } + } }, "minItems": 0, "maxItems": 1 @@ -252,22 +257,42 @@ "items": { "type": "object", "properties": { - "barcode": { - "$ref": "#/definitions/barcode" + "container": { + "type": "object", + "properties": { + "barcode": { + "$ref": "#/definitions/barcode" + } + }, + "required": [ + "barcode" + ] }, - "start": { - "type": "string", - "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", - "format": "date-time", - "sample": "yyyy-mm-dd hh:mm:ss" + "range": { + "type": "object", + "description": "if specified, dates must fall in between these values, inclusive", + "properties": { + "begin": { + "type": "string", + "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "format": "date-time", + "sample": "yyyy-mm-dd hh:mm:ss" + }, + "end": { + "type": "string", + "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "format": "date-time", + "sample": "yyyy-mm-dd hh:mm:ss" + } + } }, - "end": { - "type": "string", - "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", - "format": "date-time", - "sample": "yyyy-mm-dd hh:mm:ss" + "filter": { + "$ref": "#/definitions/stateFilter" } - } + }, + "required": [ + "container" + ] }, "minItems": 1, "maxItems": 1 @@ -700,20 +725,22 @@ }, "stateFilter": { "type": "object", - "description": "Match mode plus array of property : value pairs", + "description": "Filter asset states", "properties": { "match": { "type": "string", - "description": "Select match mode, missing property counts as not matched", + "description": "Defines how to match properties, missing property always fails match", "enum": [ - "ALL", - "ANY", - "NONE" - ] + "n/a", + "all", + "any", + "none" + ], + "default": "n/a" }, "select": { "type": "array", - "description": "Array of property : value pairs to match", + "description": "Qualified property names and values match", "items": { "type": "object", "properties": { @@ -723,7 +750,7 @@ }, "value": { "type": "string", - "description": "Property value to be matched" + "description": "Match this property value" } } } diff --git a/contracts/platform/iotcontractplatformsample/samples.go b/contracts/platform/iotcontractplatformsample/samples.go index 86b40a8..0bfa0a5 100644 --- a/contracts/platform/iotcontractplatformsample/samples.go +++ b/contracts/platform/iotcontractplatformsample/samples.go @@ -9,6 +9,93 @@ package main var samples = ` { + "API/readAssetHistoryContainer": { + "args": [ + { + "container": { + "barcode": "A container's ID" + }, + "filter": { + "match": "n/a", + "select": [ + { + "qprop": "Qualified property name, e.g. container.barcode", + "value": "Match this property value" + } + ] + }, + "range": { + "begin": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "end": "timestamp formatted yyyy-mm-dd hh:mm:ss" + } + } + ], + "function": "readAssetHistoryContainer", + "result": [ + { + "^CON": { + "AssetKey": "This container's world state container ID", + "alerts": [ + "OVERTTEMP" + ], + "assetIDpath": "Qualified property path to the container's ID, declared in the contract code", + "class": "The container's asset class", + "compliant": true, + "eventin": { + "container": { + "barcode": "A container's ID", + "carrier": "The carrier in possession of this container", + "common": { + "appdata": [ + { + "K": "carpe noctem", + "V": "carpe noctem" + } + ], + "deviceID": "A unique identifier for the device that sent the current event", + "devicetimestamp": "A timestamp recoded by the device that sent the current event", + "location": { + "latitude": 123.456, + "longitude": 123.456 + } + }, + "temperature": 123.456 + } + }, + "eventout": { + "container": { + "name": "The chaincode event's name", + "payload": {} + } + }, + "prefix": "The container's asset class prefix in world state", + "state": { + "container": { + "barcode": "A container's ID", + "carrier": "The carrier in possession of this container", + "common": { + "appdata": [ + { + "K": "carpe noctem", + "V": "carpe noctem" + } + ], + "deviceID": "A unique identifier for the device that sent the current event", + "devicetimestamp": "A timestamp recoded by the device that sent the current event", + "location": { + "latitude": 123.456, + "longitude": 123.456 + } + }, + "temperature": 123.456 + } + }, + "txnid": "Transaction UUID matching the blockchain", + "txnts": "Transaction timestamp matching the blockchain" + } + } + ] + }, "container": { "barcode": "A container's ID", "carrier": "The carrier in possession of this container", @@ -233,11 +320,11 @@ var samples = ` } }, "stateFilter": { - "match": "ANY", + "match": "n/a", "select": [ { "qprop": "Qualified property name, e.g. container.barcode", - "value": "Property value to be matched" + "value": "Match this property value" } ] } diff --git a/contracts/platform/iotcontractplatformsample/schemas.go b/contracts/platform/iotcontractplatformsample/schemas.go index 3b8a4a9..18d098c 100644 --- a/contracts/platform/iotcontractplatformsample/schemas.go +++ b/contracts/platform/iotcontractplatformsample/schemas.go @@ -101,19 +101,21 @@ var schemas = ` "properties": { "args": { "items": { - "description": "Match mode plus array of property : value pairs", + "description": "Filter asset states", "properties": { "match": { - "description": "Select match mode, missing property counts as not matched", + "default": "n/a", + "description": "Defines how to match properties, missing property always fails match", "enum": [ - "ALL", - "ANY", - "NONE" + "n/a", + "all", + "any", + "none" ], "type": "string" }, "select": { - "description": "Array of property : value pairs to match", + "description": "Qualified property names and values match", "items": { "properties": { "qprop": { @@ -121,7 +123,7 @@ var schemas = ` "type": "string" }, "value": { - "description": "Property value to be matched", + "description": "Match this property value", "type": "string" } }, @@ -273,33 +275,40 @@ var schemas = ` "properties": { "args": { "items": { - "description": "Match mode plus array of property : value pairs", "properties": { - "match": { - "description": "Select match mode, missing property counts as not matched", - "enum": [ - "ALL", - "ANY", - "NONE" - ], - "type": "string" - }, - "select": { - "description": "Array of property : value pairs to match", - "items": { - "properties": { - "qprop": { - "description": "Qualified property name, e.g. container.barcode", - "type": "string" - }, - "value": { - "description": "Property value to be matched", - "type": "string" - } + "filter": { + "description": "Filter asset states", + "properties": { + "match": { + "default": "n/a", + "description": "Defines how to match properties, missing property always fails match", + "enum": [ + "n/a", + "all", + "any", + "none" + ], + "type": "string" }, - "type": "object" + "select": { + "description": "Qualified property names and values match", + "items": { + "properties": { + "qprop": { + "description": "Qualified property name, e.g. container.barcode", + "type": "string" + }, + "value": { + "description": "Match this property value", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } }, - "type": "array" + "type": "object" } }, "type": "object" @@ -1007,23 +1016,74 @@ var schemas = ` "args": { "items": { "properties": { - "barcode": { - "description": "A container's ID", - "type": "string" + "container": { + "properties": { + "barcode": { + "description": "A container's ID", + "type": "string" + } + }, + "required": [ + "barcode" + ], + "type": "object" }, - "end": { - "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", - "format": "date-time", - "sample": "yyyy-mm-dd hh:mm:ss", - "type": "string" + "filter": { + "description": "Filter asset states", + "properties": { + "match": { + "default": "n/a", + "description": "Defines how to match properties, missing property always fails match", + "enum": [ + "n/a", + "all", + "any", + "none" + ], + "type": "string" + }, + "select": { + "description": "Qualified property names and values match", + "items": { + "properties": { + "qprop": { + "description": "Qualified property name, e.g. container.barcode", + "type": "string" + }, + "value": { + "description": "Match this property value", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" }, - "start": { - "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", - "format": "date-time", - "sample": "yyyy-mm-dd hh:mm:ss", - "type": "string" + "range": { + "description": "if specified, dates must fall in between these values, inclusive", + "properties": { + "begin": { + "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "format": "date-time", + "sample": "yyyy-mm-dd hh:mm:ss", + "type": "string" + }, + "end": { + "description": "timestamp formatted yyyy-mm-dd hh:mm:ss", + "format": "date-time", + "sample": "yyyy-mm-dd hh:mm:ss", + "type": "string" + } + }, + "type": "object" } }, + "required": [ + "container" + ], "type": "object" }, "maxItems": 1, @@ -2424,19 +2484,21 @@ var schemas = ` "type": "object" }, "stateFilter": { - "description": "Match mode plus array of property : value pairs", + "description": "Filter asset states", "properties": { "match": { - "description": "Select match mode, missing property counts as not matched", + "default": "n/a", + "description": "Defines how to match properties, missing property always fails match", "enum": [ - "ALL", - "ANY", - "NONE" + "n/a", + "all", + "any", + "none" ], "type": "string" }, "select": { - "description": "Array of property : value pairs to match", + "description": "Qualified property names and values match", "items": { "properties": { "qprop": { @@ -2444,7 +2506,7 @@ var schemas = ` "type": "string" }, "value": { - "description": "Property value to be matched", + "description": "Match this property value", "type": "string" } },