Skip to content

Commit

Permalink
Add a logging plugin (#3425)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Aug 21, 2020
1 parent c243839 commit 84116f7
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-horses-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/list-plugins': minor
---

Added a `logging` plugin to log mutations in a Keystone system.
172 changes: 172 additions & 0 deletions api-tests/hooks/logging-plugin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
const { Text, Password } = require('@keystonejs/fields');
const { multiAdapterRunners, setupServer } = require('@keystonejs/test-utils');
const { logging } = require('@keystonejs/list-plugins');
const {
createItem,
updateItem,
deleteItem,
runCustomQuery,
} = require('@keystonejs/server-side-graphql-client');
const { PasswordAuthStrategy } = require('@keystonejs/auth-password');

function setupKeystone(adapterName) {
return setupServer({
adapterName,
createLists: keystone => {
keystone._logFunction = jest.fn();
keystone.createList('User', {
fields: {
name: { type: Text },
other: { type: Text },
password: { type: Password },
},
plugins: [logging(keystone._logFunction)],
});
keystone.createAuthStrategy({
type: PasswordAuthStrategy,
list: 'User',
config: {
identityField: 'name',
secretField: 'password',
},
plugins: [logging(keystone._logFunction)],
});
},
});
}

multiAdapterRunners().map(({ runner, adapterName }) =>
describe(`Adapter: ${adapterName}`, () => {
test(
'Logging Hooks: create',
runner(setupKeystone, async ({ keystone }) => {
const context = keystone.createContext({
authentication: { item: { foo: 'bar' }, listKey: 'Other' },
});
await createItem({ context, listKey: 'User', item: { name: 'test' } });
expect(keystone._logFunction).toHaveBeenCalledWith({
operation: 'create',
authedItem: { foo: 'bar' },
authedListKey: 'Other',
listKey: 'User',
originalInput: { name: 'test' },
createdItem: expect.objectContaining({ name: 'test' }),
});
})
);

test(
'Logging Hooks: update',
runner(setupKeystone, async ({ keystone }) => {
const context = keystone.createContext({
authentication: { item: { foo: 'bar' }, listKey: 'Other' },
});
const { id } = await createItem({
context,
listKey: 'User',
item: { name: 'test', other: 'bar' },
});
await updateItem({ context, listKey: 'User', item: { id, data: { name: 'something' } } });
expect(keystone._logFunction).toHaveBeenNthCalledWith(2, {
operation: 'update',
authedItem: { foo: 'bar' },
authedListKey: 'Other',
listKey: 'User',
originalInput: { name: 'something' },
changedItem: expect.objectContaining({ name: 'something' }),
});
})
);

test(
'Logging Hooks: delete',
runner(setupKeystone, async ({ keystone }) => {
const context = keystone.createContext({
authentication: { item: { foo: 'bar' }, listKey: 'Other' },
});
const { id } = await createItem({
context,
listKey: 'User',
item: { name: 'test', other: 'bar' },
});
await deleteItem({ context, listKey: 'User', itemId: id });
expect(keystone._logFunction).toHaveBeenNthCalledWith(2, {
operation: 'delete',
authedItem: { foo: 'bar' },
authedListKey: 'Other',
listKey: 'User',
deletedItem: expect.objectContaining({ name: 'test', other: 'bar' }),
});
})
);

test(
'Logging Hooks: auth',
runner(setupKeystone, async ({ keystone }) => {
const context = keystone.createContext({});
context.startAuthedSession = () => 't0k3n';
await createItem({
context,
listKey: 'User',
item: { name: 'test', password: 't3sting!' },
});
await runCustomQuery({
context,
query: `mutation m($name: String, $password: String) {
authenticateUserWithPassword(name: $name, password: $password) { token item { id } }
}`,
variables: { name: 'test', password: 't3sting!' },
});
expect(keystone._logFunction).toHaveBeenNthCalledWith(2, {
operation: 'authenticate',
authedItem: undefined,
authedListKey: undefined,
listKey: 'User',
item: expect.objectContaining({ name: 'test' }),
success: true,
message: 'Authentication successful',
token: 't0k3n',
});
})
);

test(
'Logging Hooks: unauth',
runner(setupKeystone, async ({ keystone }) => {
const context = keystone.createContext();
context.startAuthedSession = () => 't0k3n';
await createItem({
context,
listKey: 'User',
item: { name: 'test', password: 't3sting!' },
});
await runCustomQuery({
context,
query: `mutation m($name: String, $password: String) {
authenticateUserWithPassword(name: $name, password: $password) { token item { id } }
}`,
variables: { name: 'test', password: 't3sting!' },
});

const _context = keystone.createContext({
authentication: { item: { foo: 'bar' }, listKey: 'Other' },
});
_context.endAuthedSession = () => ({ success: true, listKey: 'Foo', itemId: 'X' });
await runCustomQuery({
context: _context,
query: `mutation {
unauthenticateUser{ success }
}`,
});

expect(keystone._logFunction).toHaveBeenNthCalledWith(3, {
operation: 'unauthenticate',
authedItem: { foo: 'bar' },
authedListKey: 'Other',
listKey: 'Foo',
itemId: 'X',
});
})
);
})
);
1 change: 1 addition & 0 deletions api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@keystonejs/auth-password": "^5.1.13",
"@keystonejs/fields": "^16.1.0",
"@keystonejs/list-plugins": "^7.0.5",
"@keystonejs/test-utils": "^8.0.0",
"@keystonejs/utils": "^5.4.2",
"date-fns": "^2.14.0",
Expand Down
94 changes: 94 additions & 0 deletions packages/list-plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,97 @@ keystone.createList('ListWithPlugin', {
],
});
```

# logging

This plugin provides a mechanism for logging all mutations in a Keystone system.

## Usage

```js
const { logging } = require('@keystonejs/list-plugins');

