diff --git a/src/config/constants.go b/src/config/constants.go index 78ab265..9f90461 100644 --- a/src/config/constants.go +++ b/src/config/constants.go @@ -30,6 +30,7 @@ const ( IndividualHeaderKey = "X-ConsentBB-IndividualId" RevisionId = "revisionId" LawfulBasis = "lawfulBasis" + Id = "id" ) // Schemas diff --git a/src/database/db.go b/src/database/db.go index 5af5af9..a4a7ae8 100644 --- a/src/database/db.go +++ b/src/database/db.go @@ -157,7 +157,7 @@ func Init(config *config.Configuration) error { return err } - err = initCollection("dataAgreementRecords", []string{"id"}, true) + err = initCollection("dataAgreementRecords", []string{"id", "dataagreementid", "individualid"}, true) if err != nil { return err } diff --git a/src/v2/dataagreement_record/dataagreement_record.go b/src/v2/dataagreement_record/dataagreement_record.go index 8e7f967..c8cf1d0 100644 --- a/src/v2/dataagreement_record/dataagreement_record.go +++ b/src/v2/dataagreement_record/dataagreement_record.go @@ -47,7 +47,7 @@ type DataAgreementRecordForAuditList struct { OptIn bool `json:"optIn"` State string `json:"state" valid:"required"` SignatureId string `json:"signatureId"` - AgreementData DataAgreementForListDataAgreementRecord `json:"dataAgreement"` + DataAgreements DataAgreementForListDataAgreementRecord `json:"dataAgreement"` Timestamp string `json:"timestamp"` } @@ -61,6 +61,7 @@ const ( RevisionIdIsMissingError DataAgreementRecordIdIsMissingError LawfulBasisIsMissingError + IdIsMissingError ) // Error returns the string representation of the error. @@ -76,6 +77,8 @@ func (e DataAgreementRecordError) Error() string { return "Query param dataAgreementRecordId is missing!" case LawfulBasisIsMissingError: return "Query param lawfulbasis is missing!" + case IdIsMissingError: + return "Query param id is missing!" default: return "Unknown error!" } diff --git a/src/v2/dataagreement_record/db.go b/src/v2/dataagreement_record/db.go index f76e18c..0066b23 100644 --- a/src/v2/dataagreement_record/db.go +++ b/src/v2/dataagreement_record/db.go @@ -90,424 +90,96 @@ func (darRepo *DataAgreementRecordRepository) DeleteAllRecordsForIndividual(indi return err } -// ListByIdIncludingDataAgreement lists data agreement record by id -func ListByIdIncludingDataAgreement(dataAgreementRecordID string, organisationId string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - dataAgreementRecordId, err := primitive.ObjectIDFromHex(dataAgreementRecordID) - if err != nil { - return results, err - } - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false, "_id": dataAgreementRecordId}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, - }, - }, - }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, - }, - }, - }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, - }, - { - "$limit": 1, - }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, - } - - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err - } - defer cursor.Close(context.TODO()) - - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err - } - return results, nil -} - -// ListByIdAndLawfulBasis lists data attributes based on data agreement record and lawfulbasis -func ListByIdAndLawfulBasis(dataAgreementRecordID string, organisationId string, lawfulBasis string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - dataAgreementRecordId, err := primitive.ObjectIDFromHex(dataAgreementRecordID) - if err != nil { - return results, err - } - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false, "_id": dataAgreementRecordId}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, - }, +// PipelineForList creates pipeline for list data agreement records +func PipelineForList(organisationId string, id string, lawfulBasis string, isId bool, isLawfulBasis bool) ([]primitive.M, error) { + var pipeline []primitive.M + + var pipelineForIdExists []primitive.M + if isId { + dataAgreementRecordId, err := primitive.ObjectIDFromHex(id) + if err != nil { + return []bson.M{}, err + } + + pipelineForIdExists = []bson.M{ + {"$match": bson.M{ + "$or": []bson.M{ + {"_id": dataAgreementRecordId}, + {"dataagreementid": id}, + {"individualid": id}, }, }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - { - "$match": bson.M{ - "agreementData.lawfulbasis": lawfulBasis, }, - }, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, - }, - }, - }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, - }, - { - "$limit": 1, - }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, - } - - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err - } - defer cursor.Close(context.TODO()) - - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err - } - return results, nil -} - -// ListByDataAgreementIdIncludingDataAgreement lists data agreement record based on data agreement id -func ListByDataAgreementIdIncludingDataAgreement(dataAgreementId string, organisationId string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false, "dataagreementid": dataAgreementId}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, - }, - }, - }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, - }, - }, - }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, - }, - { - "$limit": 1, - }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, - } - - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err + } } - defer cursor.Close(context.TODO()) - - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err - } - return results, nil -} - -// ListByDataAgreementIdAndLawfulBasis lists data agreement record based on data agreement id and lawful basis -func ListByDataAgreementIdAndLawfulBasis(dataAgreementId string, organisationId string, lawfulBasis string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false, "dataagreementid": dataAgreementId}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, + lookupAgreementStage := bson.M{"$lookup": bson.M{ + "from": "dataAgreements", + "let": bson.M{"localId": "$dataagreementid"}, + "pipeline": bson.A{ + bson.M{ + "$match": bson.M{ + "$expr": bson.M{ + "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, }, }, }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - { - "$match": bson.M{ - "agreementData.lawfulbasis": lawfulBasis, - }, }, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, + "as": "dataAgreements", + }} + unwindStage := bson.M{"$unwind": "$dataAgreements"} + lookupRevisionStage := bson.M{"$lookup": bson.M{ + "from": "revisions", + "let": bson.M{"localId": "$_id"}, + "pipeline": bson.A{ + bson.M{ + "$match": bson.M{ + "$expr": bson.M{ + "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, }, }, }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, - }, - { - "$limit": 1, }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, - } - - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err - } - defer cursor.Close(context.TODO()) - - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err - } - return results, nil -} - -// ListByIndividualIdIncludingDataAgreement -func ListByIndividualIdIncludingDataAgreement(individualId string, organisationId string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false, "individualid": individualId}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, + "as": "Revisions", + }} + addRevisionFieldStage := bson.M{ + "$addFields": bson.M{ + "Revision": bson.M{ + "$arrayElemAt": []interface{}{ + bson.M{ + "$slice": []interface{}{"$Revisions", -1}, }, + 0, }, }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, - }, - }, - }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, - }, - { - "$limit": 1, }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, } - - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err - } - defer cursor.Close(context.TODO()) - - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err - } - return results, nil -} - -func ListByIndividualIdAndLawfulBasis(individualId string, organisationId string, lawfulBasis string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false, "individualid": individualId}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, - }, - }, - }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - { - "$match": bson.M{ - "agreementData.lawfulbasis": lawfulBasis, - }, + addTimestampFieldStage := bson.M{"$addFields": bson.M{"timestamp": "$Revision.timestamp"}} + projectStage := bson.M{ + "$project": bson.M{ + "Revisions": 0, + "Revision": 0, }, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, - }, - }, - }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, - }, - { - "$limit": 1, - }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, - } - - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err } - defer cursor.Close(context.TODO()) - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err + pipelineForIdNotExists := []bson.M{ + {"$match": bson.M{"organisationid": organisationId, "isdeleted": false}}, } - return results, nil -} -func ListsWithDataAgreementAndTimestamp(organisationId string) ([]DataAgreementRecordForAuditList, error) { - var results []DataAgreementRecordForAuditList - - pipeline := []bson.M{ - {"$match": bson.M{"organisationid": organisationId, "isdeleted": false}}, - {"$lookup": bson.M{ - "from": "dataAgreements", - "let": bson.M{"localId": "$dataagreementid"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$_id", bson.M{"$toObjectId": "$$localId"}}, - }, - }, - }, - }, - "as": "agreementData", - }}, - {"$unwind": "$agreementData"}, - {"$lookup": bson.M{ - "from": "revisions", - "let": bson.M{"localId": "$_id"}, - "pipeline": bson.A{ - bson.M{ - "$match": bson.M{ - "$expr": bson.M{ - "$eq": []interface{}{"$objectid", bson.M{"$toString": "$$localId"}}, - }, - }, - }, - }, - "as": "revisions", - }}, - {"$unwind": "$revisions"}, - { - "$sort": bson.M{"revisions.timestamp": -1}, + lawfulBasisMatch := bson.M{ + "$match": bson.M{ + "dataAgreements.lawfulbasis": lawfulBasis, }, - { - "$limit": 1, - }, - {"$addFields": bson.M{"timestamp": "$revisions.timestamp"}}, } + if isId && isLawfulBasis { + pipeline = append(pipelineForIdExists, lookupAgreementStage, unwindStage, lookupRevisionStage, addRevisionFieldStage, addTimestampFieldStage, projectStage, lawfulBasisMatch) + } else if isId && !isLawfulBasis { + pipeline = append(pipelineForIdExists, lookupAgreementStage, unwindStage, lookupRevisionStage, addRevisionFieldStage, addTimestampFieldStage, projectStage) - cursor, err := Collection().Aggregate(context.TODO(), pipeline) - if err != nil { - return results, err + } else if isLawfulBasis && !isId { + pipeline = append(pipelineForIdNotExists, lookupAgreementStage, unwindStage, lookupRevisionStage, addRevisionFieldStage, addTimestampFieldStage, projectStage, lawfulBasisMatch) + } else { + pipeline = []bson.M{} } - defer cursor.Close(context.TODO()) - if err = cursor.All(context.TODO(), &results); err != nil { - return results, err - } - return results, nil + return pipeline, nil } diff --git a/src/v2/handler/audit/audit_list_dataagreement_records.go b/src/v2/handler/audit/audit_list_dataagreement_records.go index c4f4dd1..74980ef 100644 --- a/src/v2/handler/audit/audit_list_dataagreement_records.go +++ b/src/v2/handler/audit/audit_list_dataagreement_records.go @@ -1,7 +1,7 @@ package audit import ( - "encoding/json" + "context" "errors" "log" "net/http" @@ -10,16 +10,9 @@ import ( "github.com/bb-consent/api/src/config" daRecord "github.com/bb-consent/api/src/v2/dataagreement_record" "github.com/bb-consent/api/src/v2/paginate" + "go.mongodb.org/mongo-driver/bson" ) -func dataAgreementRecordToInterfaceSlice(dataAgreementRecords []daRecord.DataAgreementRecordForAuditList) []interface{} { - interfaceSlice := make([]interface{}, len(dataAgreementRecords)) - for i, r := range dataAgreementRecords { - interfaceSlice[i] = r - } - return interfaceSlice -} - type DataAgreementForListDataAgreementRecord struct { Id string `json:"id" bson:"_id,omitempty"` Purpose string `json:"purpose"` @@ -43,119 +36,59 @@ func AuditListDataAgreementRecords(w http.ResponseWriter, r *http.Request) { darRepo := daRecord.DataAgreementRecordRepository{} darRepo.Init(organisationId) - var isNotDataAgreementRecordId bool - var isNotIndividualId bool - var isNotDataAgreementId bool - var isNotLawfulBasis bool - dataAgreementRecordId, err := daRecord.ParseQueryParams(r, config.DataAgreementRecordId, daRecord.DataAgreementRecordIdIsMissingError) - if err != nil && errors.Is(err, daRecord.DataAgreementRecordIdIsMissingError) { - isNotDataAgreementRecordId = true - } - dataAgreementId, err := daRecord.ParseQueryParams(r, config.DataAgreementId, daRecord.DataAgreementIdIsMissingError) - if err != nil && errors.Is(err, daRecord.DataAgreementIdIsMissingError) { - isNotDataAgreementId = true - } - individualId, err := daRecord.ParseQueryParams(r, config.IndividualId, daRecord.IndividualIdIsMissingError) - if err != nil && errors.Is(err, daRecord.IndividualIdIsMissingError) { - isNotIndividualId = true + var isIdExists bool + var isLawfulBasis bool + id, err := daRecord.ParseQueryParams(r, config.Id, daRecord.IdIsMissingError) + if err != nil && errors.Is(err, daRecord.IdIsMissingError) { + isIdExists = false + } else { + isIdExists = true } lawfulBasis, err := daRecord.ParseQueryParams(r, config.LawfulBasis, daRecord.LawfulBasisIsMissingError) if err != nil && errors.Is(err, daRecord.LawfulBasisIsMissingError) { - isNotLawfulBasis = true + isLawfulBasis = false + } else { + isLawfulBasis = true } - var daRecords []daRecord.DataAgreementRecordForAuditList - if isNotLawfulBasis { - if isNotDataAgreementRecordId { - if isNotIndividualId { - if isNotDataAgreementId { - // fetch all data agreement records - daRecords, err = daRecord.ListsWithDataAgreementAndTimestamp(organisationId) - if err != nil { - m := "Failed to fetch data agreement records" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } - - } else { - // fetch by data agreement id - daRecords, err = daRecord.ListByDataAgreementIdIncludingDataAgreement(dataAgreementId, organisationId) - if err != nil { - m := "Failed to fetch data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } - } - - } else { - // fetch by individual id - daRecords, err = daRecord.ListByIndividualIdIncludingDataAgreement(individualId, organisationId) - if err != nil { - m := "Failed to fetch data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } - } - - } else { - // fetch by data agreement record id - daRecords, err = daRecord.ListByIdIncludingDataAgreement(dataAgreementRecordId, organisationId) - if err != nil { - m := "Failed to fetch data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } - - } - - } else { - if isNotDataAgreementRecordId { - if isNotIndividualId { - // fetch by data agreement id and lawful basis - daRecords, err = daRecord.ListByDataAgreementIdAndLawfulBasis(dataAgreementId, organisationId, lawfulBasis) - if err != nil { - m := "Failed to fetch data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } - } else { - // fetch by individual id and lawfulusage - daRecords, err = daRecord.ListByIndividualIdAndLawfulBasis(individualId, organisationId, lawfulBasis) - if err != nil { - m := "Failed to fetch data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } - } + pipeline, err := daRecord.PipelineForList(organisationId, id, lawfulBasis, isIdExists, isLawfulBasis) + if err != nil { + m := "Failed to create pipeline" + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } - } else { - // fetch by data agreement record id and lawful basis - daRecords, err = daRecord.ListByIdAndLawfulBasis(dataAgreementRecordId, organisationId, lawfulBasis) - if err != nil { - m := "Failed to fetch data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return + pipeline = append(pipeline, bson.M{"$sort": bson.M{"timestamp": -1}}) + var daRecords []daRecord.DataAgreementRecordForAuditList + query := paginate.PaginateDBObjectsQueryUsingPipeline{ + Pipeline: pipeline, + Collection: daRecord.Collection(), + Context: context.Background(), + Limit: limit, + Offset: offset, + } + var resp fetchDataAgreementRecordsResp + result, err := paginate.PaginateDBObjectsUsingPipeline(query, &daRecords) + if err != nil { + if errors.Is(err, paginate.EmptyDBError) { + emptyDaRecords := make([]interface{}, 0) + resp = fetchDataAgreementRecordsResp{ + DataAgreementRecords: emptyDaRecords, + Pagination: result.Pagination, } - + common.ReturnHTTPResponse(resp, w) + return } + m := "Failed to paginate data agreement records" + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return } - query := paginate.PaginateObjectsQuery{ - Limit: limit, - Offset: offset, - } - interfaceSlice := dataAgreementRecordToInterfaceSlice(daRecords) - result := paginate.PaginateObjects(query, interfaceSlice) - - var resp = fetchDataAgreementRecordsResp{ + resp = fetchDataAgreementRecordsResp{ DataAgreementRecords: result.Items, Pagination: result.Pagination, } - - response, _ := json.Marshal(resp) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.WriteHeader(http.StatusOK) - w.Write(response) + common.ReturnHTTPResponse(resp, w) } diff --git a/src/v2/paginate/paginate.go b/src/v2/paginate/paginate.go index c7afb7c..247b3f3 100644 --- a/src/v2/paginate/paginate.go +++ b/src/v2/paginate/paginate.go @@ -241,6 +241,9 @@ func PaginateDBObjectsUsingPipeline(query PaginateDBObjectsQueryUsingPipeline, r if query.Offset >= int(totalItems) { query.Offset = int(totalItems) - query.Limit } + if query.Offset < 0 { + query.Offset = 0 + } // Calculate pages and selected page based on offset and limit totalPages := int(math.Ceil(float64(totalItems) / float64(query.Limit)))