diff --git a/.changeset/beige-dragons-jump.md b/.changeset/beige-dragons-jump.md
new file mode 100644
index 00000000000..7df11bd3089
--- /dev/null
+++ b/.changeset/beige-dragons-jump.md
@@ -0,0 +1,7 @@
+---
+'@keystonejs/list-plugins': patch
+---
+
+Tweaked hooks and utility function.
+* Renamed `composeResolveInput` utility function to `composeHook` to indicate right use by name, this can also be used in other hook type and not just `resolveInput` hook.
+* Switch to use of `operation` param to hook for detecting if this is `create` or `update` operation instead of existingItem being `undefined`.
\ No newline at end of file
diff --git a/.changeset/dirty-cougars-smoke.md b/.changeset/dirty-cougars-smoke.md
new file mode 100644
index 00000000000..2934da7147d
--- /dev/null
+++ b/.changeset/dirty-cougars-smoke.md
@@ -0,0 +1,5 @@
+---
+'@keystonejs/app-admin-ui': patch
+---
+
+Converted ResizeHandler and ScrollQuery components to custom hooks.
diff --git a/.changeset/eighty-boxes-remember.md b/.changeset/eighty-boxes-remember.md
new file mode 100644
index 00000000000..447e58be53a
--- /dev/null
+++ b/.changeset/eighty-boxes-remember.md
@@ -0,0 +1,6 @@
+---
+'@keystonejs/keystone': patch
+'@keystonejs/utils': patch
+---
+
+Converted some stray promise chains to async/await.
diff --git a/.changeset/long-adults-travel.md b/.changeset/long-adults-travel.md
new file mode 100644
index 00000000000..128894971d5
--- /dev/null
+++ b/.changeset/long-adults-travel.md
@@ -0,0 +1,10 @@
+---
+'@keystonejs/cypress-project-access-control': patch
+'@keystonejs/cypress-project-basic': patch
+'@keystonejs/cypress-project-client-validation': patch
+'@keystonejs/cypress-project-login': patch
+'@keystonejs/keystone': major
+'@keystonejs/session': major
+---
+
+The `cookieSecret` option no longer defaults to a static value. It is now required in production mode. In development mode, if undefined, a random new value is generated each time the server is started.
diff --git a/.changeset/tasty-numbers-turn.md b/.changeset/tasty-numbers-turn.md
new file mode 100644
index 00000000000..d98a611eaa1
--- /dev/null
+++ b/.changeset/tasty-numbers-turn.md
@@ -0,0 +1,5 @@
+---
+'@keystonejs/app-admin-ui': major
+---
+
+Removed the method `AdminUIApp.getAdminMeta()` in favour of the more complete `AdminUIApp.getAdminUIMeta(keystone)`.
diff --git a/packages/app-admin-ui/client/components/Nav/ResizeHandler.js b/packages/app-admin-ui/client/components/Nav/ResizeHandler.js
index 813fb2a49d2..08a60cca696 100644
--- a/packages/app-admin-ui/client/components/Nav/ResizeHandler.js
+++ b/packages/app-admin-ui/client/components/Nav/ResizeHandler.js
@@ -1,115 +1,128 @@
-import { Component } from 'react';
+import { useState, useRef, useEffect } from 'react';
import raf from 'raf-schd';
-import { withKeyboardConsumer } from '../KeyboardShortcuts';
+import { useKeyboardManager } from '../KeyboardShortcuts';
const LS_KEY = 'KEYSTONE_NAVIGATION_STATE';
const DEFAULT_STATE = { isCollapsed: false, width: 280 };
const MIN_WIDTH = 140;
const MAX_WIDTH = 800;
+
export const KEYBOARD_SHORTCUT = '[';
-function getCache() {
- if (typeof localStorage !== 'undefined') {
+const getCache = () => {
+ if (localStorage !== undefined) {
const stored = localStorage.getItem(LS_KEY);
- return stored ? JSON.parse(stored) : DEFAULT_STATE;
+
+ if (stored) {
+ return JSON.parse(stored);
+ }
}
+
return DEFAULT_STATE;
-}
-function setCache(state) {
- if (typeof localStorage !== 'undefined') {
+};
+
+const setCache = state => {
+ if (localStorage !== undefined) {
localStorage.setItem(LS_KEY, JSON.stringify(state));
}
-}
+};
-class ResizeHandler extends Component {
- state = getCache();
+export const useResizeHandler = () => {
+ // TODO: should we be calling this in the function body?
+ const { width: cachedWidth, isCollapsed: cachedIsCollapsed } = getCache();
- componentDidMount() {
- this.props.keyManager.subscribe(KEYBOARD_SHORTCUT, this.toggleCollapse);
- }
- componentWillUnmount() {
- this.props.keyManager.unsubscribe(KEYBOARD_SHORTCUT);
- }
+ // These should trigger renders
+ const [width, setWidth] = useState(cachedWidth);
+ const [isCollapsed, setIsCollapsed] = useState(cachedIsCollapsed);
+ const [isMouseDown, setIsMouseDown] = useState(false);
+ const [isDragging, setIsDragging] = useState(false);
- storeState = s => {
- // only keep the `isCollapsed` and `width` properties in locals storage
- const isCollapsed = s.isCollapsed !== undefined ? s.isCollapsed : this.state.isCollapsed;
- const width = s.width !== undefined ? s.width : this.state.width;
+ // Internal state tracking
+ const initialX = useRef();
+ const initialWidth = useRef();
- setCache({ isCollapsed, width });
+ const { addBinding, removeBinding } = useKeyboardManager();
- this.setState(s);
- };
+ useEffect(() => {
+ addBinding(KEYBOARD_SHORTCUT, toggleCollapse);
+ return () => {
+ removeBinding(KEYBOARD_SHORTCUT);
+ };
+ }, []);
+
+ useEffect(() => {
+ const handleResize = raf(event => {
+ // on occasion a mouse move event occurs before the event listeners have a chance to detach
+ if (!isMouseDown) return;
+
+ // initialize dragging
+ if (!isDragging) {
+ setIsDragging(true);
+ initialWidth.current = width;
+ return;
+ }
+
+ // allow the product nav to be 75% of the available page width
+ const adjustedMax = MAX_WIDTH - initialWidth.current;
+ const adjustedMin = MIN_WIDTH - initialWidth.current;
+
+ const newDelta = Math.max(Math.min(event.pageX - initialX.current, adjustedMax), adjustedMin);
+ const newWidth = initialWidth.current + newDelta;
+
+ setWidth(newWidth);
+ });
+
+ const handleResizeEnd = () => {
+ // reset non-width states
+ setIsDragging(false);
+ setIsMouseDown(false);
+ };
+
+ window.addEventListener('mousemove', handleResize, { passive: true });
+ window.addEventListener('mouseup', handleResizeEnd, { passive: true });
+
+ return () => {
+ window.removeEventListener('mousemove', handleResize, { passive: true });
+ window.removeEventListener('mouseup', handleResizeEnd, { passive: true });
+ };
+ }, [isMouseDown, isDragging]);
+
+ // Only keep the `isCollapsed` and `width` properties in locals storage
+ useEffect(() => {
+ setCache({ isCollapsed, width });
+ }, [isCollapsed, width]);
- handleResizeStart = (event: MouseEvent) => {
+ const handleResizeStart = event => {
// bail if not "left click"
if (event.button && event.button > 0) return;
// initialize resize gesture
- this.setState({ initialX: event.pageX, mouseIsDown: true });
-
- // attach handlers (handleResizeStart is a bound to onMouseDown)
- window.addEventListener('mousemove', this.handleResize);
- window.addEventListener('mouseup', this.handleResizeEnd);
+ initialX.current = event.pageX;
+ setIsMouseDown(true);
};
- initializeDrag = () => {
- let initialWidth = this.state.width;
-
- this.setState({ initialWidth, isDragging: true });
+ const toggleCollapse = () => {
+ setIsCollapsed(prevCollapsed => !prevCollapsed);
};
- handleResize = raf((event: MouseEvent) => {
- const { initialX, initialWidth, isDragging, mouseIsDown } = this.state;
-
- // on occasion a mouse move event occurs before the event listeners
- // have a chance to detach
- if (!mouseIsDown) return;
-
- // initialize dragging
- if (!isDragging) {
- this.initializeDrag(event);
- return;
- }
-
- // allow the product nav to be 75% of the available page width
- const adjustedMax = MAX_WIDTH - initialWidth;
- const adjustedMin = MIN_WIDTH - initialWidth;
-
- const delta = Math.max(Math.min(event.pageX - initialX, adjustedMax), adjustedMin);
- const width = initialWidth + delta;
-
- this.setState({ delta, width });
- });
- handleResizeEnd = () => {
- // reset non-width states
- this.setState({ delta: 0, isDragging: false, mouseIsDown: false });
-
- // store the width
- this.storeState({ width: this.state.width });
-
- // cleanup
- window.removeEventListener('mousemove', this.handleResize);
- window.removeEventListener('mouseup', this.handleResizeEnd);
- };
- toggleCollapse = () => {
- const isCollapsed = !this.state.isCollapsed;
- this.storeState({ isCollapsed });
+ const resizeProps = {
+ title: 'Drag to Resize',
+ onMouseDown: handleResizeStart,
};
- render() {
- const resizeProps = {
- title: 'Drag to Resize',
- onMouseDown: this.handleResizeStart,
- };
- const clickProps = {
- onClick: this.toggleCollapse,
- };
- const snapshot = this.state;
+ const clickProps = {
+ onClick: toggleCollapse,
+ };
- return this.props.children(resizeProps, clickProps, snapshot);
- }
-}
+ const snapshot = {
+ width,
+ isCollapsed,
+ isMouseDown,
+ isDragging,
+ initialX: initialX.current,
+ initialWidth: initialWidth.current,
+ };
-export default withKeyboardConsumer(ResizeHandler);
+ return { resizeProps, clickProps, snapshot };
+};
diff --git a/packages/app-admin-ui/client/components/Nav/index.js b/packages/app-admin-ui/client/components/Nav/index.js
index 0747cc72236..33bea20c478 100644
--- a/packages/app-admin-ui/client/components/Nav/index.js
+++ b/packages/app-admin-ui/client/components/Nav/index.js
@@ -23,8 +23,8 @@ import { FlexGroup } from '@arch-ui/layout';
import { PersonIcon, SignOutIcon, TerminalIcon, MarkGithubIcon } from '@arch-ui/icons';
import { useAdminMeta } from '../../providers/AdminMeta';
-import ResizeHandler, { KEYBOARD_SHORTCUT } from './ResizeHandler';
-import ScrollQuery from '../ScrollQuery';
+import { useResizeHandler, KEYBOARD_SHORTCUT } from './ResizeHandler';
+import { useScrollQuery } from '../ScrollQuery';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
@@ -244,6 +244,7 @@ function PrimaryNavItems({
mouseIsOverNav,
}) {
const isAtDashboard = useRouteMatch({ path: adminPath, exact: true });
+ const [scrollRef, snapshot] = useScrollQuery({ isPassive: false });
let hasRenderedIndexPage = false;
const onRenderIndexPage = () => {
@@ -278,23 +279,15 @@ function PrimaryNavItems({
);
return (
-
- {(ref, snapshot) => (
-
- {hasRenderedIndexPage === false && (
-
- Dashboard
-
- )}
-
- {pageNavItems}
-
+
+ {hasRenderedIndexPage === false && (
+
+ Dashboard
+
)}
-
+
+ {pageNavItems}
+
);
}
@@ -479,81 +472,81 @@ const Nav = ({ children }) => {
setMouseIsOverNav(false);
};
- return (
-
- {(resizeProps, clickProps, { isCollapsed, isDragging, width }) => {
- const navWidth = isCollapsed ? 0 : width;
- const makeResizeStyles = key => {
- const pointers = isDragging ? { pointerEvents: 'none' } : null;
- const transitions = isDragging
- ? null
- : {
- transition: `${camelToKebab(key)} ${TRANSITION_DURATION} ${TRANSITION_EASING}`,
- };
- return { [key]: navWidth, ...pointers, ...transitions };
+ const {
+ resizeProps,
+ clickProps,
+ snapshot: { isCollapsed, isDragging, width },
+ } = useResizeHandler();
+
+ const navWidth = isCollapsed ? 0 : width;
+ const makeResizeStyles = key => {
+ const pointers = isDragging ? { pointerEvents: 'none' } : null;
+ const transitions = isDragging
+ ? null
+ : {
+ transition: `${camelToKebab(key)} ${TRANSITION_DURATION} ${TRANSITION_EASING}`,
};
+ return { [key]: navWidth, ...pointers, ...transitions };
+ };
- return (
-
-
-
+
+
+
+ {isCollapsed ? null : (
+
+ )}
+
+ {isCollapsed ? 'Click to Expand' : 'Click to Collapse'}
+
+ }
+ placement="right"
+ hideOnMouseDown
+ hideOnKeyDown
+ delay={600}
+ >
+ {ref => (
+
-
- {isCollapsed ? null : (
-
- )}
-
- {isCollapsed ? 'Click to Expand' : 'Click to Collapse'}
-
- }
- placement="right"
- hideOnMouseDown
- hideOnKeyDown
- delay={600}
+
-
- {children}
-
- );
- }}
-
+
+
+
+ )}
+
+
+ {children}
+
);
};
diff --git a/packages/app-admin-ui/client/components/ScrollQuery.js b/packages/app-admin-ui/client/components/ScrollQuery.js
index 2f55e8f9a9c..c280755c114 100644
--- a/packages/app-admin-ui/client/components/ScrollQuery.js
+++ b/packages/app-admin-ui/client/components/ScrollQuery.js
@@ -1,76 +1,69 @@
-import { createRef, Component } from 'react';
-import PropTypes from 'prop-types';
+import { useState, useEffect, useRef, useCallback } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import raf from 'raf-schd';
const LISTENER_OPTIONS = { passive: true };
-export default class ScrollQuery extends Component {
- scrollElement = createRef();
- state = { hasScroll: false, isScrollable: false, scrollTop: 0 };
- static propTypes = {
- children: PropTypes.func,
- isPassive: PropTypes.bool,
- };
- static defaultProps = {
- isPassive: true,
- };
+export const useScrollQuery = ({ isPassive = true }) => {
+ const scrollElement = useRef();
+ const resizeObserver = useRef();
- componentDidMount() {
- const { isPassive } = this.props;
- const scrollEl = this.scrollElement.current;
+ const [snapshot, setSnapshot] = useState({});
- if (!isPassive) {
- scrollEl.addEventListener('scroll', this.handleScroll, LISTENER_OPTIONS);
- }
+ const setScroll = useCallback(target => {
+ const { clientHeight, scrollHeight, scrollTop } = target;
+
+ const isBottom = scrollTop === scrollHeight - clientHeight;
+ const isTop = scrollTop === 0;
+ const isScrollable = scrollHeight > clientHeight;
+ const hasScroll = !!scrollTop;
- this.resizeObserver = new ResizeObserver(([entry]) => {
- this.setScroll(entry.target);
+ setSnapshot({
+ isBottom,
+ isTop,
+ isScrollable,
+ scrollHeight,
+ scrollTop,
+ hasScroll,
});
- this.resizeObserver.observe(scrollEl);
+ }, []);
- this.setScroll(scrollEl);
- }
- componentWillUnmount() {
- const { isPassive } = this.props;
+ useEffect(() => {
+ const { current } = scrollElement;
- if (!isPassive) {
- this.scrollElement.current.removeEventListener('scroll', this.handleScroll, LISTENER_OPTIONS);
- }
+ if (!isPassive && current) {
+ const handleScroll = raf(event => {
+ setScroll(event.target);
+ });
- if (this.resizeObserver && this.scrollElement.current) {
- this.resizeObserver.disconnect(this.scrollElement.current);
+ current.addEventListener('scroll', handleScroll, LISTENER_OPTIONS);
+ return () => {
+ current.removeEventListener('scroll', handleScroll, LISTENER_OPTIONS);
+ };
}
- this.resizeObserver = null;
- }
+ }, [isPassive]);
- handleScroll = raf(event => {
- this.setScroll(event.target);
- });
+ // Not using useResizeObserver since we want to operate with the element on resize, not the dimensions
+ useEffect(() => {
+ const { current } = scrollElement;
- setScroll = target => {
- const { clientHeight, scrollHeight, scrollTop } = target;
- const isScrollable = scrollHeight > clientHeight;
- const isBottom = scrollTop === scrollHeight - clientHeight;
- const isTop = scrollTop === 0;
- const hasScroll = !!scrollTop;
- if (
- // we only need to compare some parts of state
- // because some of the parts are computed from scrollTop
- this.state.isBottom !== isBottom ||
- this.state.isScrollable !== isScrollable ||
- this.state.scrollHeight !== scrollHeight ||
- this.state.scrollTop !== scrollTop
- ) {
- this.setState({ isBottom, isTop, isScrollable, scrollHeight, scrollTop, hasScroll });
- }
- };
+ resizeObserver.current = new ResizeObserver(
+ raf(([entry]) => {
+ setScroll(entry.target);
+ })
+ );
+
+ resizeObserver.current.observe(current);
+ setScroll(current);
+
+ return () => {
+ if (resizeObserver.current && current) {
+ resizeObserver.current.disconnect(current);
+ }
- render() {
- const { children, render } = this.props;
- const ref = this.scrollElement;
- const snapshot = this.state;
+ resizeObserver.current = null;
+ };
+ }, []);
- return render ? render(ref, snapshot) : children(ref, snapshot);
- }
-}
+ return [scrollElement, snapshot];
+};
diff --git a/packages/app-admin-ui/index.js b/packages/app-admin-ui/index.js
index f7cf1b85ca3..795ad6c76d3 100644
--- a/packages/app-admin-ui/index.js
+++ b/packages/app-admin-ui/index.js
@@ -47,21 +47,6 @@ class AdminUIApp {
};
}
- getAdminMeta() {
- return {
- adminPath: this.adminPath,
- pages: this.pages,
- hooks: this.hooks,
- ...this.routes,
- ...(this.authStrategy
- ? {
- authStrategy: this.authStrategy.getAdminMeta(),
- }
- : {}),
- ...this._adminMeta,
- };
- }
-
isAccessAllowed(req) {
if (!this.authStrategy) {
return true;
@@ -134,14 +119,23 @@ class AdminUIApp {
}
getAdminUIMeta(keystone) {
- const { adminPath } = this;
-
+ // This is exposed as the global `KEYSTONE_ADMIN_META` in the client.
+ const { adminPath, apiPath, graphiqlPath, pages, hooks } = this;
+ const { signinPath, signoutPath } = this.routes;
+ const { lists, name } = keystone.getAdminMeta({ schemaName: this._schemaName });
+ const authStrategy = this.authStrategy ? this.authStrategy.getAdminMeta() : undefined;
return {
adminPath,
- apiPath: this.apiPath,
- graphiqlPath: this.graphiqlPath,
- ...this.getAdminMeta(),
- ...keystone.getAdminMeta({ schemaName: this._schemaName }),
+ apiPath,
+ graphiqlPath,
+ pages,
+ hooks,
+ signinPath,
+ signoutPath,
+ authStrategy,
+ lists,
+ name,
+ ...this._adminMeta,
};
}
diff --git a/packages/app-admin-ui/tests/server/AdminUI.test.js b/packages/app-admin-ui/tests/server/AdminUI.test.js
index d1b96c085a0..b927670b689 100644
--- a/packages/app-admin-ui/tests/server/AdminUI.test.js
+++ b/packages/app-admin-ui/tests/server/AdminUI.test.js
@@ -21,7 +21,7 @@ jest.doMock('html-webpack-plugin', () => {
const { AdminUIApp } = require('../../');
const keystone = {
- getAdminMeta: jest.fn(),
+ getAdminMeta: jest.fn(() => ({ lists: [], name: 'test' })),
};
const adminPath = 'admin_path';
diff --git a/packages/keystone/README.md b/packages/keystone/README.md
index 70401a434c3..c39bda89b78 100644
--- a/packages/keystone/README.md
+++ b/packages/keystone/README.md
@@ -24,7 +24,7 @@ const keystone = new Keystone({
| `adapters` | `Object` | `undefined` | A list of named database adapters. Use the format `{ name: adapterObject }`. |
| `appVersion` | `Object` | See [`appVersion`](#appversion) | Configure the application version and where it is made available. |
| `cookie` | `Object` | See: [`cookie`](#cookie) | Cookie object used to configure the [express-session middleware](https://github.com/expressjs/session#cookie). |
-| `cookieSecret` | `String` | `qwerty` | The secret used to sign session ID cookies. Should be long and unguessable. Don't use this default in production! |
+| `cookieSecret` | `String` | Required in production | The secret used to sign session ID cookies. Should be long and unguessable. |
| `defaultAccess` | `Object` | `undefined` | Default list, field, and custom schema access. See the [Access control API](https://www.keystonejs.com/api/access-control) docs for more details. |
| `defaultAdapter` | `String` | `undefined` | The name of the database adapter to use by default if multiple are provided. |
| `name` | `String` | `undefined` | The name of the project. Appears in the Admin UI. |
@@ -99,6 +99,10 @@ const keystone = new Keystone({
});
```
+### `cookieSecret`
+
+The secret used to sign session ID cookies. In production mode (`process.env.NODE_ENV === 'production'`) this option is required. In development mode, if undefined, a random `cookieSecret` will be generated each time Keystone starts (this will cause sessions to be reset between restarts).
+
### `sessionStore`
Sets the Express server's [session middleware](https://github.com/expressjs/session). This should be configured before deploying your app.
diff --git a/packages/keystone/lib/Keystone/index.js b/packages/keystone/lib/Keystone/index.js
index 485d6faeeb9..4802d154610 100644
--- a/packages/keystone/lib/Keystone/index.js
+++ b/packages/keystone/lib/Keystone/index.js
@@ -42,7 +42,7 @@ module.exports = class Keystone {
defaultAdapter,
name,
onConnect,
- cookieSecret = 'qwerty',
+ cookieSecret,
sessionStore,
queryLimits = {},
cookie = {
@@ -453,27 +453,20 @@ module.exports = class Keystone {
/**
* @return Promise
*/
- connect() {
+ async connect() {
const { adapters, name } = this;
const rels = this._consolidateRelationships();
- return resolveAllKeys(mapKeys(adapters, adapter => adapter.connect({ name, rels }))).then(
- () => {
- if (this.eventHandlers.onConnect) {
- return this.eventHandlers.onConnect(this);
- }
- }
- );
+ await resolveAllKeys(mapKeys(adapters, adapter => adapter.connect({ name, rels })));
+ if (this.eventHandlers.onConnect) {
+ return this.eventHandlers.onConnect(this);
+ }
}
/**
* @return Promise
*/
- disconnect() {
- return resolveAllKeys(
- mapKeys(this.adapters, adapter => adapter.disconnect())
- // Chain an empty function so that the result of this promise
- // isn't unintentionally leaked to the caller
- ).then(() => {});
+ async disconnect() {
+ await resolveAllKeys(mapKeys(this.adapters, adapter => adapter.disconnect()));
}
getAdminMeta({ schemaName }) {
diff --git a/packages/keystone/lib/List/index.js b/packages/keystone/lib/List/index.js
index 344fa315691..a0c13431d1c 100644
--- a/packages/keystone/lib/List/index.js
+++ b/packages/keystone/lib/List/index.js
@@ -826,15 +826,17 @@ module.exports = class List {
// Return these as functions so they're lazily evaluated depending
// on what the user requested
// Evaluation takes place in ../Keystone/index.js
- getCount: () => {
+ getCount: async () => {
const access = this.checkListAccess(context, undefined, 'read', { gqlName });
- return this._itemsQuery(mergeWhereClause(args, access), {
+ const { count } = await this._itemsQuery(mergeWhereClause(args, access), {
meta: true,
context,
info,
from,
- }).then(({ count }) => count);
+ });
+
+ return count;
},
};
}
diff --git a/packages/list-plugins/lib/tracking/atTracking.js b/packages/list-plugins/lib/tracking/atTracking.js
index df54eaa7a12..f54bfa5b608 100644
--- a/packages/list-plugins/lib/tracking/atTracking.js
+++ b/packages/list-plugins/lib/tracking/atTracking.js
@@ -1,5 +1,5 @@
const { DateTime } = require('@keystonejs/fields');
-const { composeResolveInput } = require('../utils');
+const { composeHook } = require('../utils');
const _atTracking = ({ created = true, updated = true }) => ({
updatedAtField = 'updatedAt',
@@ -29,9 +29,9 @@ const _atTracking = ({ created = true, updated = true }) => ({
};
}
- const newResolveInput = ({ resolvedData, existingItem, originalInput }) => {
+ const newResolveInput = ({ resolvedData, operation, originalInput }) => {
const dateNow = new Date().toISOString();
- if (existingItem === undefined) {
+ if (operation === 'create') {
// create mode
if (created) {
resolvedData[createdAtField] = dateNow;
@@ -39,7 +39,8 @@ const _atTracking = ({ created = true, updated = true }) => ({
if (updated) {
resolvedData[updatedAtField] = dateNow;
}
- } else {
+ }
+ if (operation === 'update') {
// update mode
// if no data received from the mutation, skip the update
@@ -57,7 +58,7 @@ const _atTracking = ({ created = true, updated = true }) => ({
return resolvedData;
};
const originalResolveInput = hooks.resolveInput;
- hooks.resolveInput = composeResolveInput(originalResolveInput, newResolveInput);
+ hooks.resolveInput = composeHook(originalResolveInput, newResolveInput);
return { fields, hooks, ...rest };
};
diff --git a/packages/list-plugins/lib/tracking/byTracking.js b/packages/list-plugins/lib/tracking/byTracking.js
index 30bb54e5e11..dbcd7e8fe96 100644
--- a/packages/list-plugins/lib/tracking/byTracking.js
+++ b/packages/list-plugins/lib/tracking/byTracking.js
@@ -1,5 +1,5 @@
const { AuthedRelationship } = require('@keystonejs/fields-authed-relationship');
-const { composeResolveInput } = require('../utils');
+const { composeHook } = require('../utils');
const _byTracking = ({ created = true, updated = true }) => ({
updatedByField = 'updatedBy',
@@ -24,14 +24,14 @@ const _byTracking = ({ created = true, updated = true }) => ({
};
}
- const newResolveInput = ({ resolvedData, existingItem, originalInput, context }) => {
+ const newResolveInput = ({ resolvedData, operation, originalInput, context }) => {
if (
// if no data received from the mutation, skip the update
Object.keys(originalInput).length === 0 &&
// opted-in to updatedBy tracking
updated &&
// this is an update
- existingItem !== undefined
+ operation === 'update'
) {
// If not logged in, the id is set to `null`
const { authedItem: { id = null } = {} } = context;
@@ -41,7 +41,7 @@ const _byTracking = ({ created = true, updated = true }) => ({
return resolvedData;
};
const originalResolveInput = hooks.resolveInput;
- hooks.resolveInput = composeResolveInput(originalResolveInput, newResolveInput);
+ hooks.resolveInput = composeHook(originalResolveInput, newResolveInput);
return { fields, hooks, ...rest };
};
diff --git a/packages/list-plugins/lib/utils.js b/packages/list-plugins/lib/utils.js
index 89c252d2771..f27958d8f4a 100644
--- a/packages/list-plugins/lib/utils.js
+++ b/packages/list-plugins/lib/utils.js
@@ -1,4 +1,4 @@
-exports.composeResolveInput = (originalHook, newHook) => async params => {
+exports.composeHook = (originalHook, newHook) => async params => {
let { resolvedData } = params;
if (originalHook) {
resolvedData = await originalHook(params);
diff --git a/packages/session/lib/session.js b/packages/session/lib/session.js
index eca1df01f94..c88fb43d2de 100644
--- a/packages/session/lib/session.js
+++ b/packages/session/lib/session.js
@@ -3,7 +3,21 @@ const expressSession = require('express-session');
const cookie = require('cookie');
class SessionManager {
- constructor({ cookieSecret = 'qwerty', cookie, sessionStore }) {
+ constructor({ cookieSecret, cookie, sessionStore }) {
+ if (!cookieSecret) {
+ if (process.env.NODE_ENV === 'production') {
+ throw new Error(
+ 'The cookieSecret config option is required when running Keystone in a production environment. Update your app or environment config so this value is supplied to the Keystone constructor. See [https://www.keystonejs.com/keystonejs/keystone/#config] for details.'
+ );
+ } else {
+ console.warn(
+ 'No cookieSecret value was provided. Please generate a secure value and add it to your app. Until this is done, a random cookieSecret will be generated each time Keystone is started. This will cause sessions to be reset between restarts. See [https://www.keystonejs.com/keystonejs/keystone/#config] for details.'
+ );
+
+ cookieSecret = [...Array(30)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
+ }
+ }
+
this._cookieSecret = cookieSecret;
this._cookie = cookie;
this._sessionStore = sessionStore;
diff --git a/packages/utils/src/index.js b/packages/utils/src/index.js
index 5794cde7398..8e0636fe815 100644
--- a/packages/utils/src/index.js
+++ b/packages/utils/src/index.js
@@ -22,7 +22,7 @@ export const mapKeyNames = (obj, func) =>
{}
);
-export const resolveAllKeys = obj => {
+export const resolveAllKeys = async obj => {
const returnValue = {};
const errors = {};
@@ -38,18 +38,19 @@ export const resolveAllKeys = obj => {
})
);
- return Promise.all(allPromises).then(results => {
- // If there are any errors, we want to surface them in the same shape as the
- // input object
- if (Object.keys(errors).length) {
- const firstError = results.find(({ isRejected }) => isRejected).reason;
- // Use the first error as the message so it's at least meaningful
- const error = new Error(firstError.message || firstError.toString());
- error.errors = errors;
- throw error;
- }
- return returnValue;
- });
+ const results = await Promise.all(allPromises);
+
+ // If there are any errors, we want to surface them in the same shape as the
+ // input object
+ if (Object.keys(errors).length) {
+ const firstError = results.find(({ isRejected }) => isRejected).reason;
+ // Use the first error as the message so it's at least meaningful
+ const error = new Error(firstError.message || firstError.toString());
+ error.errors = errors;
+ throw error;
+ }
+
+ return returnValue;
};
export const unique = arr => [...new Set(arr)];
diff --git a/test-projects/access-control/index.js b/test-projects/access-control/index.js
index 78700698ab7..53f1b9f1453 100644
--- a/test-projects/access-control/index.js
+++ b/test-projects/access-control/index.js
@@ -20,6 +20,7 @@ const { MongooseAdapter } = require('@keystonejs/adapter-mongoose');
const keystone = new Keystone({
name: projectName,
adapter: new MongooseAdapter(),
+ cookieSecret: 'qwerty',
});
keystone.createList('User', {
diff --git a/test-projects/basic/index.js b/test-projects/basic/index.js
index 46c76b870af..af0404b1bb2 100644
--- a/test-projects/basic/index.js
+++ b/test-projects/basic/index.js
@@ -43,6 +43,7 @@ const { MongooseAdapter } = require('@keystonejs/adapter-mongoose');
const keystone = new Keystone({
name: 'Cypress Test Project Basic',
adapter: new MongooseAdapter(),
+ cookieSecret: 'qwerty',
});
const fileAdapter = new LocalFileAdapter({
diff --git a/test-projects/client-validation/index.js b/test-projects/client-validation/index.js
index 9f7177e1f1c..746b549f64b 100644
--- a/test-projects/client-validation/index.js
+++ b/test-projects/client-validation/index.js
@@ -11,6 +11,7 @@ const { MongooseAdapter } = require('@keystonejs/adapter-mongoose');
const keystone = new Keystone({
name: 'Cypress Test Project Client Validation',
adapter: new MongooseAdapter(),
+ cookieSecret: 'qwerty',
});
keystone.createList('User', {
diff --git a/test-projects/login/index.js b/test-projects/login/index.js
index a547628e9ab..de804016a20 100644
--- a/test-projects/login/index.js
+++ b/test-projects/login/index.js
@@ -19,6 +19,7 @@ const defaultAccess = ({ authentication: { item } }) => !!item;
const keystone = new Keystone({
name: 'Cypress Test Project For Login',
adapter: new MongooseAdapter(),
+ cookieSecret: 'qwerty',
defaultAccess: {
list: defaultAccess,
},