Skip to content

Commit c67c33a

Browse files
authored
Add page size limit (#715)
1 parent bf1f555 commit c67c33a

File tree

6 files changed

+201
-40
lines changed

6 files changed

+201
-40
lines changed

.github/workflows/test.yml

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,33 @@
11
name: test
2-
on:
3-
pull_request:
4-
push:
5-
branches:
6-
- master
2+
on: { pull_request: {} }
3+
74
jobs:
8-
linux:
5+
getcidata:
96
runs-on: ubuntu-latest
7+
outputs:
8+
environments: ${{ steps.output.outputs.environments }}
9+
steps:
10+
- id: output
11+
run: |
12+
envblob="$(curl -fsSL https://raw.githubusercontent.com/vapor/ci/main/pr-environments.json | jq -cMj '.')"
13+
echo "::set-output name=environments::${envblob}"
14+
15+
test-fluent:
16+
needs: getcidata
1017
strategy:
1118
fail-fast: false
1219
matrix:
13-
image:
14-
- swift:5.2-xenial
15-
- swift:5.2-bionic
16-
- swiftlang/swift:nightly-5.2-xenial
17-
- swiftlang/swift:nightly-5.2-bionic
18-
- swiftlang/swift:nightly-5.3-xenial
19-
- swiftlang/swift:nightly-5.3-bionic
20-
- swiftlang/swift:nightly-master-xenial
21-
- swiftlang/swift:nightly-master-bionic
22-
- swiftlang/swift:nightly-master-focal
23-
- swiftlang/swift:nightly-master-centos8
24-
- swiftlang/swift:nightly-master-amazonlinux2
25-
container: ${{ matrix.image }}
26-
env:
27-
LOG_LEVEL: info
28-
steps:
29-
- name: Checkout Fluent
30-
uses: actions/checkout@v2
31-
- name: Run base tests with Thread Sanitizer
32-
run: swift test --enable-test-discovery --sanitize=thread
33-
macOS:
34-
env:
35-
LOG_LEVEL: info
36-
runs-on: macos-latest
37-
steps:
38-
- name: Select latest available Xcode
39-
uses: maxim-lobanov/[email protected]
20+
env: ${{ fromJSON(needs.getcidata.outputs.environments) }}
21+
runs-on: ${{ matrix.env.os }}
22+
container: ${{ matrix.env.image }}
23+
steps:
24+
- name: Select toolchain
25+
uses: maxim-lobanov/[email protected]
4026
with:
41-
xcode-version: latest
42-
- name: Checkout Fluent
27+
xcode-version: ${{ matrix.env.toolchain }}
28+
if: ${{ matrix.env.toolchain != '' }}
29+
- name: Check out Vapor
4330
uses: actions/checkout@v2
44-
- name: Run base Fluent tests with Thread Sanitizer
45-
run: swift test --enable-test-discovery --sanitize=thread
31+
- name: Run tests with Thread Sanitizer
32+
timeout-minutes: 30
33+
run: swift test --enable-test-discovery --sanitize=thread

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ let package = Package(
1010
.library(name: "Fluent", targets: ["Fluent"]),
1111
],
1212
dependencies: [
13-
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.0.0"),
13+
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.12.0"),
1414
.package(url: "https://github.com/vapor/vapor.git", from: "4.39.0"),
1515
],
1616
targets: [
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import FluentKit
2+
import Vapor
3+
4+
struct RequestPaginationKey: StorageKey {
5+
typealias Value = RequestPagination
6+
}
7+
8+
struct RequestPagination {
9+
let pageSizeLimit: PageLimit?
10+
}
11+
12+
struct AppPaginationKey: StorageKey {
13+
typealias Value = AppPagination
14+
}
15+
16+
struct AppPagination {
17+
let pageSizeLimit: Int?
18+
}
19+
20+
extension Request.Fluent {
21+
public var pagination: Pagination {
22+
.init(fluent: self)
23+
}
24+
25+
public struct Pagination {
26+
let fluent: Request.Fluent
27+
}
28+
}
29+
30+
extension Request.Fluent.Pagination {
31+
/// The maximum amount of elements per page. The default is `nil`.
32+
public var pageSizeLimit: PageLimit? {
33+
get {
34+
storage[RequestPaginationKey.self]?.pageSizeLimit
35+
}
36+
nonmutating set {
37+
storage[RequestPaginationKey.self] = .init(pageSizeLimit: newValue)
38+
}
39+
}
40+
41+
var storage: Storage {
42+
get {
43+
self.fluent.request.storage
44+
}
45+
nonmutating set {
46+
self.fluent.request.storage = newValue
47+
}
48+
}
49+
}
50+
51+
extension Application.Fluent.Pagination {
52+
/// The maximum amount of elements per page. The default is `nil`.
53+
public var pageSizeLimit: Int? {
54+
get {
55+
storage[AppPaginationKey.self]?.pageSizeLimit
56+
}
57+
nonmutating set {
58+
storage[AppPaginationKey.self] = .init(pageSizeLimit: newValue)
59+
}
60+
}
61+
62+
var storage: Storage {
63+
get {
64+
self.fluent.application.storage
65+
}
66+
nonmutating set {
67+
self.fluent.application.storage = newValue
68+
}
69+
}
70+
}

Sources/Fluent/FluentProvider.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ extension Request {
1313
id,
1414
logger: self.logger,
1515
on: self.eventLoop,
16-
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil
16+
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil,
17+
pageSizeLimit: self.fluent.pagination.pageSizeLimit != nil ? self.fluent.pagination.pageSizeLimit?.value : self.application.fluent.pagination.pageSizeLimit
1718
)!
1819
}
1920

@@ -33,7 +34,8 @@ extension Application {
3334
id,
3435
logger: self.logger,
3536
on: self.eventLoopGroup.next(),
36-
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil
37+
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil,
38+
pageSizeLimit: self.fluent.pagination.pageSizeLimit
3739
)!
3840
}
3941

