Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 25 additions & 37 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,33 @@
name: test
on:
pull_request:
push:
branches:
- master
on: { pull_request: {} }

jobs:
linux:
getcidata:
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.output.outputs.environments }}
steps:
- id: output
run: |
envblob="$(curl -fsSL https://raw.githubusercontent.com/vapor/ci/main/pr-environments.json | jq -cMj '.')"
echo "::set-output name=environments::${envblob}"

test-fluent:
needs: getcidata
strategy:
fail-fast: false
matrix:
image:
- swift:5.2-xenial
- swift:5.2-bionic
- swiftlang/swift:nightly-5.2-xenial
- swiftlang/swift:nightly-5.2-bionic
- swiftlang/swift:nightly-5.3-xenial
- swiftlang/swift:nightly-5.3-bionic
- swiftlang/swift:nightly-master-xenial
- swiftlang/swift:nightly-master-bionic
- swiftlang/swift:nightly-master-focal
- swiftlang/swift:nightly-master-centos8
- swiftlang/swift:nightly-master-amazonlinux2
container: ${{ matrix.image }}
env:
LOG_LEVEL: info
steps:
- name: Checkout Fluent
uses: actions/checkout@v2
- name: Run base tests with Thread Sanitizer
run: swift test --enable-test-discovery --sanitize=thread
macOS:
env:
LOG_LEVEL: info
runs-on: macos-latest
steps:
- name: Select latest available Xcode
uses: maxim-lobanov/[email protected]
env: ${{ fromJSON(needs.getcidata.outputs.environments) }}
runs-on: ${{ matrix.env.os }}
container: ${{ matrix.env.image }}
steps:
- name: Select toolchain
uses: maxim-lobanov/[email protected]
with:
xcode-version: latest
- name: Checkout Fluent
xcode-version: ${{ matrix.env.toolchain }}
if: ${{ matrix.env.toolchain != '' }}
- name: Check out Vapor
uses: actions/checkout@v2
- name: Run base Fluent tests with Thread Sanitizer
run: swift test --enable-test-discovery --sanitize=thread
- name: Run tests with Thread Sanitizer
timeout-minutes: 30
run: swift test --enable-test-discovery --sanitize=thread
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ let package = Package(
.library(name: "Fluent", targets: ["Fluent"]),
],
dependencies: [
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.12.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.39.0"),
],
targets: [
Expand Down
70 changes: 70 additions & 0 deletions Sources/Fluent/Fluent+Pagination.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import FluentKit
import Vapor

struct RequestPaginationKey: StorageKey {
typealias Value = RequestPagination
}

struct RequestPagination {
let pageSizeLimit: PageLimit?
}

struct AppPaginationKey: StorageKey {
typealias Value = AppPagination
}

struct AppPagination {
let pageSizeLimit: Int?
}

extension Request.Fluent {
public var pagination: Pagination {
.init(fluent: self)
}

public struct Pagination {
let fluent: Request.Fluent
}
}

extension Request.Fluent.Pagination {
/// The maximum amount of elements per page. The default is `nil`.
public var pageSizeLimit: PageLimit? {
get {
storage[RequestPaginationKey.self]?.pageSizeLimit
}
nonmutating set {
storage[RequestPaginationKey.self] = .init(pageSizeLimit: newValue)
}
}

var storage: Storage {
get {
self.fluent.request.storage
}
nonmutating set {
self.fluent.request.storage = newValue
}
}
}

extension Application.Fluent.Pagination {
/// The maximum amount of elements per page. The default is `nil`.
public var pageSizeLimit: Int? {
get {
storage[AppPaginationKey.self]?.pageSizeLimit
}
nonmutating set {
storage[AppPaginationKey.self] = .init(pageSizeLimit: newValue)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gwynne do we need to wrap this in a lock?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a global setting, set once during startup, I'd avoid diving into that too much.

}
}

var storage: Storage {
get {
self.fluent.application.storage
}
nonmutating set {
self.fluent.application.storage = newValue
}
}
}
14 changes: 12 additions & 2 deletions Sources/Fluent/FluentProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ extension Request {
id,
logger: self.logger,
on: self.eventLoop,
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil,
pageSizeLimit: self.fluent.pagination.pageSizeLimit != nil ? self.fluent.pagination.pageSizeLimit?.value : self.application.fluent.pagination.pageSizeLimit
)!
}

