Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation Rules #72

Merged
merged 9 commits into from
Jul 16, 2022
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
4 changes: 2 additions & 2 deletions Documentation/guides/advanced/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ Yes, you can using the [webSocketHandler(req:)](/references/pioneer/#websocketha

#### Can I use Pioneer and GraphQl to perform file upload?

Technically yes, there is an unfficial spec that is commonly used by some server libraries called [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec). Using this, you can allow file to be processed properly in by the GraphQL executor. However, there is no package / current implementation for using that with Pioneer and [GraphQLSwift/GraphQL](https://github.com/GraphQLSwift/GraphQL).
Technically yes, there is an unofficial spec that is commonly used by some server libraries called [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec). Using this, you can allow file to be processed properly in by the GraphQL executor. However, there is no package / current implementation for using that with Pioneer and [GraphQLSwift/GraphQL](https://github.com/GraphQLSwift/GraphQL).

Furthemore, there is currently no plan to add that feature directly into Pioneer package, and there are consideration to be taken account of when trying to add file upload to a GraphQL server, which is better explained [below](#why-does-pioneer-doesnt-have-graphql-file-upload-implemented-as-feature-of-the-library)
Furthemore, there is currently no plan to add that feature directly into Pioneer package, and there are considerations to be taken account of when trying to add file upload to a GraphQL server, which is better explained [below](#why-does-pioneer-doesnt-have-graphql-file-upload-implemented-as-feature-of-the-library)

#### Why does Pioneer doesn't have `graphql-file-upload` implemented as feature of the library?

Expand Down
Binary file modified Documentation/pioneer-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git",
"state": {
"branch": null,
"revision": "283cc4de56b994a00b2724328221b7a1bc846ddc",
"version": "2.2.1"
"revision": "ebd2ea40676f8bcbdfd6088c408f3ed321c1a905",
"version": "2.4.0"
}
},
{
Expand Down Expand Up @@ -186,8 +186,8 @@
"repositoryURL": "https://github.com/vapor/vapor.git",
"state": {
"branch": null,
"revision": "12e2e7460ab912b65fb7a0fe47e4f638a7d5e642",
"version": "4.62.0"
"revision": "cd91a66c7dd2d8658c9fc43ee9d96d7a259bbc5c",
"version": "4.62.1"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "2.2.0"),
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "2.4.0"),
.package(url: "https://github.com/GraphQLSwift/DataLoader.git", from: "2.0.0"),
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.61.1")
.package(url: "https://github.com/vapor/vapor.git", from: "4.62.1")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ It can work with any GraphQL schema built with [GraphQLSwift/GraphQL](https://gi
## Feedback

If you have any feedback, feel free reach out at twitter [@d_exclaimation](https://www.twitter.com/d_exclaimation)

### Attribution

This project is heavily inspired by [Apollo Server](https://github.com/apollographql/apollo-server), and it would not have been possible without the work put into [GraphQLSwift/GraphQL](https://github.com/GraphQLSwift/GraphQL) and [Vapor](https://github.com/vapor/vapor).

<sub>Logo design inspired by <a src="https://github.com/brightdigit/MistKit">MistKit</a> and <a src="https://developer.apple.com/documentation/arkit">Apple's ARKit</a>. </sub>
3 changes: 3 additions & 0 deletions Sources/Pioneer/Extensions/Pioneer+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public extension Pioneer {
/// - websocketProtocol: Websocket sub-protocol
/// - introspection: Allowing introspection
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
init(
schema: GraphQLSchema,
Expand All @@ -26,6 +27,7 @@ public extension Pioneer {
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
) {
self.init(
Expand All @@ -43,6 +45,7 @@ public extension Pioneer {
websocketProtocol: websocketProtocol,
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
)
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/Pioneer/Extensions/Pioneer+Graphiti.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public extension Pioneer {
/// - websocketProtocol: Websocket sub-protocol
/// - introspection: Allowing introspection
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
init(
schema: Schema<Resolver, Context>,
Expand All @@ -27,6 +28,7 @@ public extension Pioneer {
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
) {
self.init(
Expand All @@ -44,6 +46,7 @@ public extension Pioneer {
websocketProtocol: websocketProtocol,
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
)
}
Expand All @@ -57,6 +60,7 @@ public extension Pioneer {
/// - websocketProtocol: Websocket sub-protocol
/// - introspection: Allowing introspection
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
init(
schema: Schema<Resolver, Context>,
Expand All @@ -67,6 +71,7 @@ public extension Pioneer {
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
) {
self.init(
Expand All @@ -78,6 +83,7 @@ public extension Pioneer {
websocketProtocol: websocketProtocol,
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
)
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Pioneer/Extensions/Pioneer+RequestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public extension Pioneer where Context == Void {
/// - websocketProtocol: Websocket sub-protocol
/// - introspection: Allowing introspection
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
init(
schema: Schema<Resolver, Void>,
Expand All @@ -23,6 +24,7 @@ public extension Pioneer where Context == Void {
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
) {
self.init(
Expand All @@ -34,6 +36,7 @@ public extension Pioneer where Context == Void {
websocketProtocol: websocketProtocol,
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
)
}
Expand Down
36 changes: 33 additions & 3 deletions Sources/Pioneer/GraphQL/GraphQLRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,51 @@ import GraphQL

/// GraphQL Request according to the spec
public struct GraphQLRequest: Codable {
private enum Key: String, CodingKey {
case query, operationName, variables
}

/// Query string
public var query: String
/// Name of the operation being ran if there are more than one included in this query.
public var operationName: String?
/// Variables seperated and assign to constant in the query
public var variables: [String: Map]?

/// Parsed GraphQL Document from request
public var ast: Document?

/// Getter a GraphQL AST Source from query
public var source: Source {
.init(body: query)
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
let query = try container.decode(String.self, forKey: .query)
let operationName = try? container.decodeIfPresent(String.self, forKey: .operationName)
let variables = try? container.decodeIfPresent([String: Map].self, forKey: .variables)
self.init(query: query, operationName: operationName, variables: variables)
}


public init(query: String, operationName: String? = nil, variables: [String: Map]? = nil) {
self.query = query
self.operationName = operationName
self.variables = variables
self.ast = try? parse(source: .init(body: query))
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
try container.encode(query, forKey: .query)
try container.encodeIfPresent(operationName, forKey: .operationName)
try container.encodeIfPresent(variables, forKey: .variables)
}

/// Getting parsed operationType
public func operationType() throws -> OperationType? {
let ast = try parse(source: source)
public var operationType: OperationType? {
guard let ast = ast else { return nil }
let operations = ast.definitions
.compactMap { def -> OperationDefinition? in
def as? OperationDefinition
Expand All @@ -44,7 +74,7 @@ public struct GraphQLRequest: Codable {

/// Check if query is any type of introspection
public var isIntrospection: Bool {
guard let ast = try? parse(source: source) else { return false }
guard let ast = ast else { return false }
return ast.definitions.contains { def in
guard let operation = def as? OperationDefinition else { return false }
return operation.selectionSet.selections.contains { select in
Expand Down
5 changes: 5 additions & 0 deletions Sources/Pioneer/Http/Pioneer+Http.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ extension Pioneer {
return try GraphQLError(message: "Operation of this type is not allowed and has been blocked")
.response(with: .badRequest)
}
let errors = validationRules(using: schema, for: gql)
guard errors.isEmpty else {
return try errors.response(with: .badRequest)
}

let res = Response()
do {
let context = try await contextBuilder(req, res)
Expand Down
13 changes: 7 additions & 6 deletions Sources/Pioneer/Pioneer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public struct Pioneer<Resolver, Context> {
public private(set) var introspection: Bool
/// Allowing GraphQL IDE
public private(set) var playground: IDE
/// Validation rules
public private(set) var validationRules: Validations
/// Keep alive period
public private(set) var keepAlive: UInt64?

Expand All @@ -44,6 +46,7 @@ public struct Pioneer<Resolver, Context> {
/// - websocketProtocol: Websocket sub-protocol
/// - introspection: Allowing introspection
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
public init(
schema: GraphQLSchema,
Expand All @@ -54,6 +57,7 @@ public struct Pioneer<Resolver, Context> {
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
) {
self.schema = schema
Expand All @@ -64,6 +68,7 @@ public struct Pioneer<Resolver, Context> {
self.websocketProtocol = websocketProtocol
self.introspection = introspection
self.playground = !introspection ? .disable : playground
self.validationRules = validationRules
self.keepAlive = keepAlive


Expand Down Expand Up @@ -147,13 +152,9 @@ public struct Pioneer<Resolver, Context> {
guard introspection || !gql.isIntrospection else {
return false
}
do {
guard let operationType = try gql.operationType() else {
return false
}
return allowing.contains(operationType)
} catch {
guard let operationType = gql.operationType else {
return false
}
return allowing.contains(operationType)
}
}
5 changes: 5 additions & 0 deletions Sources/Pioneer/Utils/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ extension Pioneer {
let introspection: Bool
/// Allowing GraphQL IDE
let playground: Pioneer<Resolver, Context>.IDE
/// Validation rules
let validationRules: Validations
/// Keep alive period
let keepAlive: UInt64?

Expand All @@ -40,6 +42,7 @@ extension Pioneer {
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
) {
self.schema = schema
Expand All @@ -50,6 +53,7 @@ extension Pioneer {
self.websocketProtocol = websocketProtocol
self.introspection = introspection
self.playground = !introspection ? .disable : playground
self.validationRules = validationRules
self.keepAlive = keepAlive
}
}
Expand All @@ -66,6 +70,7 @@ public extension Pioneer {
websocketProtocol: config.websocketProtocol,
introspection: config.introspection,
playground: config.playground,
validationRules: config.validationRules,
keepAlive: config.keepAlive
)
}
Expand Down
13 changes: 11 additions & 2 deletions Sources/Pioneer/Utils/Configurations/Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ public extension Pioneer.Config {
/// - resolver: The top level object
/// - context: The context builder for HTTP
/// - websocketContext: The context builder for WebSocket
/// - validationRules: Validation rules to be applied for every operations
/// - introspection: Allowing introspection
static func `default`(
using schema: GraphQLSchema,
resolver: Resolver,
context: @escaping @Sendable (Request, Response) async throws -> Context,
websocketContext: @escaping @Sendable (Request, ConnectionParams, GraphQLRequest) async throws -> Context,
validationRules: Pioneer<Resolver, Context>.Validations = .none,
introspection: Bool = true
) -> Self {
.init(
Expand All @@ -32,7 +34,8 @@ public extension Pioneer.Config {
websocketContextBuilder: websocketContext,
websocketProtocol: .graphqlWs,
introspection: introspection,
playground: .apolloSandbox
playground: .apolloSandbox,
validationRules: validationRules
)
}

Expand All @@ -42,11 +45,13 @@ public extension Pioneer.Config {
/// - schema: The GraphQL schema from GraphQLSwift/GraphQL
/// - resolver: The top level object
/// - context: The shared context builder
/// - validationRules: Validation rules to be applied for every operations
/// - introspection: Allowing introspection
static func `default`(
using schema: GraphQLSchema,
resolver: Resolver,
context: @escaping @Sendable (Request, Response) async throws -> Context,
validationRules: Pioneer<Resolver, Context>.Validations = .none,
introspection: Bool = true
) -> Self {
.init(
Expand All @@ -57,25 +62,29 @@ public extension Pioneer.Config {
websocketContextBuilder: { try await $0.defaultWebsocketContextBuilder(payload: $1, gql: $2, contextBuilder: context) },
websocketProtocol: .graphqlWs,
introspection: introspection,
playground: .apolloSandbox
playground: .apolloSandbox,
validationRules: validationRules
)
}

/// Default configuration for Pioneer
/// - Parameters:
/// - schema: The GraphQL schema from GraphQLSwift/GraphQL
/// - resolver: The top level object
/// - validationRules: Validation rules to be applied for every operations
/// - introspection: Allowing introspection
static func `default`(
using schema: GraphQLSchema,
resolver: Resolver,
validationRules: Pioneer<Resolver, Context>.Validations = .none,
introspection: Bool = true
) -> Self where Context == Void {
.default(
using: schema,
resolver: resolver,
context: { _, _ in },
websocketContext: { _, _, _ in },
validationRules: validationRules,
introspection: introspection
)
}
Expand Down
Loading