Skip to content

Commit 7793f5c

Browse files
authored
Merge pull request #651 from vapor/tn-pagination
Pagination
2 parents 5adacb3 + 87cb727 commit 7793f5c

File tree

4 files changed

+185
-63
lines changed

4 files changed

+185
-63
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Vapor
2+
3+
extension QueryBuilder {
4+
public func paginate(
5+
for request: Request
6+
) -> EventLoopFuture<Page<Model>> {
7+
do {
8+
let page = try request.query.decode(PageRequest.self)
9+
return self.paginate(page)
10+
} catch {
11+
return request.eventLoop.makeFailedFuture(error)
12+
}
13+
}
14+
}
15+
16+
extension Page: Content { }
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import Fluent
2+
import Vapor
3+
import XCTVapor
4+
5+
final class FluentPaginationTests: XCTestCase {
6+
func testPagination() throws {
7+
let app = Application(.testing)
8+
defer { app.shutdown() }
9+
10+
var rows: [TestRow] = []
11+
for i in 1...1_000 {
12+
rows.append(TestRow(data: ["id": i, "title": "Todo #\(i)"]))
13+
}
14+
15+
app.databases.use(TestDatabaseDriver { query in
16+
XCTAssertEqual(query.schema, "todos")
17+
let result: [TestRow]
18+
if let limit = query.limits.first?.value, let offset = query.offsets.first?.value {
19+
result = [TestRow](rows[min(offset, rows.count - 1)..<min(offset + limit, rows.count)])
20+
} else {
21+
result = rows
22+
}
23+
24+
if query.fields.count == 1 {
25+
// aggregate
26+
return [
27+
TestRow(data: ["fluentAggregate": rows.count])
28+
]
29+
} else {
30+
return result
31+
}
32+
33+
}, as: .test)
34+
35+
app.get("todos") { req -> EventLoopFuture<Page<Todo>> in
36+
Todo.query(on: req.db).paginate(for: req)
37+
}
38+
39+
try app.test(.GET, "todos") { res in
40+
XCTAssertEqual(res.status, .ok)
41+
let todos = try res.content.decode(Page<Todo>.self)
42+
XCTAssertEqual(todos.items[0].id, 1)
43+
XCTAssertEqual(todos.items.count, 10)
44+
}.test(.GET, "todos?page=2") { res in
45+
XCTAssertEqual(res.status, .ok)
46+
let todos = try res.content.decode(Page<Todo>.self)
47+
XCTAssertEqual(todos.items[0].id, 11)
48+
XCTAssertEqual(todos.items.count, 10)
49+
}.test(.GET, "todos?page=2&per=15") { res in
50+
XCTAssertEqual(res.status, .ok)
51+
let todos = try res.content.decode(Page<Todo>.self)
52+
XCTAssertEqual(todos.items[0].id, 16)
53+
XCTAssertEqual(todos.items.count, 15)
54+
}.test(.GET, "todos?page=1000&per=1") { res in
55+
XCTAssertEqual(res.status, .ok)
56+
let todos = try res.content.decode(Page<Todo>.self)
57+
XCTAssertEqual(todos.items[0].id, 1000)
58+
XCTAssertEqual(todos.items.count, 1)
59+
}.test(.GET, "todos?page=1&per=1") { res in
60+
XCTAssertEqual(res.status, .ok)
61+
let todos = try res.content.decode(Page<Todo>.self)
62+
XCTAssertEqual(todos.items[0].id, 1)
63+
XCTAssertEqual(todos.items.count, 1)
64+
}
65+
}
66+
}
67+
68+
private extension DatabaseQuery.Limit {
69+
var value: Int? {
70+
switch self {
71+
case .count(let count):
72+
return count
73+
case .custom:
74+
return nil
75+
}
76+
}
77+
}
78+
79+
private extension DatabaseQuery.Offset {
80+
var value: Int? {
81+
switch self {
82+
case .count(let count):
83+
return count
84+
case .custom:
85+
return nil
86+
}
87+
}
88+
}
89+
90+
private final class Todo: Model, Content {
91+
static let schema = "todos"
92+
93+
@ID(key: "id")
94+
var id: Int?
95+
96+
@Field(key: "title")
97+
var title: String
98+
99+
init() { }
100+
101+
init(id: Int? = nil, title: String) {
102+
self.id = id
103+
self.title = title
104+
}
105+
}
106+

Tests/FluentTests/FluentTests.swift renamed to Tests/FluentTests/FluentRepositoryTests.swift

Lines changed: 8 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Fluent
22
import Vapor
33
import XCTVapor
44