Expand All @@ -33,7 +34,8 @@ extension Application {
id,
logger: self.logger,
on: self.eventLoopGroup.next(),
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil
history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil,
pageSizeLimit: self.fluent.pagination.pageSizeLimit
)!
}

Expand Down Expand Up @@ -139,6 +141,14 @@ extension Application {
public struct History {
let fluent: Fluent
}

public var pagination: Pagination {
.init(fluent: self)
}

public struct Pagination {
let fluent: Fluent
}
}

public var fluent: Fluent {
Expand Down
21 changes: 21 additions & 0 deletions Sources/Fluent/PageLimit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

public struct PageLimit {
public let value: Int?

public static var noLimit: PageLimit {
.init(value: nil)
}
}

extension PageLimit {
public init(_ value: Int) {
self.value = value
}
}

extension PageLimit: ExpressibleByIntegerLiteral {
public init(integerLiteral value: IntegerLiteralType) {
self.value = value
}
}
72 changes: 72 additions & 0 deletions Tests/FluentTests/PaginationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,78 @@ final class PaginationTests: XCTestCase {
XCTAssertEqual(todos.items.count, 1)
}
}

func testPaginationLimits() throws {
let app = Application(.testing)
defer { app.shutdown() }

let rows = [
TestOutput(["id": 1, "title": "a"]),
TestOutput(["id": 2, "title": "b"]),
TestOutput(["id": 3, "title": "c"]),
TestOutput(["id": 4, "title": "d"]),
TestOutput(["id": 5, "title": "e"]),
]

let test = CallbackTestDatabase { query in
XCTAssertEqual(query.schema, "todos")
let result: [TestOutput]
if let limit = query.limits.first?.value, let offset = query.offsets.first?.value {
result = [TestOutput](rows[min(offset, rows.count - 1)..<min(offset + limit, rows.count)])
} else {
result = rows
}
switch query.action {
case .aggregate(_):
return [TestOutput([.aggregate: rows.count])]
default:
return result
}
}

app.databases.use(test.configuration, as: .test)
app.fluent.pagination.pageSizeLimit = 4

app.get("todos-request-limit") { req -> EventLoopFuture<Page<Todo>> in
req.fluent.pagination.pageSizeLimit = 2
return Todo.query(on: req.db).paginate(for: req)
}

app.get("todos-request-no-limit") { req -> EventLoopFuture<Page<Todo>> in
req.fluent.pagination.pageSizeLimit = .noLimit
return Todo.query(on: req.db).paginate(for: req)
}

app.get("todos-request-app-limit") { req -> EventLoopFuture<Page<Todo>> in
req.fluent.pagination.pageSizeLimit = nil
return Todo.query(on: req.db).paginate(for: req)
}

app.get("todos-app-limit") { req -> EventLoopFuture<Page<Todo>> in
Todo.query(on: req.db).paginate(for: req)
}

try app.test(.GET, "todos-request-limit?page=1&per=5") { response in
XCTAssertEqual(response.status, .ok)
let todos = try response.content.decode(Page<Todo>.self)
XCTAssertEqual(todos.items.count, 2, "Should be capped by request-level limit.")
}
.test(.GET, "todos-request-no-limit?page=1&per=5") { response in
XCTAssertEqual(response.status, .ok)
let todos = try response.content.decode(Page<Todo>.self)
XCTAssertEqual(todos.items.count, 5, "Request-level override should suspend app-level limit.")
}
.test(.GET, "todos-request-app-limit?page=1&per=5") { response in
XCTAssertEqual(response.status, .ok)
let todos = try response.content.decode(Page<Todo>.self)
XCTAssertEqual(todos.items.count, 4, "Should be capped by app-level limit.")
}
.test(.GET, "todos-app-limit?page=1&per=5") { response in
XCTAssertEqual(response.status, .ok)
let todos = try response.content.decode(Page<Todo>.self)
XCTAssertEqual(todos.items.count, 4, "Should be capped by app-level limit.")
}
}
}

private extension DatabaseQuery.Limit {
Expand Down