From 76b4134a9bd6cf40f758df784bbcaa230f2fa0f1 Mon Sep 17 00:00:00 2001 From: AntonioClaudioSouza Date: Fri, 5 Apr 2024 13:40:04 -0300 Subject: [PATCH] Added files to package query-search Signed-off-by: AntonioClaudioSouza --- querySearch/cleanAssetMap.go | 56 ++++++ querySearch/date.go | 80 ++++++++ querySearch/querySearch.go | 188 ++++++++++++++++++ querySearch/resolveExternalAsset.go | 183 ++++++++++++++++++ querySearch/results.go | 286 ++++++++++++++++++++++++++++ querySearch/resume.go | 78 ++++++++ querySearch/sort.go | 13 ++ 7 files changed, 884 insertions(+) create mode 100644 querySearch/cleanAssetMap.go create mode 100644 querySearch/date.go create mode 100644 querySearch/querySearch.go create mode 100644 querySearch/resolveExternalAsset.go create mode 100644 querySearch/results.go create mode 100644 querySearch/resume.go create mode 100644 querySearch/sort.go diff --git a/querySearch/cleanAssetMap.go b/querySearch/cleanAssetMap.go new file mode 100644 index 0000000..a564c24 --- /dev/null +++ b/querySearch/cleanAssetMap.go @@ -0,0 +1,56 @@ +// This code contains functions related to cleaning up an asset map by removing certain tags. +package querysearch + +// List of default tags that will be removed if enabled +func GetDefaultRemoveTags() *[]string { + return &[]string{"@lastTouchBy", "@lastTx", "@lastUpdated", "@txId", "@txID"} +} + +// Remove fields in map by default tags +func CleanAssetMapDefault(m map[string]interface{}) map[string]interface{} { + return CleanAssetMap(m, GetDefaultRemoveTags()) +} + +// This is a recursive function that cleans an asset map by removing tags. +// It takes a map and a slice of tags to remove. +// It iterates over the map and removes any tags that are in the removeTag slice. +// If a value in the map is another map or a slice, +// it recursively calls CleanAssetMap on that value. +func CleanAssetMap(m map[string]interface{}, removeTag *[]string) map[string]interface{} { + if removeTag != nil { + for _, tag := range *removeTag { + delete(m, tag) + } + } + + for k, v := range m { + switch prop := v.(type) { + case map[string]interface{}: + m[k] = CleanAssetMap(prop, removeTag) + + case []interface{}: + for idx, elem := range prop { + if elemMap, ok := elem.(map[string]interface{}); ok { + prop[idx] = CleanAssetMap(elemMap, removeTag) + } + } + } + } + + return m +} + +// Internal function for remove tags +func (q *QuerySearch) removeTags(m *map[string]interface{}) { + if q.config.NoRemoveTagsTransaction && len(q.config.RemoveTags) == 0 { + return + } + + remove := []string{} + if !q.config.NoRemoveTagsTransaction { + remove = append(remove, q.config.removeDefaultTags...) + } + + remove = append(remove, q.config.RemoveTags...) + *m = CleanAssetMap(*m, &remove) +} diff --git a/querySearch/date.go b/querySearch/date.go new file mode 100644 index 0000000..6a560d2 --- /dev/null +++ b/querySearch/date.go @@ -0,0 +1,80 @@ +// These functions are useful when you need to create specific date and time queries, +// like when you need to filter results between a time interval or within a specific time of day. +package querysearch + +import ( + "fmt" + "time" +) + +var defaultLocationTimezone = "America/Sao_Paulo" + +// Set default timezone for reference +func SetDefaultTimezone(locationDefault string) { + defaultLocationTimezone = locationDefault +} + +// Return date in format '2006-01-02 00:00:00 ' +func GetDateFormatted(dateRef time.Time) string { + dateF := setTimeZone(dateRef) + return dateF.Format("2006-01-02") + " 00:00:00 " + GetTimeZoneOffSet(dateF) +} + +// Return hour and minute in UTC, with offset from the local timezone +func GetTimeZoneOffSet(dateRef time.Time) string { + _, offset := dateRef.Zone() + offsetHours := offset / 3600 + offsetMinutes := (offset % 3600) / 60 + offsetStr := fmt.Sprintf("%03d:%02d", offsetHours, offsetMinutes) + return offsetStr +} + +// Change date to timezone defined on variable default LocationTimezone +func setTimeZone(dateRef time.Time) time.Time { + loc, _ := time.LoadLocation(defaultLocationTimezone) + return dateRef.In(loc) +} + +// Define fisrt hour of day +func SetDateFirstHour(dateRef time.Time) time.Time { + newDate := setTimeZone(dateRef) + newDate = time.Date(newDate.Year(), newDate.Month(), newDate.Day(), 0, 0, 0, 0, newDate.Location()) + return newDate +} + +// Define last hour of day +func SetDateLastHour(dateRef time.Time) time.Time { + newDate := setTimeZone(dateRef) + newDate = time.Date(newDate.Year(), newDate.Month(), newDate.Day(), 23, 59, 59, 0, newDate.Location()) + return newDate +} + +// Define the selection of a date >= the date entered +func StartDate(dateRef time.Time) map[string]interface{} { + return map[string]interface{}{ + "$gte": SetDateFirstHour(dateRef), + } +} + +// Define the selection of a date <= the date entered +func EndDate(dateRef time.Time) map[string]interface{} { + return map[string]interface{}{ + "$lte": SetDateLastHour(dateRef), + } +} + +// Define the date selection within a period of 1 day from the date entered +func OneDay(dateRef time.Time) map[string]interface{} { + return map[string]interface{}{ + "$gte": SetDateFirstHour(dateRef), + "$lte": SetDateLastHour(dateRef), + } +} + +// Define the date range between two dates +func PeriodDay(dateStart, dateEnd time.Time) map[string]interface{} { + return map[string]interface{}{ + "$gte": SetDateFirstHour(dateStart), + "$lte": SetDateLastHour(dateEnd), + } +} diff --git a/querySearch/querySearch.go b/querySearch/querySearch.go new file mode 100644 index 0000000..3ee6ee3 --- /dev/null +++ b/querySearch/querySearch.go @@ -0,0 +1,188 @@ +// This package provides a way to query a database, possibly using a CouchDB-like interface, +// and defines several types, methods, and functions for querying. It facilitates the creation of queries +// without having to write queries in a raw form. +// +// Configuration +// ------------- +// The Config struct contains the settings used for querying. +// These settings include RemoveTags (a list of tags to be removed from the results), +// AssetName (the name of the asset being queried), PageSize (the number of results per page), +// BookMark (the bookmark for pagination), Resolve (a list of relations to be resolved), +// Sort (a list of fields to sort by), IndexDoc (a document containing the index design and name), +// NoRemoveTagsTransaction (a boolean indicating whether to remove tags during the transaction), +// and CallBack (a function to be called after the query is executed). +package querysearch + +import ( + "encoding/json" + "time" + + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" +) + +type Config struct { + RemoveTags []string + removeDefaultTags []string + AssetName string + PageSize int32 + BookMark string + Resolve []string + Sort []map[string]string + IndexDoc IndexDocument + NoRemoveTagsTransaction bool + CallBack func(*sw.StubWrapper, map[string]interface{}, map[string]interface{}) error + CallBackList func(*sw.StubWrapper, map[string]interface{}, []map[string]interface{}, map[string]interface{}) (map[string]interface{}, error) +} + +type IndexDocument struct { + Design string + IndexName string +} + +type QuerySearch struct { + Query map[string]interface{} + config Config + QueryParser string +} + +type FieldSearch struct { + KeyName string + Value interface{} +} + +func NewQuery(cfg Config) *QuerySearch { + if len(cfg.AssetName) == 0 { + return nil + } + + // ** Set default tags to remove + cfg.removeDefaultTags = *GetDefaultRemoveTags() + return &QuerySearch{ + Query: map[string]interface{}{ + "selector": map[string]interface{}{ + "@assetType": cfg.AssetName, + }, + }, + config: cfg, + } +} + +func (q *QuerySearch) GetConfig() Config { + return q.config +} + +func (q *QuerySearch) AddCallBack(f func(*sw.StubWrapper, map[string]interface{}, map[string]interface{}) error) { + q.config.CallBack = f +} + +func (q *QuerySearch) AddCallBackList(f func(*sw.StubWrapper, map[string]interface{}, []map[string]interface{}, map[string]interface{}) (map[string]interface{}, error)) { + q.config.CallBackList = f +} + +func (q *QuerySearch) SetPagination(bookMark string, pageSize int32) { + q.config.BookMark = bookMark + q.config.PageSize = pageSize +} + +func (q *QuerySearch) SetIndexDoc(designDoc, nameIndex string) { + q.config.IndexDoc.Design = designDoc + q.config.IndexDoc.IndexName = nameIndex +} + +func (q *QuerySearch) SetIndexDocSimple(nameIndex string) { + q.config.IndexDoc.Design = nameIndex + q.config.IndexDoc.IndexName = nameIndex +} + +func (q *QuerySearch) SetSort(sort ...Sort) { + s := make([]map[string]string, 0) + for _, v := range sort { + s = append(s, map[string]string{ + v.Field: string(v.Type), + }) + } + q.config.Sort = s +} + +func (q *QuerySearch) SetResolve(resolv ...string) { + q.config.Resolve = append(q.config.Resolve, resolv...) +} + +func (q *QuerySearch) SetRemoveCustomTags(tags ...string) { + q.config.RemoveTags = append(q.config.RemoveTags, tags...) +} + +func (q *QuerySearch) NoContains(value interface{}) map[string]interface{} { + return map[string]interface{}{ + "$ne": value, + } +} + +func (q *QuerySearch) NoContainsValues(values []interface{}) map[string]interface{} { + return map[string]interface{}{ + "$nin": values, + } +} + +func (q *QuerySearch) AddFieldsOR(fields map[string]interface{}) { + aux := q.Query["selector"].(map[string]interface{}) + listOfElementsFinds := make([]map[string]interface{}, 0) + + for k, v := range fields { + listOfElementsFinds = append(listOfElementsFinds, map[string]interface{}{ + k: v, + }) + } + + aux["$or"] = listOfElementsFinds + q.Query["selector"] = aux +} + +func (q *QuerySearch) AddField(key string, value interface{}) { + q.AddFields(FieldSearch{ + KeyName: key, + Value: value, + }) +} + +func (q *QuerySearch) AddFields(fields ...FieldSearch) { + for _, f := range fields { + if len(f.KeyName) > 0 { + aux := q.Query["selector"].(map[string]interface{}) + aux[f.KeyName] = f.Value + q.Query["selector"] = aux + } + } +} + +func (q *QuerySearch) AddDateRange(nameField string, dataStart, dataEnd time.Time) { + q.AddFields(FieldSearch{ + KeyName: nameField, + Value: PeriodDay(dataStart, dataEnd), + }) +} + +func (q *QuerySearch) Parser() (string, error) { + qAux := q.Query + + //** Add Fields for sort query + if len(q.config.Sort) > 0 { + qAux["sort"] = q.config.Sort + } + + //** Add indexDoc and indexName for use in query + if len(q.config.IndexDoc.Design) > 0 && len(q.config.IndexDoc.IndexName) > 0 { + qAux["use_index"] = []string{ + "_design/" + q.config.IndexDoc.Design, + q.config.IndexDoc.IndexName, + } + } + + queryJson, err := json.Marshal(qAux) + if err != nil { + return "", errors.WrapError(err, "error marshaling query") + } + + return string(queryJson), nil +} diff --git a/querySearch/resolveExternalAsset.go b/querySearch/resolveExternalAsset.go new file mode 100644 index 0000000..e641a0f --- /dev/null +++ b/querySearch/resolveExternalAsset.go @@ -0,0 +1,183 @@ +package querysearch + +import ( + "log" + "net/http" + "strings" + + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" +) + +// ResolveExternalAsset resolves external assets and their sub-assets based on the given configuration. +// It iterates through the `config.Resolve` slice and resolves assets recursively. +func (q *QuerySearch) ResolveExternalAsset(stub *sw.StubWrapper, m *map[string]interface{}) (map[string]interface{}, errors.ICCError) { + data := *m + + // Remove tags from the data before resolving assets. + q.removeTags(&data) + + // Iterate through the `config.Resolve` slice to resolve assets. + for _, resolveStr := range q.config.Resolve { + // Check exist sub asset for resolve + var resolve string + var subResolve []string + + tuplaKeys := strings.Split(resolveStr, ".") + + if len(tuplaKeys) > 1 { + resolve = tuplaKeys[0] + + subAssetStr := strings.ReplaceAll(tuplaKeys[1], "{", "") + subAssetStr = strings.ReplaceAll(subAssetStr, "}", "") + + // Create a list of sub-assets to resolve. + subResolve = strings.Split(subAssetStr, ",") + } else { + resolve = resolveStr + } + + key, has := data[resolve] + + if has { + switch prop := key.(type) { + case map[string]interface{}: + { + assetKey, err := assets.NewKey(prop) + if err != nil { + return nil, errors.NewCCError("failed to get asset in ledger", http.StatusBadRequest) + } + + assetMap, err := assetKey.GetRecursive(stub) + if err != nil { + return nil, errors.NewCCError("failed to get asset in ledger", http.StatusBadRequest) + } + + // Remove tags from the resolved asset. + q.removeTags(&assetMap) + data[resolve] = assetMap + } + + case []interface{}: + { + res, err := q.resolveSlice(stub, prop, subResolve) + if err != nil { + return nil, errors.NewCCError(err.Error(), http.StatusBadRequest) + } + data[resolve] = res + } + default: + log.Printf("resolveExternalAsset typeKey: %T", key) + } + } + } + return data, nil +} + +// resolveSlice resolves assets within a slice. +// This function can handle two types of slices: +// 1. Reference slice for an asset +// 2. Slice containing a map with @key and other data at the same level +func (q *QuerySearch) resolveSlice(stub *sw.StubWrapper, prop []interface{}, subResolve []string) ([]interface{}, errors.ICCError) { + lstResult := make([]interface{}, 0) + + // ** Revolve assets into slice + //! ** There can be 2 types of slices + // ** Type 1 + //! ** - reference slice for asset + // ** Type 2 + //! ** - slice containing a map with @key and other data at the same level + for _, elem := range prop { + + elemMap, ok := elem.(map[string]interface{}) + + // ** Not a map or not subResolve, append to result and continue + if !ok { + lstResult = append(lstResult, elem) + continue + } + + // ** + // ** TYPE 1 ** + // ** + // ** Check if element is a asset + if _, ok := elemMap["@key"]; ok { + res, err := q.resolveAsset(stub, elemMap, &subResolve) + if err != nil { + return nil, err + } + + lstResult = append(lstResult, res) + continue + } + + // ** + // ** TYPE 2 ** + // ** + itemMap := make(map[string]interface{}) + for key, value := range elemMap { + + // ** check if value is a map of asset + if _, ok := value.(map[string]interface{}); ok { + + assetMap, err := q.resolveAsset(stub, value.(map[string]interface{}), &subResolve) + if err != nil { + return nil, err + } + + //! ** add key (with resolved asset) to itemMap + itemMap[key] = assetMap + continue + } + + //! ** add key OTHER type ( no asset ) to itemMap + itemMap[key] = value + } + + lstResult = append(lstResult, itemMap) + } + + return lstResult, nil +} + +// resolveAsset resolves an asset and its sub-assets based on the given configuration. +func (q *QuerySearch) resolveAsset(stub *sw.StubWrapper, elemMap map[string]interface{}, subResolve *[]string) (map[string]interface{}, errors.ICCError) { + assetKey, err := assets.NewKey(elemMap) + if err != nil { + return nil, errors.NewCCError("failed to get generate key", http.StatusBadRequest) + } + + assetMap, err := assetKey.GetMap(stub) + if err != nil { + return nil, errors.NewCCError("failed to get asset in ledger", http.StatusBadRequest) + } + + q.removeTags(&assetMap) + + // ** CAUTION ** + // ** This solutions is low performance + if len(*subResolve) > 0 { + for _, subKey := range *subResolve { + + subKey = strings.TrimSpace(subKey) + if subAsset, ok := assetMap[subKey]; ok { + if subAssetMap, ok := subAsset.(map[string]interface{}); ok { + subAssetKey, err := assets.NewKey(subAssetMap) + if err != nil { + return nil, errors.NewCCError("failed to generate key for sub asset", http.StatusBadRequest) + } + + subAssetMap, err := subAssetKey.GetMap(stub) + if err != nil { + return nil, errors.NewCCError("failed to get sub asset in ledger", http.StatusBadRequest) + } + q.removeTags(&subAssetMap) + assetMap[subKey] = subAssetMap + } + } + } + } + + return assetMap, nil +} diff --git a/querySearch/results.go b/querySearch/results.go new file mode 100644 index 0000000..a0e0937 --- /dev/null +++ b/querySearch/results.go @@ -0,0 +1,286 @@ +package querysearch + +import ( + "encoding/json" + "net/http" + + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger/fabric-chaincode-go/shim" +) + +// GetFirstKey returns the key of the first asset that matches the query +func (q *QuerySearch) GetFirstKey(stub *sw.StubWrapper) (assets.Key, errors.ICCError) { + itemMap, err := q.GetFirst(stub) + if err != nil { + return nil, err + } + + if itemMap == nil { + return nil, nil + } + + itemKey, err := assets.NewKey(map[string]interface{}{ + "@assetType": itemMap["@assetType"].(string), + "@key": itemMap["@key"].(string), + }) + + if err != nil { + return nil, errors.WrapError(err, "failed to get item key") + } + + return itemKey, nil +} + +// GetFirst returns the first asset that matches the query +func (q *QuerySearch) GetFirst(stub *sw.StubWrapper) (map[string]interface{}, errors.ICCError) { + result, err := q.getResultsNoPagination(stub) + if err != nil { + return nil, err + } + + data, isExist := result["data"].([]map[string]interface{}) + if !isExist || len(data) == 0 { + return nil, nil + } + + fisrtItem := make(map[string]interface{}) + for _, item := range data { + fisrtItem = item + break + } + + return fisrtItem, nil +} + +// GetResults returns the assets that match the query with or without pagination +func (q *QuerySearch) GetResults(stub *sw.StubWrapper) (map[string]interface{}, errors.ICCError) { + + if q.config.PageSize > 0 { + return q.getResultsWithPagination(stub) + } + + return q.getResultsNoPagination(stub) +} + +// GetResultsByCallBackWithContext returns the assets that match the query using a callback function with a given context +func (q *QuerySearch) GetResultsByCallBackWithContext(stub *sw.StubWrapper, resultContext map[string]interface{}) (map[string]interface{}, errors.ICCError) { + var err error + if q.config.CallBack == nil { + return nil, errors.WrapError(nil, "failed, func callback not defined") + } + + // ** Create results + var res map[string]interface{} + + if resultContext == nil { + res = map[string]interface{}{} + } else { + res = resultContext + } + + // ** Assembly query + q.QueryParser, err = q.Parser() + if err != nil { + return nil, errors.WrapError(err, "failed to get query result") + } + + // ** Execute query + searchIterator, err := stub.GetQueryResult(q.QueryParser) + if err != nil { + return nil, errors.WrapError(err, "failed to get query search result") + } + + // ** Get records + for searchIterator.HasNext() { + + searchResult, err := searchIterator.Next() + if err != nil { + return nil, errors.WrapError(err, "error iterating result query response") + } + + var assetMap map[string]interface{} + err = json.Unmarshal(searchResult.Value, &assetMap) + if err != nil { + return nil, errors.WrapError(err, "failed to unmarshal order values") + } + + assetMap, errResolve := q.ResolveExternalAsset(stub, &assetMap) + if errResolve != nil { + return nil, errors.WrapErrorWithStatus(errResolve, "failed resolve asset", http.StatusInternalServerError) + } + + q.removeTags(&assetMap) + errCallBack := q.config.CallBack(stub, assetMap, res) + if errCallBack != nil { + return nil, errors.WrapError(errCallBack, "") + } + } + + return res, nil +} + +func (q *QuerySearch) GetResultsByCallBack(stub *sw.StubWrapper) (map[string]interface{}, errors.ICCError) { + return q.GetResultsByCallBackWithContext(stub, nil) +} + +// GetResultsNoPagination returns the assets that match the query without pagination +func (q *QuerySearch) GetResultsByCallBackPagination(stub *sw.StubWrapper) (map[string]interface{}, map[string]interface{}, errors.ICCError) { + + var err error + if q.config.CallBackList == nil { + return nil, nil, errors.WrapError(nil, "failed, func callback not defined") + } + + //** Initialize auxiliary variables + var results []map[string]interface{} + ctx := map[string]interface{}{} + + // ** Assembly query + q.QueryParser, err = q.Parser() + + if err != nil { + return nil, nil, errors.WrapError(err, "failed assemble query search") + } + + // ** Execute query + searchIterator, queryResponse, err := stub.GetQueryResultWithPagination(q.QueryParser, q.config.PageSize, q.config.BookMark) + if err != nil { + return nil, nil, errors.WrapError(err, "failed to get query search result") + } + + // ** Get records + for searchIterator.HasNext() { + + searchResult, err := searchIterator.Next() + if err != nil { + return nil, nil, errors.WrapError(err, "error iterating result query response") + } + + var assetMap map[string]interface{} + err = json.Unmarshal(searchResult.Value, &assetMap) + if err != nil { + return nil, nil, errors.WrapError(err, "failed to unmarshal order values") + } + + assetMap, errResolve := q.ResolveExternalAsset(stub, &assetMap) + if errResolve != nil { + return nil, nil, errors.WrapErrorWithStatus(errResolve, "failed resolve asset", http.StatusInternalServerError) + } + + data, errCallBack := q.config.CallBackList(stub, assetMap, results, ctx) + if errCallBack != nil { + return nil, nil, errors.WrapError(errCallBack, "") + } + + if data != nil { + results = append(results, data) + } + } + + response := make(map[string]interface{}) + response["data"] = results + response["bookmark"] = queryResponse.Bookmark + return response, ctx, nil +} + +func (q *QuerySearch) GetIterator(stub *sw.StubWrapper) (shim.StateQueryIteratorInterface, errors.ICCError) { + + // ** Assembly query + var err error + q.QueryParser, err = q.Parser() + + if err != nil { + return nil, errors.WrapError(err, "failed to get query result") + } + + // ** Execute query + searchIterator, err := stub.GetQueryResult(q.QueryParser) + if err != nil { + return nil, errors.WrapError(err, "failed to get query search result") + } + + return searchIterator, nil +} + +// GetResultsNoPagination returns the assets that match the query without pagination +func (q *QuerySearch) getResultsNoPagination(stub *sw.StubWrapper) (map[string]interface{}, errors.ICCError) { + + // ** Create results + var res []map[string]interface{} + + searchIterator, err := q.GetIterator(stub) + if err != nil { + return nil, errors.WrapError(err, "failed to get query search result") + } + + res, errParse := q.parseResults(stub, searchIterator) + if errParse != nil { + return nil, errParse + } + + response := make(map[string]interface{}) + response["data"] = res + return response, nil +} + +// GetResultsWithPagination returns the assets that match the query with pagination +func (q *QuerySearch) getResultsWithPagination(stub *sw.StubWrapper) (map[string]interface{}, errors.ICCError) { + + //** Initialize auxiliary variables + var results []map[string]interface{} + + // ** Assembly query + var err error + q.QueryParser, err = q.Parser() + + if err != nil { + return nil, errors.WrapError(err, "failed assemble query search") + } + + //** Get results + resultsIterator, queryResponse, err := stub.GetQueryResultWithPagination(q.QueryParser, q.config.PageSize, q.config.BookMark) + if err != nil { + return nil, errors.WrapError(err, "failed to get query result") + } + + results, errParse := q.parseResults(stub, resultsIterator) + if errParse != nil { + return nil, errParse + } + + response := make(map[string]interface{}) + response["data"] = results + response["bookmark"] = queryResponse.Bookmark + return response, nil +} + +func (q *QuerySearch) parseResults(stub *sw.StubWrapper, resultsIterator shim.StateQueryIteratorInterface) ([]map[string]interface{}, errors.ICCError) { + + var results []map[string]interface{} + + for resultsIterator.HasNext() { + + queryResponse, err := resultsIterator.Next() + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "error iterating response", http.StatusInternalServerError) + } + + var data map[string]interface{} + + err = json.Unmarshal(queryResponse.Value, &data) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "failed to unmarshal queryResponse values", http.StatusInternalServerError) + } + + data, err = q.ResolveExternalAsset(stub, &data) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "failed resolve asset", http.StatusInternalServerError) + } + + results = append(results, data) + } + + return results, nil +} diff --git a/querySearch/resume.go b/querySearch/resume.go new file mode 100644 index 0000000..22316d5 --- /dev/null +++ b/querySearch/resume.go @@ -0,0 +1,78 @@ +package querysearch + +import ( + "encoding/json" + + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" +) + +// *** Simple count +func (q *QuerySearch) GetCount(stub *sw.StubWrapper) (float64, error) { + + // ** Assembly query + query, err := q.Parser() + if err != nil { + return 0, errors.WrapError(err, "failed to get query result") + } + + // ** Execute query + searchIterator, err := stub.GetQueryResult(query) + if err != nil { + return 0, errors.WrapError(err, "failed to get query search result") + } + + var count float64 + + //** Get records + for searchIterator.HasNext() { + + _, err := searchIterator.Next() + if err != nil { + return 0, errors.WrapError(err, "error iterating order query response") + } + + count++ + } + + return count, nil +} + +// *** Simple Sum +func (q *QuerySearch) GetSum(stub *sw.StubWrapper, propertyNameForSum string) (float64, error) { + + // ** Assembly query + query, err := q.Parser() + if err != nil { + return 0, errors.WrapError(err, "failed to get query result") + } + + // ** Execute query + searchIterator, err := stub.GetQueryResult(query) + if err != nil { + return 0, errors.WrapError(err, "failed to get query search result") + } + + var sum float64 + + //** Get records + for searchIterator.HasNext() { + + searchResult, err := searchIterator.Next() + if err != nil { + return 0, errors.WrapError(err, "error iterating order query response") + } + + var searchMap map[string]interface{} + err = json.Unmarshal(searchResult.Value, &searchMap) + if err != nil { + return 0, errors.WrapError(err, "failed to unmarshal searchResult values") + } + + if value, has := searchMap[propertyNameForSum].(float64); has { + sum += value + } + } + + return sum, nil +} diff --git a/querySearch/sort.go b/querySearch/sort.go new file mode 100644 index 0000000..fa3cc67 --- /dev/null +++ b/querySearch/sort.go @@ -0,0 +1,13 @@ +package querysearch + +type OrderSort string + +const ( + Desc OrderSort = "desc" + Asc OrderSort = "asc" +) + +type Sort struct { + Field string + Type OrderSort +}