Skip to content

Instantly share code, notes, and snippets.

@queerviolet
Last active May 25, 2024 04:38
Show Gist options
  • Save queerviolet/41c01c6b7b5198f688c0b6e10523e735 to your computer and use it in GitHub Desktop.
Save queerviolet/41c01c6b7b5198f688c0b6e10523e735 to your computer and use it in GitHub Desktop.
Handling Database Transactions with Apollo Server
/**
* If your database provides block transactions, (like Sequelize,
* here: https://sequelize.org/master/manual/transactions.html) here's how to
* use them in Apollo Server:
*/
import { ApolloServerPlugin, GraphQLRequestContext } from 'apollo-server-plugin-base'
export interface Transactable<Txn> {
transact?: Transact<Txn>
}
export type Transact<Txn> = <T>(block: TransactionBlock<Txn, T>) => Promise<T>
export type TransactionBlock<Txn, T> = (txn: Txn) => Promise<T>
export const TransactionPlugin = <Txn>(transact: Transact<Txn>): ApolloServerPlugin => ({
requestDidStart<T extends Transactable<Txn>>(requestContext: GraphQLRequestContext<T>) {
// transactionResult is going to track the eventual result of our
// db transaction.
//
// It may remain undefined if execution never started—i.e. because
// parsing or validation failed.
//
// We need to attach error handlers after the transaction may have failed,
// so we track the result or failure as a value (that is,
// transactionResult will never reject).
let transactionResult: Promise<{ ok?: boolean, error?: any }> | void = undefined
return {
executionDidStart() {
// didFinish is going to get resolved after graphQL execution is done,
// thereby commiting the transaction.
let
ok: (value?: unknown) => void,
fail: (reason?: any) => void
const didFinish = new Promise((resolve, reject) => {
ok = resolve
fail = reject
})
// Create the transaction.
//
// The db.transact function may create a transaction and call its
// callback asynchronously—this deals with that correctly.
// `transaction` captures the (promise of the) transaction.
const transaction: Promise<Txn> = new Promise((resolve) =>
// We capture the result of this transaction here, attaching
// error handlers immediately to avoid UnhandledPromiseError
// warnings from node.
transactionResult = transact(t => {
resolve(t)
// We return our didFinish promise here to hold the
// transaction open while resolvers are executing.
return didFinish
}).then(() => ({ ok: true }), error => ({ error }))
)
// Add a transaction utility to the context.
// This behaves exactly like the `db.transact` provided by the database:
// You do your queries, passing in the request handle, and making
// sure to chain and return them as appropriate.
requestContext.context.transact =
async <T,>(block: (txn: Txn) => Promise<T>): Promise<T> =>
block(await transaction)
// Finally, in the executionDidFinish callback, we either resolve
// or reject the didFinish promise. This commits or rolls back the
// transaction, respectively.
return (err) => {
if (err) fail(err)
ok()
}
},
willSendResponse() {
// If execution never started, presumably some other error occurred
// and will be reported in its own way.
if (!transactionResult) return
// Finally, in willSendResponse, we return the (promise of the)
// transactionResult. If this is a failing promise (i.e., the
// transaction could not be committed), the entire
// request will fail with an error.
return transactionResult
.then(({ error }) => { throw error })
}
}
}
})
import { ApolloServerPlugin, GraphQLRequestContext } from 'apollo-server-plugin-base'
/**
* If your database provides imperative transaction (with separate create,
* commit, and rollback methods), here's how you can use that in Apollo
* Server:
*/
export interface TxnControls<Txn> {
create(): Promise<Txn>
commit(txn: Txn): Promise<void>
rollback(txn: Txn): void
}
export interface Transactable<Txn> {
transact?: Transact<Txn>
}
export type Transact<Txn> = <T>(block: TransactionBlock<Txn, T>) => Promise<T>
export type TransactionBlock<Txn, T> = (txn: Txn) => Promise<T>
export const ImperativeTransactionPlugin = <Txn>(controls: TxnControls<Txn>): ApolloServerPlugin => ({
requestDidStart<T extends Transactable<Txn>>(requestContext: GraphQLRequestContext<T>) {
let transactionResult: Promise<{ ok?: boolean, error?: any }> | void = undefined
return {
executionDidStart() {
const txn = controls.create()
requestContext.context.transact =
async (block: (txn: Txn) => any) =>
block(await txn)
return (err) => txn
.then(t => err ? controls.rollback(t) : controls.commit(t))
.then(() => ({ ok: true }), error => ({ error }))
},
willSendResponse() {
// If execution never started, presumably some other error occurred
// and will be reported in its own way.
if (!transactionResult) return
// Finally, in willSendResponse, we return the (promise of the)
// transactionResult. If this is a failing promise (i.e., the
// transaction could not be committed), the entire
// request will fail with an error.
return Promise.resolve(transactionResult)
.then(({ error }) => { throw error })
}
}
}
})
@hanego
Copy link

hanego commented May 26, 2020

In the ImperativeTransactionPlugin, the variable transactionResult is never assigned. It's not normal right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment