Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
│ ├── FSharp.Data.GraphQL.Server.AspNetCore/ – ASP.NET Core integration (HTTP and WebSocket)
│ ├── FSharp.Data.GraphQL.Server.Giraffe/ – Giraffe integration
│ ├── FSharp.Data.GraphQL.Server.Oxpecker/ – Oxpecker integration
│ ├── FSharp.Data.GraphQL.Server.Suave/ – Suave integration
│ ├── FSharp.Data.GraphQL.Client/ – client type provider runtime
│ └── FSharp.Data.GraphQL.Client.DesignTime/ – client type provider design-time component
├── tests/
Expand Down
2 changes: 2 additions & 0 deletions FSharp.Data.GraphQL.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
</Folder>
<Folder Name="/samples/">
<Project Path="samples/chat-app/server/chat-app.fsproj" />
<Project Path="samples/magic-eight-ball/magic-eight-ball.fsproj" />
<Project Path="samples/relay-book-store/relay-book-store.fsproj" />
<Project Path="samples/star-wars-api/star-wars-api.fsproj" />
</Folder>
Expand Down Expand Up @@ -123,6 +124,7 @@
<Project Path="src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj" />
<Project Path="src/FSharp.Data.GraphQL.Server.Oxpecker/FSharp.Data.GraphQL.Server.Oxpecker.fsproj" />
<Project Path="src/FSharp.Data.GraphQL.Server.Relay/FSharp.Data.GraphQL.Server.Relay.fsproj" />
<Project Path="src/FSharp.Data.GraphQL.Server.Suave/FSharp.Data.GraphQL.Server.Suave.fsproj" />
<Project Path="src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj" />
<Project Path="src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj" />
</Folder>
Expand Down
1 change: 1 addition & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<PackageReference Update="Microsoft.NETCore.Platforms" Version="$(SystemVersion)" />
<PackageReference Update="NuGet.CommandLine" Version="1.*" />
<PackageReference Update="Oxpecker" Version="2.*" />
<PackageReference Update="Suave" Version="2.6.*" />
<PackageReference Update="System.CodeDom" Version="$(SystemVersion)" />
<PackageReference Update="System.Collections.Immutable" Version="$(SystemVersion)" />
<PackageReference Update="System.Diagnostics.DiagnosticSource" Version="$(SystemVersion)" />
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,25 +279,27 @@ type OperationExecutionMiddleware =
ExecutionContext -> (ExecutionContext -> AsyncVal<GQLResponse>) -> AsyncVal<GQLResponse>

type IExecutorMiddleware =
abstract CompileSchema : SchemaCompileMiddleware option
abstract PlanOperation : OperationPlanningMiddleware option
abstract ExecuteOperationAsync : OperationExecutionMiddleware option
abstract CompileSchema : SchemaCompileMiddleware voption
abstract PostCompileSchema : SchemaPostCompileMiddleware voption
abstract PlanOperation : OperationPlanningMiddleware voption
abstract ExecuteOperationAsync : OperationExecutionMiddleware voption
```

Optionally, for ease of implementation, concrete class to derive from can be used, receiving only the optional sub-middleware functions in the constructor:

```fsharp
type ExecutorMiddleware(?compile, ?plan, ?execute) =
type ExecutorMiddleware([<Struct>] ?compile, [<Struct>] ?postCompile, [<Struct>] ?plan, [<Struct>] ?execute) =
interface IExecutorMiddleware with
member _.CompileSchema = compile
member _.PostCompileSchema = postCompile
member _.PlanOperation = plan
member _.ExecuteOperationAsync = execute
```

Each of the middleware functions act like an intercept function, with two parameters: the context of the phase, the function of the next middleware (or the actual phase itself, which is the last to run), and the return value. Those functions can be passed as an argument to the constructor of the `Executor<'Root>` object:

```fsharp
let middleware = [ ExecutorMiddleware(compileFn, planningFn, executionFn) ]
let middleware = [ ExecutorMiddleware(compile = compileFn, plan = planningFn, execute = executionFn) ]
let executor = Executor(schema, middleware)
```

Expand Down
303 changes: 303 additions & 0 deletions samples/magic-eight-ball/GraphiQL.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
module FSharp.Data.GraphQL.Samples.MagicEightBall.GraphiQL

open Suave
open Suave.Http
open Suave.Operators

