src/plugins/make-coinflip-bet.ts
that implements a graphql mutation makeCoinflipBet(wager, 'HEADS' | 'TAILS')
You can write plugins to extend the GraphQL API with your own queries and mutations.
Just pass your additional plugins to the @moneypot/hub
server's startAndListen
function:
import { defaultPlugins, ServerOptions, startAndListen } from "@moneypot/hub"; import { MyPlugin } from "./MyPlugin"; const options: ServerOptions = { extraPgSchemas: ["app"], plugins: [...defaultPlugins, MyPlugin], // ... }; startAndListen(options, ({ port }) => { console.log(`Server is listening on ${port}...`); });
Note: defaultPlugins
are the core @moneypot/hub
plugins that create the base API.
Here's a simple resolver
-style plugin that should help you get started:
import { makeExtendSchemaPlugin, gql } from "@moneypot/hub/graphile"; type Fortune = { id: number; text: string; }; const database = { nextId: 10, fortunes: [ { id: 0, text: "You will find a new friend soon." }, { id: 1, text: "A pleasant surprise is waiting for you." }, { id: 2, text: "Your hard work will pay off in the near future." }, { id: 3, text: "An exciting opportunity will present itself to you." }, { id: 4, text: "You will overcome a significant challenge." }, { id: 5, text: "A long-lost acquaintance will reenter your life." }, { id: 6, text: "Your creativity will lead you to success." }, { id: 7, text: "A journey of a thousand miles begins with a single step." }, { id: 8, text: "Your kindness will be repaid tenfold." }, { id: 9, text: "Good fortune will be yours in the coming months." }, ], insertFortune: async (text: string): Promise<Fortune> => { const fortune: Fortune = { id: database.nextId++, text }; database.fortunes.push(todo); return fortune; }, getFortuneById: async (id: number): Promise<Fortune | null> => { return database.fortunes[id] || null; }, }; export const MyPlugin = makeExtendSchemaPlugin(() => { return { typeDefs: gql` type Fortune { id: Int! text: String! } extend type Query { randomFortune: Fortune } extend type Mutation { addFortune(text: String!): Fortune } `, resolvers: { Mutation: { addFortune: async (_, args) => { const { text } = args; const fortune = await database.insertFortune(text); return fortune; }, }, Query: { randomFortune: async () => { const id = Math.floor(Math.random() * database.fortunes.length); const fortune = await database.getFortuneById(id); return fortune; }, }, }, }; });
Let's add our new plugin to hub:
import { defaultPlugins, ServerOptions, startAndListen } from "@moneypot/hub"; import { MyPlugin } from "./MyPlugin"; const options: ServerOptions = { extraPgSchemas: ["app"], plugins: [...defaultPlugins, MyPlugin], // ... }; startAndListen(options, ({ port }) => { console.log(`Server is listening on ${port}...`); });
Now when you launch the server, you should be able to visit it in the browser (GraphiQL interface) and see our new query and mutation.
For those familiar with postgraphile, you can also provide postgraphile v5 plan
-based plugins, though resolver
-based plugins are much simpler.
resolver
plugin example (simple)Here's a more realistic example of a resolver
-style plugin that adds a makeCoinflip
mutation to the API:
// ./plugins/coinflip-resolver.ts import type { PluginContext } from "@moneypot/hub"; import { superuserPool, withPgPoolTransaction } from "@moneypot/hub/db"; import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile"; import { GraphQLError } from "@moneypot/hub/graphql"; import crypto from "crypto"; export const MakeCoinflipResolverPlugin = makeExtendSchemaPlugin(() => { return { typeDefs: gql` input MakeCoinflipInput { currency: String! wager: Float! } type CoinflipOut { id: UUID! net: Float! } extend type Mutation { makeCoinflip(input: MakeCoinflipInput!): CoinflipOut } `, resolvers: { Mutation: { makeCoinflip: async (_, { input }, context: PluginContext) => { if (context.identity?.kind !== "user") { // Not logged in as a user return null; } const { currency, wager } = input; const { user_id, casino_id, experience_id } = context.identity.session; // TODO: Validate input if (wager < 1) { throw new GraphQLError("Wager must be at least 1"); } return withPgPoolTransaction(superuserPool, async (pgClient) => { // TODO: Ensure user can afford wager and bankroll can afford payout. const playerWon = crypto.randomInt(0, 2) === 0; const net = playerWon ? wager * 0.99 : -wager; // TODO: Update user's balance and casino's bankroll const coinflip = await pgClient .query( ` insert into app.coinflip (id, wager, net, currency_key, user_id, casino_id, experience_id) values (hub_hidden.uuid_generate_v7(), $1, $2, $3, $4, $5, $6) returning * `, [wager, net, currency, user_id, casino_id, experience_id] ) .then((result) => result.rows[0]); return coinflip; }); }, }, }, }; });
Notice how we can't return a first-class Coinflip
graphql entity from our resolver
plugin; we can only return explicit data. That's a limitation of resolver
plugins.
But the following plan
plugin will let us return a Coinflip
graphql entity.
plan
plugin example (advanced)Postgraphile plan
plugins are more powerful than resolver
plugins, but they require more Postgraphile-specific knowledge.
Plan plugins are more advanced because they use [Grafast][grafast] step plans which allow certain optimizations but require more understanding of the underlaying grafast library.
Some benefits of using plans over resolvers:
makeCoinflip()
plan can return a canonical Coinflip
entity (a type that postgraphile generated from our app.coinflip
table) unlike the resolver plan.Here's the resolver plugin converted into a grafast plan plugin. Notice how it returns a Coinflip
instead of a type that the resolver plugin created just for its own return value.
// ./plugins/coinflip-plan.ts import type { PluginContext } from "@moneypot/hub"; import { superuserPool, withPgPoolTransaction } from "@moneypot/hub/db"; import { context, sideEffect } from "@moneypot/hub/grafast"; import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile"; import { GraphQLError } from "@moneypot/hub/graphql"; import crypto from "crypto"; export const ConflipPlanPlugin = makeExtendSchemaPlugin((build) => { // This line is key since it will let us return a plan that looks up a coinflip record by id. const coinflipTable = build.input.pgRegistry.pgResources.coinflip; return { typeDefs: gql` input MakeCoinflipInput { currency: String! wager: Float! } extend type Mutation { makeCoinflip(input: MakeCoinflipInput!): Coinflip } `, plans: { Mutation: { makeCoinflip: (_, { $input }) => { const $context = context<PluginContext>(); const $conflipId = sideEffect( [$input, $context], ([input, context]) => { if (context.identity?.kind !== "user") { // Not logged in as user return null; } // You could use tsafe or zod to enforce the MakeCoinflipInput type here // which will be guaranteed by postgraphile. // But for this demo, we'll just use `any`. const { currency, wager } = input as any; const { user_id, casino_id, experience_id } = context.identity.session; // TODO: Validate input if (wager < 1) { throw new GraphQLError("Wager must be at least 1"); } return withPgPoolTransaction(superuserPool, async (pgClient) => { // TODO: Ensure user can afford wager and bankroll can afford payout. const playerWon = crypto.randomInt(0, 2) === 0; const net = playerWon ? wager * 0.99 : -wager; // TODO: Update user's balance and casino's bankroll const coinflip = await pgClient .query( ` insert into app.coinflip (id, wager, net, currency_key, user_id, casino_id, experience_id) values (hub_hidden.uuid_generate_v7(), $1, $2, $3, $4, $5, $6) returning id `, [wager, net, currency, user_id, casino_id, experience_id] ) .then((result) => result.rows[0]); return coinflip.id; }); } ); return coinflipTable.get({ id: $conflipId }); }, }, }, }; });