Skip to content

Instantly share code, notes, and snippets.

@queerviolet
Last active May 25, 2024 04:38
Show Gist options
  • Select an option

  • Save queerviolet/41c01c6b7b5198f688c0b6e10523e735 to your computer and use it in GitHub Desktop.

Select an option

Save queerviolet/41c01c6b7b5198f688c0b6e10523e735 to your computer and use it in GitHub Desktop.

Revisions

  1. queerviolet revised this gist Nov 21, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -49,7 +49,7 @@ export const TransactionPlugin = <Txn>(transact: Transact<Txn>): ApolloServerPlu
    transactionResult = transact(t => {
    resolve(t)

    // We return our didFinish promises here to hold the
    // We return our didFinish promise here to hold the
    // transaction open while resolvers are executing.
    return didFinish
    }).then(() => ({ ok: true }), error => ({ error }))
  2. queerviolet revised this gist Nov 21, 2019. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -48,6 +48,9 @@ export const TransactionPlugin = <Txn>(transact: Transact<Txn>): ApolloServerPlu
    // warnings from node.
    transactionResult = transact(t => {
    resolve(t)

    // We return our didFinish promises here to hold the
    // transaction open while resolvers are executing.
    return didFinish
    }).then(() => ({ ok: true }), error => ({ error }))
    )
  3. queerviolet revised this gist Nov 21, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -40,7 +40,7 @@ export const TransactionPlugin = <Txn>(transact: Transact<Txn>): ApolloServerPlu
    // Create the transaction.
    //
    // The db.transact function may create a transaction and call its
    // callback asynchonrously—this deals with that correctly.
    // 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
  4. queerviolet revised this gist Nov 21, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -78,7 +78,7 @@ export const TransactionPlugin = <Txn>(transact: Transact<Txn>): ApolloServerPlu
    // 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)
    return transactionResult
    .then(({ error }) => { throw error })
    }
    }
  5. queerviolet revised this gist Nov 21, 2019. 2 changed files with 2 additions and 4 deletions.
    3 changes: 1 addition & 2 deletions block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -4,8 +4,7 @@
    * use them in Apollo Server:
    */

    import { ApolloServerPlugin } from "src/plugins"
    import { GraphQLRequestContext } from "apollo-server-plugin-base"
    import { ApolloServerPlugin, GraphQLRequestContext } from 'apollo-server-plugin-base'

    export interface Transactable<Txn> {
    transact?: Transact<Txn>
    3 changes: 1 addition & 2 deletions imperative-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,4 @@
    import { ApolloServerPlugin } from "src/plugins";
    import { GraphQLRequestContext } from "apollo-server-plugin-base";
    import { ApolloServerPlugin, GraphQLRequestContext } from 'apollo-server-plugin-base'

    /**
    * If your database provides imperative transaction (with separate create,
  6. queerviolet revised this gist Nov 21, 2019. 1 changed file with 51 additions and 0 deletions.
    51 changes: 51 additions & 0 deletions imperative-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,51 @@
    import { ApolloServerPlugin } from "src/plugins";
    import { 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 })
    }
    }
    }
    })
  7. queerviolet revised this gist Nov 21, 2019. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -1,10 +1,12 @@

    /**
    * 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 } from "src/plugins"
    import { GraphQLRequestContext } from "apollo-server-plugin-base"

    export interface Transactable<Txn> {
    transact?: Transact<Txn>
    }
  8. queerviolet created this gist Nov 21, 2019.
    85 changes: 85 additions & 0 deletions block-transactions.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,85 @@

    /**
    * 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:
    */

    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 asynchonrously—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)
    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 Promise.resolve(transactionResult)
    .then(({ error }) => { throw error })
    }
    }
    }
    })