@@ -139,6 +141,14 @@ extension Application {
139141
public struct History {
140142
let fluent: Fluent
141143
}
144+
145+
public var pagination: Pagination {
146+
.init(fluent: self)
147+
}
148+
149+
public struct Pagination {
150+
let fluent: Fluent
151+
}
142152
}
143153

144154
public var fluent: Fluent {

Sources/Fluent/PageLimit.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
public struct PageLimit {
4+
public let value: Int?
5+
6+
public static var noLimit: PageLimit {
7+
.init(value: nil)
8+
}
9+
}
10+
11+
extension PageLimit {
12+
public init(_ value: Int) {
13+
self.value = value
14+
}
15+
}
16+
17+
extension PageLimit: ExpressibleByIntegerLiteral {
18+
public init(integerLiteral value: IntegerLiteralType) {
19+
self.value = value
20+
}
21+
}

Tests/FluentTests/PaginationTests.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,78 @@ final class PaginationTests: XCTestCase {
6464
XCTAssertEqual(todos.items.count, 1)
6565
}
6666
}
67+
68+
func testPaginationLimits() throws {
69+
let app = Application(.testing)
70+
defer { app.shutdown() }
71+
72+
let rows = [
73+
TestOutput(["id": 1, "title": "a"]),
74+
TestOutput(["id": 2, "title": "b"]),
75+
TestOutput(["id": 3, "title": "c"]),
76+
TestOutput(["id": 4, "title": "d"]),
77+
TestOutput(["id": 5, "title": "e"]),
78+
]
79+
80+
let test = CallbackTestDatabase { query in
81+
XCTAssertEqual(query.schema, "todos")
82+
let result: [TestOutput]
83+
if let limit = query.limits.first?.value, let offset = query.offsets.first?.value {
84+
result = [TestOutput](rows[min(offset, rows.count - 1)..<min(offset + limit, rows.count)])
85+
} else {
86+
result = rows
87+
}
88+
switch query.action {
89+
case .aggregate(_):
90+
return [TestOutput([.aggregate: rows.count])]
91+
default:
92+
return result
93+
}
94+
}
95+
96+
app.databases.use(test.configuration, as: .test)
97+
app.fluent.pagination.pageSizeLimit = 4
98+
99+
app.get("todos-request-limit") { req -> EventLoopFuture<Page<Todo>> in
100+
req.fluent.pagination.pageSizeLimit = 2
101+
return Todo.query(on: req.db).paginate(for: req)
102+
}
103+
104+
app.get("todos-request-no-limit") { req -> EventLoopFuture<Page<Todo>> in
105+
req.fluent.pagination.pageSizeLimit = .noLimit
106+
return Todo.query(on: req.db).paginate(for: req)
107+
}
108+
109+
app.get("todos-request-app-limit") { req -> EventLoopFuture<Page<Todo>> in
110+
req.fluent.pagination.pageSizeLimit = nil
111+
return Todo.query(on: req.db).paginate(for: req)
112+
}
113+
114+
app.get("todos-app-limit") { req -> EventLoopFuture<Page<Todo>> in
115+
Todo.query(on: req.db).paginate(for: req)
116+
}
117+
118+
try app.test(.GET, "todos-request-limit?page=1&per=5") { response in
119+
XCTAssertEqual(response.status, .ok)
120+
let todos = try response.content.decode(Page<Todo>.self)
121+
XCTAssertEqual(todos.items.count, 2, "Should be capped by request-level limit.")
122+
}
123+
.test(.GET, "todos-request-no-limit?page=1&per=5") { response in
124+
XCTAssertEqual(response.status, .ok)
125+
let todos = try response.content.decode(Page<Todo>.self)
126+
XCTAssertEqual(todos.items.count, 5, "Request-level override should suspend app-level limit.")
127+
}
128+
.test(.GET, "todos-request-app-limit?page=1&per=5") { response in
129+
XCTAssertEqual(response.status, .ok)
130+
let todos = try response.content.decode(Page<Todo>.self)
131+
XCTAssertEqual(todos.items.count, 4, "Should be capped by app-level limit.")
132+
}
133+
.test(.GET, "todos-app-limit?page=1&per=5") { response in
134+
XCTAssertEqual(response.status, .ok)
135+
let todos = try response.content.decode(Page<Todo>.self)
136+
XCTAssertEqual(todos.items.count, 4, "Should be capped by app-level limit.")
137+
}
138+
}
67139
}
68140

69141
private extension DatabaseQuery.Limit {

0 commit comments

Comments
 (0)