Last active
May 25, 2024 04:38
-
-
Save queerviolet/41c01c6b7b5198f688c0b6e10523e735 to your computer and use it in GitHub Desktop.
Handling Database Transactions with Apollo Server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 }) | |
} | |
} | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | |
} | |
} | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In the ImperativeTransactionPlugin, the variable transactionResult is never assigned. It's not normal right?