postgraphile-plugin-atomic-mutations
Advanced tools
Comparing version 1.0.2 to 1.0.3
/// <reference types="node" /> | ||
import { IncomingMessage } from 'http'; | ||
import { PostGraphilePlugin } from 'postgraphile'; | ||
export declare const MutationAtomicityHeaderName = "x-mutation-atomicity"; | ||
@@ -18,5 +17,4 @@ export declare enum MutationAtomicityHeaderValue { | ||
}; | ||
export declare const getMutationAtomicityContext: (req: any) => MutationAtomicityContext; | ||
export declare const AtomicMutationsHook: PostGraphilePlugin; | ||
export declare const getMutationAtomicityContext: (req: any, enablePluginByDefault?: boolean) => MutationAtomicityContext; | ||
export declare const AtomicMutationsPlugin: import("postgraphile").Plugin; | ||
//# sourceMappingURL=atomic-mutations-plugin.d.ts.map |
@@ -11,4 +11,30 @@ "use strict"; | ||
})(MutationAtomicityHeaderValue = exports.MutationAtomicityHeaderValue || (exports.MutationAtomicityHeaderValue = {})); | ||
exports.getMutationAtomicityContext = (req) => { | ||
if (req.headers[exports.MutationAtomicityHeaderName] === 'on') { | ||
exports.getMutationAtomicityContext = (req, enablePluginByDefault = false) => { | ||
const body = req.body; | ||
const paramsList = typeof body === 'string' ? { query: body } : body; | ||
if (Array.isArray(paramsList) && paramsList.length > 1) { | ||
throw Error('AtomicMutationsPlugin does not support GraphQL query batching'); | ||
} | ||
const { query, operationName } = paramsList; | ||
const parsedQuery = graphql_1.parse(query); | ||
const [executedDefinition] = parsedQuery.definitions.filter((definition) => (definition.name === undefined && operationName === undefined) || | ||
definition.name.value === operationName); | ||
if (executedDefinition && | ||
executedDefinition.operation === 'mutation' && | ||
((req.headers[exports.MutationAtomicityHeaderName] && | ||
req.headers[exports.MutationAtomicityHeaderName].toLowerCase() === | ||
MutationAtomicityHeaderValue.ON) || | ||
(req.headers[exports.MutationAtomicityHeaderName] === undefined && | ||
enablePluginByDefault))) { | ||
req.headers[exports.MutationAtomicityHeaderName] = MutationAtomicityHeaderValue.ON; | ||
const totalMutations = executedDefinition.selectionSet.selections.filter((selection) => selection.name.value !== '__typename').length; | ||
req.mutationAtomicityContext = { | ||
totalMutations, | ||
executedMutations: [], | ||
}; | ||
} | ||
else { | ||
req.headers[exports.MutationAtomicityHeaderName] = MutationAtomicityHeaderValue.OFF; | ||
} | ||
if (req.headers[exports.MutationAtomicityHeaderName] === MutationAtomicityHeaderValue.ON) { | ||
return req.mutationAtomicityContext; | ||
@@ -20,30 +46,2 @@ } | ||
}; | ||
exports.AtomicMutationsHook = { | ||
['postgraphile:httpParamsList'](paramsList, { req }) { | ||
var _a; | ||
if (paramsList.length > 1) { | ||
throw Error('AtomicMutationsPlugin does not support GraphQL query batching'); | ||
} | ||
if (paramsList.length === 1) { | ||
const { query, operationName } = paramsList[0]; | ||
const parsedQuery = graphql_1.parse(query); | ||
const [executedDefinition] = parsedQuery.definitions.filter((definition) => definition.name.value === operationName); | ||
if (executedDefinition && | ||
executedDefinition.operation === 'mutation' && | ||
((_a = req.headers[exports.MutationAtomicityHeaderName]) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === | ||
MutationAtomicityHeaderValue.ON) { | ||
const totalMutations = executedDefinition.selectionSet.selections.filter((selection) => selection.name.value !== '__typename').length; | ||
req.mutationAtomicityContext = { | ||
totalMutations, | ||
executedMutations: [], | ||
}; | ||
} | ||
else { | ||
req.headers[exports.MutationAtomicityHeaderName] = | ||
MutationAtomicityHeaderValue.OFF; | ||
} | ||
} | ||
return paramsList; | ||
}, | ||
}; | ||
exports.AtomicMutationsPlugin = postgraphile_1.makeWrapResolversPlugin((context) => { | ||
@@ -59,2 +57,6 @@ if (context.scope.isRootMutation) { | ||
try { | ||
if (mutationAtomicityContext && | ||
mutationAtomicityContext.executedMutations.length === 0) { | ||
await pgClient.query('SAVEPOINT atomic_mutations_tx_start'); | ||
} | ||
const result = await resolver(source, args, context, resolveInfo); | ||
@@ -75,7 +77,11 @@ if (mutationAtomicityContext) { | ||
mutationAtomicityContext.executedMutations.length === | ||
mutationAtomicityContext.totalMutations && | ||
mutationAtomicityContext.executedMutations.filter((mutation) => mutation.hasErrors === true).length) { | ||
pgClient.query(`rollback`); | ||
mutationAtomicityContext.totalMutations) { | ||
if (mutationAtomicityContext.executedMutations.filter((mutation) => mutation.hasErrors === true).length) { | ||
await pgClient.query(`ROLLBACK TO SAVEPOINT atomic_mutations_tx_start`); | ||
} | ||
else { | ||
await pgClient.query('RELEASE SAVEPOINT atomic_mutations_tx_start'); | ||
} | ||
} | ||
} | ||
}); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const atomic_mutations_plugin_1 = require("./atomic-mutations-plugin"); | ||
const reqWithHeaderOff = { | ||
const reqSuperSimpleQueryHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
body: `{ | ||
someQuery | ||
}`, | ||
}; | ||
const reqSimpleQueryHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
body: { | ||
query: `{ | ||
someQuery | ||
}`, | ||
}, | ||
}; | ||
const reqSimpleMutationHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
body: { | ||
query: `mutation { | ||
someMutation | ||
}`, | ||
}, | ||
}; | ||
const reqSimpleMutationHeaderOff = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.OFF }, | ||
mutationAtomicityContext: { | ||
totalMutations: 1, | ||
executedMutations: [{ hasErrors: false }], | ||
body: { | ||
query: `mutation { | ||
someMutation | ||
}`, | ||
}, | ||
}; | ||
const reqWithoutHeader = { | ||
const reqSimpleMutationHeaderUndefined = { | ||
headers: {}, | ||
mutationAtomicityContext: { | ||
totalMutations: 1, | ||
executedMutations: [{ hasErrors: false }], | ||
body: { | ||
query: `mutation { | ||
someMutation | ||
}`, | ||
}, | ||
}; | ||
const reqMutationWithErrors = { | ||
const reqSingleQueryHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
mutationAtomicityContext: { | ||
totalMutations: 1, | ||
executedMutations: [{ hasErrors: false }], | ||
body: { | ||
query: `query QueryName { | ||
someQuery | ||
anotherQuery | ||
}`, | ||
operationName: 'QueryName', | ||
}, | ||
}; | ||
const reqMutationWithoutErrors = { | ||
const reqSingleMutationHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
mutationAtomicityContext: { | ||
totalMutations: 1, | ||
executedMutations: [{ hasErrors: false }], | ||
body: { | ||
query: `mutation OperationName { | ||
__typename | ||
someMutation | ||
anotherMutation | ||
}`, | ||
operationName: 'OperationName', | ||
totalMutations: 2, | ||
}, | ||
}; | ||
test('retrieve mutationAtomicityContext - reqMutationWithErrors', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMutationWithErrors); | ||
expect(mutationAtomicityContext).toBeTruthy(); | ||
const reqMixedQueryMutationHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
body: { | ||
query: ` | ||
query QueryName { | ||
someQuery | ||
} | ||
mutation MutationName { | ||
__typename | ||
someMutation | ||
anotherMutation | ||
}`, | ||
operationName: 'MutationName', | ||
totalMutations: 2, | ||
}, | ||
}; | ||
const reqMixedQueryMutationHeaderOff = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.OFF }, | ||
body: { | ||
query: ` | ||
query QueryName { | ||
someQuery | ||
} | ||
mutation MutationName { | ||
__typename | ||
someMutation | ||
anotherMutation | ||
}`, | ||
operationName: 'MutationName', | ||
totalMutations: 2, | ||
}, | ||
}; | ||
const reqMultipleMutationHeaderOn = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
body: { | ||
query: ` | ||
query QueryName { | ||
someQuery | ||
} | ||
mutation MutationName { | ||
__typename | ||
someMutation | ||
anotherMutation | ||
}`, | ||
operationName: 'MutationName', | ||
totalMutations: 2, | ||
}, | ||
}; | ||
const reqMultipleMutationHeaderOff = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.OFF }, | ||
body: { | ||
query: ` | ||
query QueryName { | ||
someQuery | ||
} | ||
mutation MutationName { | ||
__typename | ||
someMutation | ||
anotherMutation | ||
}`, | ||
operationName: 'MutationName', | ||
totalMutations: 2, | ||
}, | ||
}; | ||
const reqUnsupportedQueryBatching = { | ||
headers: { [atomic_mutations_plugin_1.MutationAtomicityHeaderName]: atomic_mutations_plugin_1.MutationAtomicityHeaderValue.ON }, | ||
body: [ | ||
{ | ||
query: `query QueryName { | ||
someQuery | ||
}`, | ||
}, | ||
{ | ||
query: `query QueryName { | ||
someQuery | ||
}`, | ||
}, | ||
], | ||
}; | ||
describe('query operations', () => { | ||
test('super simple query - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSuperSimpleQueryHeaderOn); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
test('simple query - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSimpleQueryHeaderOn); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
test('single query - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSingleQueryHeaderOn); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
}); | ||
test('retrieve mutationAtomicityContext - reqMutationWithoutErrors', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMutationWithoutErrors); | ||
expect(mutationAtomicityContext).toBeTruthy(); | ||
describe('mutation operations', () => { | ||
test('simple mutation - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSimpleMutationHeaderOn); | ||
expect(mutationAtomicityContext).toBeTruthy(); | ||
}); | ||
test('simple mutation - header off', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSimpleMutationHeaderOff); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
test('simple mutation - header undefined', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSimpleMutationHeaderUndefined); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
test('single mutation - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSingleMutationHeaderOn); | ||
expect(mutationAtomicityContext).toBeTruthy(); | ||
}); | ||
test('multiple mutation - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMultipleMutationHeaderOn); | ||
expect(mutationAtomicityContext).toBeTruthy(); | ||
}); | ||
test('multiple mutation - header off', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMultipleMutationHeaderOff); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
test('mixed query & mutation - header on', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMixedQueryMutationHeaderOn); | ||
expect(mutationAtomicityContext).toBeTruthy(); | ||
}); | ||
test('mixed query & mutation - header off', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMixedQueryMutationHeaderOff); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
}); | ||
}); | ||
test('MutationAtomicityHeaderName - OFF', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqWithHeaderOff); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
describe('mutation counts', () => { | ||
test('single mutation - total mutation count', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqSingleMutationHeaderOn); | ||
expect(mutationAtomicityContext.totalMutations).toEqual(reqSingleMutationHeaderOn.body.totalMutations); | ||
}); | ||
test('multiple mutation - total mutation count', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMultipleMutationHeaderOn); | ||
expect(mutationAtomicityContext.totalMutations).toEqual(reqMultipleMutationHeaderOn.body.totalMutations); | ||
}); | ||
test('mixed query & mutation - total mutation count', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqMixedQueryMutationHeaderOn); | ||
expect(mutationAtomicityContext.totalMutations).toEqual(reqMixedQueryMutationHeaderOn.body.totalMutations); | ||
}); | ||
}); | ||
test('MutationAtomicityHeaderName - Missing', () => { | ||
const mutationAtomicityContext = atomic_mutations_plugin_1.getMutationAtomicityContext(reqWithoutHeader); | ||
expect(mutationAtomicityContext).toBeFalsy(); | ||
describe('unsupported', () => { | ||
test('query batching exception', () => { | ||
const unsupportedQueryBatching = () => { | ||
atomic_mutations_plugin_1.getMutationAtomicityContext(reqUnsupportedQueryBatching); | ||
}; | ||
expect(unsupportedQueryBatching).toThrow('AtomicMutationsPlugin does not support GraphQL query batching'); | ||
}); | ||
}); |
{ | ||
"name": "postgraphile-plugin-atomic-mutations", | ||
"version": "1.0.2", | ||
"version": "1.0.3", | ||
"description": "Postgraphile plugin to enable mutation atomicty with GraphQL requests containing multiple mutations", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -27,6 +27,3 @@ # postgraphile-plugin-atomic-mutations | ||
```js | ||
import { makePluginHook } from 'postgraphile'; | ||
import { | ||
AtomicMutationsHook, | ||
AtomicMutationsPlugin, | ||
@@ -38,15 +35,10 @@ getMutationAtomicityContext, | ||
// Used to detect the custom HTTP header which enables or disables the plugin dynamically. | ||
const pluginHook = makePluginHook([AtomicMutationsHook]); | ||
app.use( | ||
postgraphile(pgConfig, schema, { | ||
// The actual plugin which controls atomicity. | ||
// Append the plugin to the array. | ||
appendPlugins: [AtomicMutationsPlugin], | ||
pluginHook, | ||
async additionalGraphQLContextFromRequest(req, res) { | ||
return { | ||
// Additional context shared between AtomicMutationsHook and AtomicMutationsPlugin | ||
// Additional context needed by the plugin | ||
mutationAtomicityContext: getMutationAtomicityContext(req), | ||
@@ -61,2 +53,16 @@ }; | ||
```js | ||
const getMutationAtomicityContext = ( | ||
req, | ||
enablePluginByDefault = false, | ||
): MutationAtomicityContext; | ||
``` | ||
This function will extract the HTTP headers from the `req` object passed as the | ||
1st argument to determine if the mutation atomicity behavior will be enabled or | ||
not. See _Usage from Client_ for more details. | ||
The 2nd argument `enablePluginByDefault` will determine plugin behavior when the | ||
HTTP header is not set (defaults to `false`). | ||
## Usage from Client | ||
@@ -103,8 +109,5 @@ | ||
`#3` will never be committed to the database since Mutation `#2` caused an | ||
error, guaranteeing atomicity for the set of Mutations. If the client decides to | ||
re-run the same request again, the outcome will be the same. | ||
error, thus, guaranteeing atomicity for the set of Mutations. If the client | ||
decides to re-run the same request again, the outcome will be the same. | ||
If the `'X-Mutation-Atomicity'` is set to `'off` or not set at all, the plugin | ||
will be disabled. | ||
## Limitations | ||
@@ -125,4 +128,2 @@ | ||
- Introduce plugin options to control default behavior. | ||
- When the client does not specify a `'X-Mutation-Atomicity'` header. | ||
- Improve tests to have more code coverage. | ||
@@ -129,0 +130,0 @@ |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
20537
321
136
1