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
7 changes: 7 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 1,
"indentation" : {
"spaces" : 4
},
"lineBreakBeforeEachArgument": true
}
4 changes: 2 additions & 2 deletions Examples/HelloWorld/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "HelloWorld",
platforms: [
.macOS(.v13),
.macOS(.v13)
],
dependencies: [
.package(name: "graphql-vapor", path: "../../"),
Expand All @@ -22,6 +22,6 @@ let package = Package(
.product(name: "GraphQL", package: "GraphQL"),
.product(name: "Vapor", package: "vapor"),
]
),
)
]
)
9 changes: 6 additions & 3 deletions Examples/HelloWorld/Sources/HelloWorld/HelloWorld.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct HelloWorld {
resolve: { _, _, _, _ in
"World"
}
),
)
]
),
subscription: GraphQLObjectType(
Expand All @@ -34,11 +34,14 @@ struct HelloWorld {
subscribe: { _, _, _, _ in
let clock = ContinuousClock()
let start = clock.now
return AsyncTimerSequence(interval: .seconds(3), clock: ContinuousClock()).map { instant in
return AsyncTimerSequence(
interval: .seconds(3),
clock: ContinuousClock()
).map { instant in
"World at \(start.duration(to: instant))"
}
}
),
)
]
)
)
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import PackageDescription
let package = Package(
name: "GraphQLVapor",
platforms: [
.macOS(.v13),
.macOS(.v13)
],
products: [
.library(
name: "GraphQLVapor",
targets: ["GraphQLVapor"]
),
)
],
dependencies: [
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.0.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@ import GraphQL
import Vapor

extension GraphQLJSONEncoder: @retroactive ContentEncoder {
public func encode<E: Encodable>(_ encodable: E, to body: inout NIOCore.ByteBuffer, headers: inout NIOHTTP1.HTTPHeaders) throws {
public func encode<E: Encodable>(
_ encodable: E,
to body: inout NIOCore.ByteBuffer,
headers: inout NIOHTTP1.HTTPHeaders
) throws {
try encode(encodable, to: &body, headers: &headers, userInfo: [:])
}

public func encode<E: Encodable>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws {
public func encode<E: Encodable>(
_ encodable: E,
to body: inout ByteBuffer,
headers: inout HTTPHeaders,
userInfo: [CodingUserInfoKey: Sendable]
) throws {
headers.contentType = .jsonGraphQL

if !userInfo.isEmpty { // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy
if !userInfo.isEmpty { // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy
let encoder = GraphQLJSONEncoder.custom(
dates: dateEncodingStrategy,
data: dataEncodingStrategy,
keys: keyEncodingStrategy,
format: outputFormatting,
floats: nonConformingFloatEncodingStrategy
) // don't use userInfo parameter of `JSONEncoder.custom()` until Swift 6.2 is required
) // don't use userInfo parameter of `JSONEncoder.custom()` until Swift 6.2 is required
encoder.userInfo = self.userInfo.merging(userInfo) { $1 }
try body.writeBytes(encoder.encode(encodable))
} else {
Expand All @@ -25,7 +34,7 @@ extension GraphQLJSONEncoder: @retroactive ContentEncoder {
}
}

public extension GraphQLJSONEncoder {
extension GraphQLJSONEncoder {
/// Convenience for creating a customized ``Foundation/GraphQLJSONEncoder``.
///
/// let encoder: GraphQLJSONEncoder = .custom(dates: .millisecondsSince1970)
Expand All @@ -38,7 +47,7 @@ public extension GraphQLJSONEncoder {
/// - floats: Non-conforming float encoding strategy.
/// - userInfo: Coder userInfo.
/// - Returns: Newly created ``Foundation/JSONEncoder``.
static func custom(
public static func custom(
dates dateStrategy: GraphQLJSONEncoder.DateEncodingStrategy? = nil,
data dataStrategy: GraphQLJSONEncoder.DataEncodingStrategy? = nil,
keys keyStrategy: GraphQLJSONEncoder.KeyEncodingStrategy? = nil,
Expand Down
8 changes: 6 additions & 2 deletions Sources/GraphQLVapor/Content/HTTPMediaType+jsonGraphQL.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Vapor

public extension HTTPMediaType {
static let jsonGraphQL = HTTPMediaType(type: "application", subType: "graphql-response+json", parameters: ["charset": "utf-8"])
extension HTTPMediaType {
public static let jsonGraphQL = HTTPMediaType(
type: "application",
subType: "graphql-response+json",
parameters: ["charset": "utf-8"]
)
}
5 changes: 4 additions & 1 deletion Sources/GraphQLVapor/GraphQLConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ public struct GraphQLConfig<
/// authorization using the `payload` field of the `connection_init` message.
/// Throw from this closure to indicate that authorization has failed.
public init(
onWebSocketInit: @Sendable @escaping (WebSocketInit, Request) async throws -> WebSocketInitResult = { (_: EmptyWebSocketInit, _: Request) in }
onWebSocketInit:
@Sendable @escaping (WebSocketInit, Request) async throws -> WebSocketInitResult = {
(_: EmptyWebSocketInit, _: Request) in
}
) {
self.onWebSocketInit = onWebSocketInit
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/GraphQLVapor/GraphQLHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ struct GraphQLHandler<
let schema: GraphQLSchema
let rootValue: any Sendable
let config: GraphQLConfig<WebSocketInit, WebSocketInitResult>
let computeContext: @Sendable (GraphQLContextComputationInputs<WebSocketInitResult>) async throws -> Context
let computeContext:
@Sendable (GraphQLContextComputationInputs<WebSocketInitResult>) async throws -> Context
}
5 changes: 4 additions & 1 deletion Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ extension GraphQLHandler {
// This indicates a request parsing error
return GraphQLResult(data: nil, errors: [error])
} catch {
return GraphQLResult(data: nil, errors: [GraphQLError(message: error.localizedDescription)])
return GraphQLResult(
data: nil,
errors: [GraphQLError(message: error.localizedDescription)]
)
}
return result
}
Expand Down
152 changes: 76 additions & 76 deletions Sources/GraphQLVapor/IDEs/GraphiQLHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,89 +14,89 @@ enum GraphiQLHandler {
graphiQLFetcherArgs += ", subscriptionUrl: '\(subscriptionUrl)'"
}
return """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GraphiQL 5 with React 19 and GraphiQL Explorer</title>
<style>
body {
margin: 0;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GraphiQL 5 with React 19 and GraphiQL Explorer</title>
<style>
body {
margin: 0;
}

#graphiql {
height: 100dvh;
}
#graphiql {
height: 100dvh;
}

.loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
}
</style>
<link rel="stylesheet" href="https://esm.sh/graphiql/dist/style.css" />
<link
rel="stylesheet"
href="https://esm.sh/@graphiql/plugin-explorer/dist/style.css"
/>
<!--
* Note:
* The ?standalone flag bundles the module along with all of its `dependencies`, excluding `peerDependencies`, into a single JavaScript file.
* `@emotion/is-prop-valid` is a shim to remove the console error ` module "@emotion /is-prop-valid" not found`. Upstream issue: https://github.com/motiondivision/motion/issues/3126
-->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.1.0",
"react/": "https://esm.sh/react@19.1.0/",
.loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
}
</style>
<link rel="stylesheet" href="https://esm.sh/graphiql/dist/style.css" />
<link
rel="stylesheet"
href="https://esm.sh/@graphiql/plugin-explorer/dist/style.css"
/>
<!--
* Note:
* The ?standalone flag bundles the module along with all of its `dependencies`, excluding `peerDependencies`, into a single JavaScript file.
* `@emotion/is-prop-valid` is a shim to remove the console error ` module "@emotion /is-prop-valid" not found`. Upstream issue: https://github.com/motiondivision/motion/issues/3126
-->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.1.0",
"react/": "https://esm.sh/react@19.1.0/",

"react-dom": "https://esm.sh/react-dom@19.1.0",
"react-dom/": "https://esm.sh/react-dom@19.1.0/",
"react-dom": "https://esm.sh/react-dom@19.1.0",
"react-dom/": "https://esm.sh/react-dom@19.1.0/",

"graphiql": "https://esm.sh/graphiql?standalone&external=react,react-dom,@graphiql/react,graphql",
"graphiql/": "https://esm.sh/graphiql/",
"@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer?standalone&external=react,@graphiql/react,graphql",
"@graphiql/react": "https://esm.sh/@graphiql/react?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
"graphiql": "https://esm.sh/graphiql?standalone&external=react,react-dom,@graphiql/react,graphql",
"graphiql/": "https://esm.sh/graphiql/",
"@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer?standalone&external=react,@graphiql/react,graphql",
"@graphiql/react": "https://esm.sh/@graphiql/react?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",

"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit?standalone&external=graphql",
"graphql": "https://esm.sh/graphql@16.11.0",
"@emotion/is-prop-valid": "data:text/javascript,"
}
}
</script>
<script type="module">
import React from 'react';
import ReactDOM from 'react-dom/client';
import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import 'graphiql/setup-workers/esm.sh';
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit?standalone&external=graphql",
"graphql": "https://esm.sh/graphql@16.11.0",
"@emotion/is-prop-valid": "data:text/javascript,"
}
}
</script>
<script type="module">
import React from 'react';
import ReactDOM from 'react-dom/client';
import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import 'graphiql/setup-workers/esm.sh';

const fetcher = createGraphiQLFetcher({\(graphiQLFetcherArgs)});
const plugins = [HISTORY_PLUGIN, explorerPlugin()];
const fetcher = createGraphiQLFetcher({\(graphiQLFetcherArgs)});
const plugins = [HISTORY_PLUGIN, explorerPlugin()];

function App() {
return React.createElement(GraphiQL, {
fetcher,
plugins,
defaultEditorToolsVisibility: true,
});
}
function App() {
return React.createElement(GraphiQL, {
fetcher,
plugins,
defaultEditorToolsVisibility: true,
});
}

const container = document.getElementById('graphiql');
const root = ReactDOM.createRoot(container);
root.render(React.createElement(App));
</script>
</head>
<body>
<div id="graphiql">
<div class="loading">Loading…</div>
</div>
</body>
</html>
"""
const container = document.getElementById('graphiql');
const root = ReactDOM.createRoot(container);
root.render(React.createElement(App));
</script>
</head>
<body>
<div id="graphiql">
<div class="loading">Loading…</div>
</div>
</body>
</html>
"""
}
}
25 changes: 17 additions & 8 deletions Sources/GraphQLVapor/RoutesBuilder+graphql.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import GraphQL
import Vapor

public extension RoutesBuilder {
extension RoutesBuilder {
/// Registers graphql routes that respond using the provided schema.
///
/// The resulting routes adhere to the [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md).
Expand All @@ -18,26 +18,34 @@ public extension RoutesBuilder {
/// - rootValue: The `rootValue` GraphQL execution arg. This is the object passed to the root resolvers.
/// - config: GraphQL Handler configuration options. See type documentation for details.
/// - computeContext: A closure used to compute the GraphQL context from incoming requests. This must be provided.
func graphql<
public func graphql<
Context: Sendable,
WebSocketInit: Equatable & Codable & Sendable,
WebSocketInitResult: Sendable
>(
_ path: [PathComponent] = ["graphql"],
schema: GraphQLSchema,
rootValue: any Sendable = (),
config: GraphQLConfig<WebSocketInit, WebSocketInitResult> = GraphQLConfig<EmptyWebSocketInit, Void>(),
computeContext: @Sendable @escaping (GraphQLContextComputationInputs<WebSocketInitResult>) async throws -> Context
config: GraphQLConfig<WebSocketInit, WebSocketInitResult> = GraphQLConfig<
EmptyWebSocketInit, Void
>(),
computeContext:
@Sendable @escaping (GraphQLContextComputationInputs<WebSocketInitResult>) async throws
-> Context
) {
ContentConfiguration.global.use(encoder: GraphQLJSONEncoder(), for: .jsonGraphQL)
ContentConfiguration.global.use(decoder: JSONDecoder(), for: .jsonGraphQL)

// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request
let handler = GraphQLHandler<Context, WebSocketInit, WebSocketInitResult>(schema: schema, rootValue: rootValue, config: config, computeContext: computeContext)
let handler = GraphQLHandler<Context, WebSocketInit, WebSocketInitResult>(
schema: schema,
rootValue: rootValue,
config: config,
computeContext: computeContext
)
get(path) { request in
// WebSocket handling
if
config.subscriptionProtocols.contains(.websocket),
if config.subscriptionProtocols.contains(.websocket),
request.headers.connection?.value.lowercased() == "upgrade"
{
return try await handler.handleWebSocket(request: request)
Expand All @@ -49,7 +57,8 @@ public extension RoutesBuilder {
case .graphiql:
return try await GraphiQLHandler.respond(
url: request.url.string,
subscriptionUrl: config.subscriptionProtocols.contains(.websocket) ? request.url.string : nil
subscriptionUrl: config.subscriptionProtocols.contains(.websocket)
? request.url.string : nil
)
case .none:
// Let this get caught by the graphQLRequest decoding
Expand Down
Loading
Loading