keystone.createList('ListWithPlugin', {
fields: {...},
plugins: [
logging(args => console.log(args),
],
});

keystone.createAuthStrategy({
type: PasswordAuthStrategy,
list: 'User',
plugins: [
logging(args => console.log(args),
]
})
```
## Provided hooks
The `logging` plugin will log the arguments of all mutations with the function `args => console.log(JSON.stringify(args))`.
You can customise its behaviour by providing an alternate logging function.
The plugin provides the following hooks:
- `afterChange`
- `afterDelete`
- `afterAuth`
- `afterUnauth`
The logging function for each hook recieves specific arguments related to the mutation.
### afterChange (create)
| Option | Type | Description |
| --------------- | -------- | ----------------------------------------- |
| `operation` | `String` | `"create"` |
| `authedItem` | `Object` | The currently authenticated item. |
| `authedListKey` | `String` | The list currently authenticated against. |
| `listKey` | `String` | The key of the list being operated on. |
| `originalInput` | `Object` | The original input to the mutation. |
| `createdItem` | `Object` | The database record of the created item. |
### afterChange (update)
| Option | Type | Description |
| --------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `operation` | `String` | `"update"` |
| `authedItem` | `Object` | The currently authenticated item. |
| `authedListKey` | `String` | The list currently authenticated against. |
| `listKey` | `String` | The key of the list being operated on. |
| `originalInput` | `Object` | The original input to the mutation. |
| `changedItem` | `Object` | The database record of the updated item. This will only include those fields which have actually been changed. |
### afterDelete
| Option | Type | Description |
| --------------- | -------- | ----------------------------------------- |
| `operation` | `String` | `"delete"` |
| `authedItem` | `Object` | The currently authenticated item. |
| `authedListKey` | `String` | The list currently authenticated against. |
| `listKey` | `String` | The key of the list being operated on. |
| `deletedItem` | `Object` | The database record of the deleted item. |
### afterAuth
| Option | Type | Description |
| --------------- | --------- | ------------------------------------------------------- |
| `operation` | `String` | `"authenticate"` |
| `authedItem` | `Object` | The currently authenticated item. |
| `authedListKey` | `String` | The list currently authenticated against. |
| `listKey` | `String` | The key of the list being operated on. |
| `item` | `Object` | The authenticated item |
| `success` | `Boolean` | A success indicator returned by authentication strategy |
| `message` | `String` | A success message returned by authentication strategy |
| `token` | `String` | The token returned by authentication strategy |
### afterUnauth
| Option | Type | Description |
| --------------- | -------- | ----------------------------------------- |
| `operation` | `String` | `"unauthenticate"` |
| `authedItem` | `Object` | The currently authenticated item. |
| `authedListKey` | `String` | The list currently authenticated against. |
| `listKey` | `String` | The key of the list being operated on. |
| `itemId` | `String` | The `ID` of the unauthenticated item |
2 changes: 2 additions & 0 deletions packages/list-plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { atTracking, createdAt, updatedAt } = require('./lib/tracking/atTracking');
const { byTracking, createdBy, updatedBy } = require('./lib/tracking/byTracking');
const { singleton } = require('./lib/limiting/singleton');
const { logging } = require('./lib/logging');

module.exports = {
atTracking,
Expand All @@ -10,4 +11,5 @@ module.exports = {
createdBy,
updatedBy,
singleton,
logging,
};
57 changes: 57 additions & 0 deletions packages/list-plugins/lib/logging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const logging = (loggingFn = s => console.log(JSON.stringify(s))) => ({ hooks = {}, ...rest }) => ({
hooks: {
...hooks,
afterChange: async args => {
if (hooks.afterChange) {
await hooks.afterChange(args);
}
const { operation, existingItem, originalInput, updatedItem, context, listKey } = args;
const { authedItem, authedListKey } = context;
if (operation === 'create') {
loggingFn({
operation,
authedItem,
authedListKey,
originalInput,
listKey,
createdItem: updatedItem,
});
} else if (operation === 'update') {
const changedItem = Object.entries(updatedItem)
.filter(([key, value]) => key === 'id' || value !== existingItem[key])
.reduce((acc, [k, v]) => {
acc[k] = v;
return acc;
}, {});
loggingFn({ operation, authedItem, authedListKey, originalInput, listKey, changedItem });
}
},
afterDelete: async args => {
if (hooks.afterDelete) {
await hooks.afterDelete(args);
}
const { operation, existingItem, context, listKey } = args;
const { authedItem, authedListKey } = context;
loggingFn({ operation, authedItem, authedListKey, listKey, deletedItem: existingItem });
},
afterAuth: async args => {
if (hooks.afterAuth) {
await hooks.afterAuth(args);
}
const { operation, item, success, message, token, context, listKey } = args;
const { authedItem, authedListKey } = context;
loggingFn({ operation, authedItem, authedListKey, item, success, message, token, listKey });
},
afterUnauth: async args => {
if (hooks.afterAuth) {
await hooks.afterAuth(args);
}
const { operation, context, listKey, itemId } = args;
const { authedItem, authedListKey } = context;
loggingFn({ operation, authedItem, authedListKey, listKey, itemId });
},
},
...rest,
});

module.exports = { logging };

0 comments on commit 84116f7

Please sign in to comment.