5-
final class FluentTests: XCTestCase {
5+
final class FluentRepositoryTests: XCTestCase {
66
func testRepositoryPatternStatic() throws {
77
let app = Application(.testing)
88
defer { app.shutdown() }
@@ -65,50 +65,13 @@ final class FluentTests: XCTestCase {
6565
}
6666
}
6767

68-
extension DatabaseID {
69-
static var test: DatabaseID { .init(string: "test") }
70-
}
71-
72-
struct TestRow: DatabaseRow {
73-
var data: [String: Any]
74-
75-
var description: String {
76-
self.data.description
77-
}
78-
79-
func contains(field: String) -> Bool {
80-
self.data.keys.contains(field)
81-
}
82-
83-
func decode<T>(field: String, as type: T.Type, for database: Database) throws -> T where T : Decodable {
84-
return self.data[field] as! T
85-
}
86-
}
87-
88-
final class TestDatabaseDriver: DatabaseDriver {
89-
let handler: (DatabaseQuery) -> [DatabaseRow]
90-
91-
init(_ handler: @escaping (DatabaseQuery) -> [DatabaseRow]) {
92-
self.handler = handler
93-
}
94-
95-
func makeDatabase(with context: DatabaseContext) -> Database {
96-
TestDatabase(driver: self, context: context)
97-
}
98-
99-
func shutdown() {
100-
// nothing
101-
}
102-
}
103-
104-
105-
extension Request {
68+
private extension Request {
10669
var posts: PostRepository {
10770
self.application.posts.makePosts!(self)
10871
}
10972
}
11073

111-
extension Application {
74+
private extension Application {
11275
var posts: PostRepositoryFactory {
11376
get {
11477
if let existing = self.userInfo["posts"] as? PostRepositoryFactory {
@@ -125,32 +88,14 @@ extension Application {
12588
}
12689
}
12790

128-
struct PostRepositoryFactory {
91+
private struct PostRepositoryFactory {
12992
var makePosts: ((Request) -> PostRepository)?
13093
mutating func use(_ makePosts: @escaping (Request) -> PostRepository) {
13194
self.makePosts = makePosts
13295
}
13396
}
13497

135-
struct TestDatabase: Database {
136-
let driver: TestDatabaseDriver
137-
let context: DatabaseContext
138-
139-
func execute(query: DatabaseQuery, onRow: @escaping (DatabaseRow) -> ()) -> EventLoopFuture<Void> {
140-
self.driver.handler(query).forEach(onRow)
141-
return self.eventLoop.makeSucceededFuture(())
142-
}
143-
144-
func execute(schema: DatabaseSchema) -> EventLoopFuture<Void> {
145-
fatalError()
146-
}
147-
148-
func withConnection<T>(_ closure: @escaping (Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> {
149-
closure(self)
150-
}
151-
}
152-
153-
final class Post: Model, Content, Equatable {
98+
private final class Post: Model, Content, Equatable {
15499
static func == (lhs: Post, rhs: Post) -> Bool {
155100
lhs.id == rhs.id && lhs.content == rhs.content
156101
}
@@ -171,7 +116,7 @@ final class Post: Model, Content, Equatable {
171116
}
172117
}
173118

174-
struct TestPostRepository: PostRepository {
119+
private struct TestPostRepository: PostRepository {
175120
let posts: [Post]
176121
let eventLoop: EventLoop
177122

@@ -180,13 +125,13 @@ struct TestPostRepository: PostRepository {
180125
}
181126
}
182127

183-
struct DatabasePostRepository: PostRepository {
128+
private struct DatabasePostRepository: PostRepository {
184129
let database: Database
185130
func all() -> EventLoopFuture<[Post]> {
186131
database.query(Post.self).all()
187132
}
188133
}
189134

190-
protocol PostRepository {
135+
private protocol PostRepository {
191136
func all() -> EventLoopFuture<[Post]>
192137
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Fluent
2+
3+
extension DatabaseID {
4+
static var test: DatabaseID { .init(string: "test") }
5+
}
6+
7+
struct TestDatabase: Database {
8+
let driver: TestDatabaseDriver
9+
let context: DatabaseContext
10+
11+
func execute(query: DatabaseQuery, onRow: @escaping (DatabaseRow) -> ()) -> EventLoopFuture<Void> {
12+
self.driver.handler(query).forEach(onRow)
13+
return self.eventLoop.makeSucceededFuture(())
14+
}
15+
16+
func execute(schema: DatabaseSchema) -> EventLoopFuture<Void> {
17+
fatalError()
18+
}
19+
20+
func withConnection<T>(_ closure: @escaping (Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> {
21+
closure(self)
22+
}
23+
}
24+
25+
struct TestRow: DatabaseRow {
26+
var data: [String: Any]
27+
28+
var description: String {
29+
self.data.description
30+
}
31+
32+
func contains(field: String) -> Bool {
33+
self.data.keys.contains(field)
34+
}
35+
36+
func decode<T>(field: String, as type: T.Type, for database: Database) throws -> T where T : Decodable {
37+
return self.data[field] as! T
38+
}
39+
}
40+
41+
final class TestDatabaseDriver: DatabaseDriver {
42+
let handler: (DatabaseQuery) -> [DatabaseRow]
43+
44+
init(_ handler: @escaping (DatabaseQuery) -> [DatabaseRow]) {
45+
self.handler = handler
46+
}
47+
48+
func makeDatabase(with context: DatabaseContext) -> Database {
49+
TestDatabase(driver: self, context: context)
50+
}
51+
52+
func shutdown() {
53+
// nothing
54+
}
55+
}

0 commit comments

Comments
 (0)