// `GraphQL.Server.Ui.GraphiQL` (used by the other samples) only ships an ASP.NET Core middleware, so it can't be
// composed into a Suave `WebPart`. Its `GraphiQLPageModel.Render()` method produces static HTML for a fixed set of
// options (GraphQL endpoint `/`, subscriptions endpoint `/ws`, `graphql-ws` subscriptions), so that output is
// hardcoded here instead of invoking the internal renderer through reflection.
let private page =
"""<!--
* Copyright (c) 2024 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}

#graphiql {
height: 100vh;
}
</style>

<!--
This GraphiQL example depends on Promise and fetch, which are available in
modern browsers, but can be "polyfilled" for older browsers.
GraphiQL itself depends on React DOM.
If you do not want to rely on a CDN, you can host these files locally or
include them directly in your favored resource bunder.
-->
<script
src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
integrity="sha384-dcF7KoWRaRpjcNbVPUFgatYgAijf8DqW6NWuqLdfB5Sb4Cdbb8iHX7bHsl9YhpKa"
crossorigin="anonymous"
></script>
<script
src="https://unpkg.com/react@16.14.0/umd/react.production.min.js"
integrity="sha384-N7y5SSAooNlIfb9H750GR82ufkn1JXJFaCjg8pmt+OZuKcZoTvTGfog4d4taG/cF"
crossorigin="anonymous"
></script>
<script
src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.production.min.js"
integrity="sha384-j7WmMv3OO6n8pZRATOsaMVEdZcHpoaTBIika/l92YM2AkEex72QunlTQlgmu+pI8"
crossorigin="anonymous"
></script>

<!--
These two files can be found in the npm module, however you may wish to
copy them directly into your environment, or perhaps include them in your
favored resource bundler.
-->
<link
rel="stylesheet"
href="https://unpkg.com/graphiql@3.2.0/graphiql.min.css"
integrity="sha384-yz3/sqpuplkA7msMo0FE4ekg0xdwdvZ8JX9MVZREsxipqjU4h8IRfmAMRcb1QpUy"
crossorigin="anonymous"
/>
<script
src="https://unpkg.com/subscriptions-transport-ws@0.8.2/browser/client.js"
integrity="sha384-Eqe2SG8kA+Au+rwrgfWJ+epqYAtKGW/As+WdcywebVKX7377xelWa+/il4CHiHXI"
crossorigin="anonymous"
></script>
<script
src="https://unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"
integrity="sha384-ArTEHLNWIe9TuoDpFEtD/NeztNdWn3SdmWwMiAuZaSJeOaYypEGzeQoBxuPO+ORM"
crossorigin="anonymous"
></script>
<script
src="https://unpkg.com/graphql-ws@5.16.0/umd/graphql-ws.min.js"
integrity="sha384-oEPbisbEBMo7iCrbQcKx244HXUjGnF1jyS8hkVZ3oCwnw9c9oLfY70c1RKeKj3+i"
crossorigin="anonymous"
></script>

</head>
<body>
<div id="graphiql">Loading...</div>
<script
src="https://unpkg.com/graphiql@3.2.0/graphiql.min.js"
type="application/javascript"
integrity="sha384-h7T8iQ4OYtRS2RIWfZ7ahqfNRtVrjc6Uz/gUJjydAV/uWn/HgC5GMnSKmsvNGeUH"
crossorigin="anonymous"
></script>
<script>

/**
* This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/

// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});

// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}

// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}

function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}

function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}

function updateURL() {
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
return Boolean(parameters[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}

// Defines a GraphQL fetcher using the fetch API. You're not required to
// use fetch, and could instead implement graphQLFetcher however you like,
// as long as it returns a Promise or Observable.
function graphQLFetcher(graphQLParams, fetcherOpts = { headers: {} }) {
// This example expects a GraphQL server at the path /graphql.
// Change this to point wherever you host your GraphQL server.
return fetch('/', {
method: 'post',
headers: Object.assign({"Accept":"application/json","Content-Type":"application/json"}, fetcherOpts.headers),
body: JSON.stringify(graphQLParams),
credentials: 'same-origin',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}

function getSubscriptionsEndPoint() {
let subscriptionsEndPoint = "/ws";
if (/^(?:[a-z]+:)?\/\//i.test(subscriptionsEndPoint)) {
// if location includes protocol (e.g. "wss://") then return exact string
return subscriptionsEndPoint;
} else if (subscriptionsEndPoint[0] != '/') {
// if location is relative (e.g. "api") then prepend host and current path
let currentUrl = /^[^?]*(?=\/)/.exec(window.location.pathname);
currentUrl = currentUrl ? currentUrl[0] : '';
while (subscriptionsEndPoint.substring(0, 3) == '../') {
subscriptionsEndPoint = subscriptionsEndPoint.substring(3);
currentUrl = /^[^?]*(?=\/)/.exec(currentUrl);
currentUrl = currentUrl ? currentUrl[0] : '';
}
return (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + currentUrl + '/' + subscriptionsEndPoint;
}
// if location is absolute (e.g. "/api") then prepend host only
return (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + subscriptionsEndPoint;
}
const subscriptionEndPoint = getSubscriptionsEndPoint();

// Enable Subscriptions via WebSocket
let subscriptionsClient = null;
function subscriptionsTransportWsFetcher(graphQLParams, fetcherOpts = { headers: {} }) {
if (!subscriptionsClient)
subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndPoint, { reconnect: true });
return window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, function (_graphQLParams) {
return graphQLFetcher(_graphQLParams, fetcherOpts);
})(graphQLParams);
}

function isSubscription(operationName, documentAST) {
if (!documentAST.definitions || !documentAST.definitions.length || !documentAST.definitions.filter) return false;
let definitions = documentAST.definitions.filter(function (def) { return def.kind === 'OperationDefinition'; });
if (operationName) definitions = definitions.filter(function (def) { return def.name && def.name.value === operationName; });
if (definitions.length === 0) return false;
return definitions[0].operation === 'subscription';
}

let wsClient = null;
function graphQLWsFetcher(payload, fetcherOpts) {
if (!fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST))
return graphQLFetcher(payload, fetcherOpts);
if (!wsClient) {
wsClient = graphqlWs.createClient({ url: subscriptionEndPoint });
}
let deferred = null;
const pending = [];
let throwMe = null,
done = false;
const dispose = wsClient.subscribe(payload, {
next: (data) => {
pending.push(data);
if (deferred) deferred.resolve(false);
},
error: (err) => {
if (err instanceof Error) {
throwMe = err;
} else if (err instanceof CloseEvent) {
throwMe = new Error(`Socket closed with event ${err.code} ${err.reason || ""}`.trim());
} else {
// GraphQLError[]
throwMe = new Error(err.map(({ message }) => message).join(", "));
}
if (deferred) deferred.reject(throwMe);
},
complete: () => {
done = true;
if (deferred) deferred.resolve(true);
},
});

return {
[Symbol.asyncIterator]: function() {
return this;
},
next: function() {
if (done) return Promise.resolve({ done: true, value: undefined });
if (throwMe) return Promise.reject(throwMe);
if (pending.length) return Promise.resolve({ value: pending.shift() });
return new Promise(function(resolve, reject) {
deferred = { resolve, reject };
}).then(function(result) {
if (result) {
return { done: true, value: undefined };
} else {
return { value: pending.shift() };
}
});
},
return: function() {
dispose();
return Promise.resolve({ done: true, value: undefined });
}
};
}

const subscriptionFetcher = (true) ? graphQLWsFetcher : subscriptionsTransportWsFetcher;

// Render <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: subscriptionFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
isHeadersEditorEnabled: true,
}),
document.getElementById('graphiql'),
);
</script>
</body>
</html>
"""

/// <summary>A <see cref="Suave.Http.WebPart"/> that serves the GraphiQL IDE at <c>/graphiql</c>, pointed at the GraphQL API served at <c>/</c>.</summary>
let webPart : WebPart =
Filters.path "/graphiql"
>=> Filters.GET
>=> Writers.setMimeType "text/html; charset=utf-8"
>=> Successful.OK page
22 changes: 22 additions & 0 deletions samples/magic-eight-ball/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module FSharp.Data.GraphQL.Samples.MagicEightBall.Program

open System
open Suave
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Server.Suave
open FSharp.Data.GraphQL.Samples.MagicEightBall.GraphiQL
open FSharp.Data.GraphQL.Samples.MagicEightBall.Schema

[<EntryPoint>]
let main _ =
let random = Random ()

let rootFactory (_ctx : HttpContext) : Root = { Random = random }

let executor = Executor (schema)

let app = choose [ webPart; GraphQL.graphQL executor rootFactory ]

startWebServer defaultConfig app

0
Loading
Loading