diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..6940a61 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,39 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master", "dev" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... + + - name: check test coverage + uses: vladopajic/go-test-coverage@v2 + with: + config: ./.testcoverage.yml + ## when token is not specified (value '') this feature is turned off + ## in this example badge is created and committed only for main branch + git-token: ${{ github.ref_name == 'main' && secrets.GITHUB_TOKEN || '' }} + ## name of branch where badges are stored + ## ideally this should be orphan branch (see below how to create this branch) + git-branch: badges diff --git a/.gitignore b/.gitignore index 6f72f89..1c2a4fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go,intellij +# Edit at https://www.toptal.com/developers/gitignore?templates=go,intellij + +### Go ### # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # @@ -19,7 +23,100 @@ # Go workspace file go.work -go.work.sum -# env file -.env +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# CMake +cmake-build-*/ + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +*.iml +modules.xml +.idea/.gitignore +.idea/vcs.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/go,intellij \ No newline at end of file diff --git a/.testcoverage.yml b/.testcoverage.yml new file mode 100644 index 0000000..2469a98 --- /dev/null +++ b/.testcoverage.yml @@ -0,0 +1,32 @@ +# (mandatory) +# Path to coverprofile file (output of `go test -coverprofile` command). +# +# For cases where there are many coverage profiles, such as when running +# unit tests and integration tests separately, you can combine all those +# profiles into one. In this case, the profile should have a comma-separated list +# of profile files, e.g., 'cover_unit.out,cover_integration.out'. +profile: cover.out + +local-prefix: "github.com/manuelarte/pagorminator" + +# Holds coverage thresholds percentages, values should be in range [0-100] +threshold: + # (optional; default 0) + # The minimum coverage that each file should have + file: 50 + + # (optional; default 0) + # The minimum coverage that each package should have + package: 50 + + # (optional; default 0) + # The minimum total coverage project should have + total: 50 + +# Holds regexp rules which will exclude matched files or packages +# from coverage statistics +exclude: + # Exclude files or packages matching their paths + paths: + - ^internal/model.go$ + - ^pkg/bar # exclude package `pkg/bar` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6151294 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### TODO + +- Add code coverage badge +- Check for fmt +- Add more examples + +### Added + +- Added pagorminator plugin for gorm +- Added examples \ No newline at end of file diff --git a/README.md b/README.md index 83741fc..9497b37 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ -# pagorminator -Pagination plugin for Gorm +[![Go](https://github.com/manuelarte/pagorminator/actions/workflows/go.yml/badge.svg)](https://github.com/manuelarte/pagorminator/actions/workflows/go.yml) +# 📃 pagorminator + +Gorm plugin to add pagination to your select queries + +## 😍 How to install it + +> go get github.com/manuelarte/pagorminator + +## 🎯 How to use it + +```go +var products []*Products +// give me the first 10 products +db.Scopes(pagorminator.WithPagination(pagorminator.PageRequestOf(0, 10))).Find(&products) +``` + +## 🎓 Examples + +- [Simple](./examples/simple/main.go) + +Simple query with no filters (where clause) + +- Filtered + +Using where to filter + +## Contact + +- 📧 manueldoncelmartos@gmail.com diff --git a/examples/simple/go.mod b/examples/simple/go.mod new file mode 100644 index 0000000..427468f --- /dev/null +++ b/examples/simple/go.mod @@ -0,0 +1,16 @@ +module simple + +go 1.18 + +require ( + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/manuelarte/pagorminator v0.0.0-20241109213332-29069dcc8d67 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + golang.org/x/text v0.20.0 // indirect +) diff --git a/examples/simple/go.sum b/examples/simple/go.sum new file mode 100644 index 0000000..a4ce51c --- /dev/null +++ b/examples/simple/go.sum @@ -0,0 +1,16 @@ +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/manuelarte/pagorminator v0.0.0-20241109213332-29069dcc8d67 h1:HhZkJJJ8/xalo8HK4ENMG+7YsfvliSv8ALti4Sa2Y9s= +github.com/manuelarte/pagorminator v0.0.0-20241109213332-29069dcc8d67/go.mod h1:e7ZYAl1XwI3uc0rOXmfF4FToPSS+C65DM4sPXwRNkKs= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/examples/simple/main.go b/examples/simple/main.go new file mode 100644 index 0000000..d0c6d60 --- /dev/null +++ b/examples/simple/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "github.com/manuelarte/pagorminator" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type Product struct { + gorm.Model + Code string + Price uint +} + +func (p Product) String() string { + return fmt.Sprintf("Product{Code: %s, Price: %d}", p.Code, p.Price) +} + +func main() { + db, err := gorm.Open(sqlite.Open("file:mem?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + + db.Use(pagorminator.PaGormMinator{}) + // Migrate the schema + db.AutoMigrate(&Product{}) + + // Create + db.Create(&Product{Code: "D42", Price: 100}) + + // Read + var products []*Product + pageRequest, _ := pagorminator.PageRequestOf(0, 1) + db.Scopes(pagorminator.WithPagination(pageRequest)).First(&products) + for _, product := range products { + fmt.Printf("PageRequest: {Page: %d, Size: %d, TotalElements: %d, TotalPages: %d\n", + pageRequest.GetPage(), pageRequest.GetSize(), pageRequest.GetTotalElements(), pageRequest.GetTotalPages()) + println(product.String()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a1ea0ee --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/manuelarte/pagorminator + +go 1.18 + +require ( + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + golang.org/x/text v0.19.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3e3ba70 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/internal/model.go b/internal/model.go new file mode 100644 index 0000000..bab061a --- /dev/null +++ b/internal/model.go @@ -0,0 +1,32 @@ +package internal + +type PageRequestImpl struct { + Page int + Size int + TotalPages int + TotalElements int +} + +func (p PageRequestImpl) GetOffset() int { + return (p.Page - 1) * p.Size +} + +func (p PageRequestImpl) GetPage() int { + return p.Page +} + +func (p PageRequestImpl) GetSize() int { + return p.Size +} + +func (p PageRequestImpl) GetTotalPages() int { + return p.TotalPages +} + +func (p PageRequestImpl) GetTotalElements() int { + return p.TotalElements +} + +func (p PageRequestImpl) IsUnPaged() bool { + return p.Size == 0 && p.Page == 0 +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..ee438cb --- /dev/null +++ b/model.go @@ -0,0 +1,43 @@ +package pagorminator + +import ( + "errors" + "github.com/manuelarte/pagorminator/internal" +) + +var ( + ErrPageCantBeNegative = errors.New("page number can't be negative") + ErrSizeCantBeNegative = errors.New("size can't be negative") + ErrSizeNotAllowed = errors.New("size is not allowed") +) + +var _ PageRequest = internal.PageRequestImpl{} + +// PageRequest Struct that contains the pagination information +type PageRequest interface { + GetPage() int + GetSize() int + GetOffset() int + GetTotalPages() int + GetTotalElements() int + IsUnPaged() bool +} + +// PageRequestOf Creates a PageRequest with the page and size values +func PageRequestOf(page, size int) (PageRequest, error) { + if page < 0 { + return nil, ErrPageCantBeNegative + } + if size < 0 { + return nil, ErrSizeCantBeNegative + } + if page > 0 && size == 0 { + return nil, ErrSizeNotAllowed + } + return &internal.PageRequestImpl{Page: page, Size: size}, nil +} + +// UnPaged Create an unpaged request (no pagination is applied) +func UnPaged() PageRequest { + return &internal.PageRequestImpl{Page: 0, Size: 0} +} diff --git a/pagorminator.go b/pagorminator.go new file mode 100644 index 0000000..1a306ca --- /dev/null +++ b/pagorminator.go @@ -0,0 +1,91 @@ +package pagorminator + +import ( + "github.com/manuelarte/pagorminator/internal" + "gorm.io/gorm" +) + +const ( + countKey = "pagorminator.count" +) + +func WithPagination(pageRequest PageRequest) func(*gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Set("pagorminator:pageRequest", pageRequest) + } +} + +var _ gorm.Plugin = new(PaGormMinator) + +// PaGormMinator Gorm plugin to add pagination to your queries +type PaGormMinator struct { +} + +func (p PaGormMinator) Name() string { + return "pagorminator" +} + +func (p PaGormMinator) Initialize(db *gorm.DB) error { + err := db.Callback().Query().Before("gorm:query").Register("pagorminator:addPagination", p.addPagination) + if err != nil { + return err + } + err = db.Callback().Query().After("pagorminator:addPagination").Register("pagorminator:count", p.count) + if err != nil { + return err + } + return nil +} + +func (p PaGormMinator) addPagination(db *gorm.DB) { + if db.Statement.Schema != nil { + if pageRequest, ok := p.getPageRequest(db); ok { + if !pageRequest.IsUnPaged() { + db.Limit(pageRequest.GetSize()).Offset(pageRequest.GetOffset()) + } + } + + } +} + +func (p PaGormMinator) count(db *gorm.DB) { + if db.Statement.Schema != nil { + if pageRequest, ok := p.getPageRequest(db); ok { + if value, ok := db.Get(countKey); !ok || !value.(bool) { + casted, _ := pageRequest.(*internal.PageRequestImpl) + + newDb := db.Session(&gorm.Session{NewDB: true}) + newDb.Statement = db.Statement.Statement + + var totalElements int64 + tx := newDb.Debug().Set(countKey, true). + Model(newDb.Statement.Model) + if whereClause, existWhere := db.Statement.Clauses["WHERE"]; existWhere { + tx.Where(whereClause.Expression) + } + tx.Count(&totalElements) + if tx.Error != nil { + db.AddError(tx.Error) + } else { + casted.TotalElements = int(totalElements) + if casted.IsUnPaged() { + casted.Page = 0 + casted.TotalPages = 1 + } else { + casted.TotalPages = int(totalElements) / casted.Size + } + } + } + } + + } +} + +func (p PaGormMinator) getPageRequest(db *gorm.DB) (PageRequest, bool) { + if value, ok := db.Get("pagorminator:pageRequest"); ok { + if pageRequest, ok := value.(PageRequest); ok { + return pageRequest, true + } + } + return nil, false +} diff --git a/pagorminator_test.go b/pagorminator_test.go new file mode 100644 index 0000000..1959b37 --- /dev/null +++ b/pagorminator_test.go @@ -0,0 +1,186 @@ +package pagorminator + +import ( + "fmt" + "github.com/manuelarte/pagorminator/internal" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "testing" +) + +type TestStruct struct { + gorm.Model + Code string + Price uint +} + +func TestPaginationScopeMetadata_NoWhere(t *testing.T) { + tests := map[string]struct { + toMigrate []*TestStruct + pageRequest PageRequest + expectedPage PageRequest + }{ + "UnPaged one item": { + toMigrate: []*TestStruct{ + {Code: "1"}, + }, + pageRequest: UnPaged(), + expectedPage: &internal.PageRequestImpl{ + Page: 0, + Size: 0, + TotalElements: 1, + TotalPages: 1, + }, + }, + "UnPaged several items": { + toMigrate: []*TestStruct{ + {Code: "1", Price: 1}, {Code: "2", Price: 2}, + }, + pageRequest: UnPaged(), + expectedPage: &internal.PageRequestImpl{ + Page: 0, + Size: 0, + TotalElements: 2, + TotalPages: 1, + }, + }, + "Paged 1/2 items": { + toMigrate: []*TestStruct{ + {Code: "1", Price: 1}, {Code: "2", Price: 2}, + }, + pageRequest: mustPageRequestOf(1, 1), + expectedPage: &internal.PageRequestImpl{ + Page: 1, + Size: 1, + TotalElements: 2, + TotalPages: 2, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + db := setupDb(t, name) + db.CreateInBatches(&test.toMigrate, len(test.toMigrate)) + + // Read + var products []*TestStruct + + db.Scopes(WithPagination(test.pageRequest)).Find(&products) // find product with integer primary key + if !equalPageRequests(test.pageRequest, test.expectedPage) { + t.Fatalf("expected page to be %d, got %d", test.expectedPage, test.pageRequest) + } + }) + } +} + +func TestPaginationScopeMetadata_Where(t *testing.T) { + tests := map[string]struct { + toMigrate []*TestStruct + pageRequest PageRequest + where string + expectedPage PageRequest + }{ + "UnPaged one item, not filtered": { + toMigrate: []*TestStruct{ + {Code: "1", Price: 1}, + }, + pageRequest: UnPaged(), + where: "price < 100", + expectedPage: &internal.PageRequestImpl{ + Page: 0, + Size: 0, + TotalElements: 1, + TotalPages: 1, + }, + }, + "UnPaged one item, filtered out": { + toMigrate: []*TestStruct{ + {Code: "1", Price: 1}, + }, + pageRequest: UnPaged(), + where: "price > 100", + expectedPage: &internal.PageRequestImpl{ + Page: 0, + Size: 0, + TotalElements: 0, + TotalPages: 1, + }, + }, + "UnPaged two items, one filtered out": { + toMigrate: []*TestStruct{ + {Code: "1", Price: 1}, {Code: "100", Price: 100}, + }, + pageRequest: UnPaged(), + where: "price > 50", + expectedPage: &internal.PageRequestImpl{ + Page: 0, + Size: 0, + TotalElements: 1, + TotalPages: 1, + }, + }, + "Paged four items, two filtered out": { + toMigrate: []*TestStruct{ + {Code: "1", Price: 1}, {Code: "2", Price: 2}, + {Code: "1", Price: 100}, {Code: "2", Price: 200}, + }, + pageRequest: mustPageRequestOf(0, 1), + where: "price > 50", + expectedPage: &internal.PageRequestImpl{ + Page: 0, + Size: 1, + TotalElements: 2, + TotalPages: 2, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + db := setupDb(t, name) + db.CreateInBatches(&test.toMigrate, len(test.toMigrate)) + + // Read + var products []*TestStruct + + db.Debug().Scopes(WithPagination(test.pageRequest)).Where(test.where).Find(&products) // find product with integer primary key + if !equalPageRequests(test.pageRequest, test.expectedPage) { + t.Fatalf("expected page to be %d, got %d", test.expectedPage, test.pageRequest) + } + }) + } +} + +func setupDb(t *testing.T, name string) *gorm.DB { + db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", name)), &gorm.Config{}) + if err != nil { + t.Fatal("failed to connect database") + } + + // Migrate the schema + err = db.AutoMigrate(&TestStruct{}) + if err != nil { + t.Fatal(err) + } + err = db.Use(PaGormMinator{}) + if err != nil { + t.Fatal(err) + } + + return db +} + +func mustPageRequestOf(page, size int) PageRequest { + toReturn, _ := PageRequestOf(page, size) + return toReturn +} + +func equalPageRequests(p1, p2 PageRequest) bool { + casted1 := p1.(*internal.PageRequestImpl) + casted2 := p2.(*internal.PageRequestImpl) + return casted1.Page == casted2.Page && + casted1.Size == casted2.Size && + casted1.TotalElements == casted2.TotalElements && + casted1.TotalPages == casted2.TotalPages +}