-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Refactor GraphQL Server and CreateYoga to Support "api serve" with Fastify Server #8339
Refactor GraphQL Server and CreateYoga to Support "api serve" with Fastify Server #8339
Conversation
7555591
to
66706f0
Compare
Have started the work to merge in project subscriptions into the over schema and resolvers. |
I have subscriptions and live queries running in a test app with this PR. Server File SetupNote here that you'll import a glob of import path from 'path'
import { useLiveQuery } from '@envelop/live-query'
import { astFromDirective } from '@graphql-tools/utils'
import { GraphQLLiveDirective } from '@n1ru4l/graphql-live-query'
import chalk from 'chalk'
import { config } from 'dotenv-defaults'
import Fastify from 'fastify'
import { OperationTypeNode } from 'graphql'
import {
coerceRootPath,
redwoodFastifyWeb,
redwoodFastifyAPI,
redwoodFastifyGraphQLServer,
DEFAULT_REDWOOD_FASTIFY_CONFIG,
} from '@redwoodjs/fastify'
import { getPaths, getConfig } from '@redwoodjs/project-config'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import subscriptions from 'src/subscriptions/**/*.{js,ts}'
import { liveQueryStore } from 'src/services/auctions/auctions'
import { logger } from './lib/logger'
async function serve() {
// Load .env files
const redwoodProjectPaths = getPaths()
const redwoodConfig = getConfig()
const apiRootPath = coerceRootPath(redwoodConfig.web.apiUrl)
const port = redwoodConfig.web.port
const tsServer = Date.now()
config({
path: path.join(redwoodProjectPaths.base, '.env'),
defaults: path.join(redwoodProjectPaths.base, '.env.defaults'),
multiline: true,
})
console.log(chalk.italic.dim('Starting API and Web Servers...'))
// Configure Fastify
const fastify = Fastify({
...DEFAULT_REDWOOD_FASTIFY_CONFIG,
})
await fastify.register(redwoodFastifyWeb)
await fastify.register(redwoodFastifyAPI, {
redwood: {
apiRootPath,
},
})
await fastify.register(redwoodFastifyGraphQLServer, {
loggerConfig: {
logger: logger,
options: { query: true, data: true, level: 'trace' },
},
graphiQLEndpoint: '/yoga',
sdls,
services,
directives,
subscriptions,
allowedOperations: [
OperationTypeNode.SUBSCRIPTION,
OperationTypeNode.QUERY,
OperationTypeNode.MUTATION,
],
allowIntrospection: true,
allowGraphiQL: true,
schemaOptions: { typeDefs: [astFromDirective(GraphQLLiveDirective)] },
extraPlugins: [useLiveQuery({ liveQueryStore })],
})
// Start
fastify.listen({ port })
fastify.ready(() => {
console.log(chalk.italic.dim('Took ' + (Date.now() - tsServer) + ' ms'))
const on = chalk.magenta(`http://localhost:${port}${apiRootPath}`)
const webServer = chalk.green(`http://localhost:${port}`)
const apiServer = chalk.magenta(`http://localhost:${port}`)
console.log(`Web server started on ${webServer}`)
console.log(`API serving from ${apiServer}`)
console.log(`API listening on ${on}`)
const graphqlEnd = chalk.magenta(`${apiRootPath}graphql`)
console.log(`GraphQL serverless function endpoint at ${graphqlEnd}`)
})
process.on('exit', () => {
fastify.close()
})
}
serve() Subscriptions
A subscription file can be import gql from 'graphql-tag'
export const schema = gql`
type Subscription {
countdown(from: Int!, interval: Int!): Int!
}
`
const countdown = {
countdown: {
async *subscribe(_, { from, interval }) {
for (let i = from; i >= 0; i--) {
await new Promise((resolve) => setTimeout(resolve, interval ?? 1000))
yield { countdown: i }
}
},
},
}
export default countdown or for a import { createPubSub } from '@graphql-yoga/node'
import gql from 'graphql-tag'
import { logger } from 'src/lib/logger'
export const pubSubNewMessage = createPubSub<{
newMessage: [payload: { from: string; body: string }]
}>()
export const schema = gql`
type Subscription {
newMessage(roomId: ID!): Message!
}
`
const newMessage = {
newMessage: {
subscribe: (_, { roomId }) => {
logger.debug({ roomId }, 'newMessage subscription')
return pubSubNewMessage.subscribe(roomId)
},
resolve: (payload) => {
logger.debug({ payload }, 'newMessage subscription resolve')
return payload
},
},
}
export default newMessage Define SDL typesHere the export const schema = gql`
type Message {
from: String
body: String
}
type Query {
room(id: ID!): [Message!]! @skipAuth
}
input SendMessageInput {
roomId: ID!
from: String!
body: String!
}
type Mutation {
send(input: SendMessageInput!): Message! @skipAuth
}
` ServicesThe import { logger } from 'src/lib/logger'
import { pubSubNewMessage } from 'src/subscriptions/newMessage/newMessage'
export const room = ({ id }) => [id]
// important to be async!
export const send = async ({ input }) => {
logger.debug({ input }, 'send input')
const { roomId, ...newMessage } = input
pubSubNewMessage.publish(roomId, newMessage)
return input
} Live QueriesThe DX isn't 100% yet because the two parts to the server and yoga setup are:
SDL TypesHere is an export const schema = gql`
type Query {
auction(id: ID!): Auction @skipAuth
}
type Auction {
id: ID!
title: String!
highestBid: Bid
bids: [Bid!]!
}
type Bid {
amount: Int!
}
type Mutation {
bid(input: BidInput!): Bid @skipAuth
}
input BidInput {
auctionId: ID!
amount: Int!
}
` ServicesHere, there is a collection on auctions and can fetch an auction and make a bid on an auction. When a bid is made, the Auction key is invalidated in the live query memory store. import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store'
import { logger } from 'src/lib/logger'
export const liveQueryStore = new InMemoryLiveQueryStore()
const auctions = [
{ id: '1', title: 'Digital-only PS5', bids: [{ amount: 100 }] },
]
export const auction = async ({ id }) => {
const foundAuction = auctions.find((a) => a.id === id)
logger.debug({ id, auction: foundAuction }, 'auction')
return foundAuction
}
export const bid = async ({ input }) => {
const { auctionId, amount } = input
const index = auctions.findIndex((a) => a.id === auctionId)
const bid = { amount }
auctions[index].bids.push(bid)
logger.debug({ auctionId, bid }, 'Added bid to auction')
const key = `Auction:${auctionId}`
liveQueryStore.invalidate(key)
logger.debug({ key }, 'Invalidated auction key in liveQueryStore')
return bid
}
export const Auction = {
highestBid: (obj, { root }) => {
const [max] = root.bids.sort((a, b) => b.amount - a.amount)
logger.debug({ obj, root }, 'highestBid')
return max
},
} |
packages/cli/src/commands/experimental/templates/server.ts.template
Outdated
Show resolved
Hide resolved
@dthyresson and I talked about this one; we're synced up on concerns and next steps and want to get this into main so development can keep going on other milestones. |
woohoo |
…te-default * 'main' of github.com:redwoodjs/redwood: (23 commits) chore(deps): update dependency @clerk/clerk-react to v4.16.2 (redwoodjs#8362) chore(package size): implement `findup-sync` in `@redwoodjs/project-config` (redwoodjs#8315) Refactor GraphQL Server and CreateYoga to Support "api serve" with Fastify Server (redwoodjs#8339) chore(deps): update dependency octokit to v2.0.15 (redwoodjs#8360) fix(coherence): correct doc links, add commas to template (redwoodjs#8351) Parse as int, fix jsdoc (redwoodjs#8357) Update forms.md (redwoodjs#8352) chore: update yarn.lock chore(release): update release command for minors chore(deps): update dependency rimraf to v5.0.1 (redwoodjs#8350) chore(deps): update dependency glob to v10.2.5 (redwoodjs#8349) feat(coherence deploy): add setup deploy coherence (redwoodjs#8234) fix(deps): update dependency listr2 to v6.6.0 (redwoodjs#8347) fix(deps): update dependency react-router-dom to v6.11.2 (redwoodjs#8345) fix(deps): update prisma monorepo to v4.14.1 (redwoodjs#8346) fix(deps): update dependency webpack to v5.83.1 (redwoodjs#8348) chore(deps): update dependency dependency-cruiser to v13 (redwoodjs#8322) chore(deps): update dependency @clerk/clerk-react to v4.16.1 (redwoodjs#8324) chore(deps): update dependency @clerk/types to v3.38.0 (redwoodjs#8325) chore(deps): update dependency nx to v16.2.1 (redwoodjs#8343) ...
…stify Server (#8339) * Move makeMergedSchema * refactor createGraphQLYoga * Make a Fastify plugin for graphql yoga * Set yoga options * Adds graphql fastify plugin to template * Whitespace * Adds allowGraphiQL config setting and docs * fastify gql plugin awaits * Merge in subscriptions into schema from project * Get to green on tests * Fix subscriptions module api import * Tests makeSubscriptions * Fastify graphql plugin needed graphqlserver package * remove unneeded comments * Update docs/docs/graphql.md * Adds HookHandlerDoneFunction * Update packages/cli/src/commands/experimental/templates/server.ts.template Co-authored-by: Dominic Saadi <[email protected]> --------- Co-authored-by: Dominic Saadi <[email protected]>
…quest (#9039) **Problem** v6 has seen some performance issues. These appear to have sneaked in on #8339. This just reverts the change by moving the creation of the yoga object so that it isn't created on every request. **Changes** 1. Moves yoga creation. **Outstanding** 1. We still have a tiny bit of work to do analysing a slight performance decline since 4.4.3
…quest (#9039) **Problem** v6 has seen some performance issues. These appear to have sneaked in on #8339. This just reverts the change by moving the creation of the yoga object so that it isn't created on every request. **Changes** 1. Moves yoga creation. **Outstanding** 1. We still have a tiny bit of work to do analysing a slight performance decline since 4.4.3
…quest (#9039) **Problem** v6 has seen some performance issues. These appear to have sneaked in on #8339. This just reverts the change by moving the creation of the yoga object so that it isn't created on every request. **Changes** 1. Moves yoga creation. **Outstanding** 1. We still have a tiny bit of work to do analysing a slight performance decline since 4.4.3
…quest (#9039) **Problem** v6 has seen some performance issues. These appear to have sneaked in on #8339. This just reverts the change by moving the creation of the yoga object so that it isn't created on every request. **Changes** 1. Moves yoga creation. **Outstanding** 1. We still have a tiny bit of work to do analysing a slight performance decline since 4.4.3
As part of the effort to have fastify plugins for running the redwood api and web servers, this PR refactors parts of the graphql-server package so it too can be used in a more "serverful" manner.
And, in doing so, begin the support for "RedwoodJS Realtime" via GraphQL subscriptions and Live Queries when deployed with a stateful server and memory stores.
This PR:
createYoga
portion on the createGraphQLHandler so it can be used by both the serverless functiongraphql
and aallowGraphiQL
option because when running even in dev asyarn rw api serve
this is considered production and both introspection and the playground are disabled. Now, you can control both and allow the playground to be used withrw ap serve
Use
The
server.ts
here creates a server and also sets up subscription and live query support (in the playground only, there is no web side support yet to actually listen or subscribe).Currently, the types and resolvers are added to the RedwoodJS GraphQL schema via the extra
schemaOptions
option when merging the schema from the sdls, services and directives.Note: I just realized that this example doesn't mix both LQ and Subs due to the way I set the schemaOptions. But both do work independently. Hence, why need a way to merge these into there schema in the next iteration.
Important here is to "allow" subscriptions, introspection and graphiql.
and Subscriptions
Next Steps
There are many next steps to support realtime and GraphQL Subscriptions and Live Queries.