From 929ca380bbc6bf7b52bc16cafe62b4854eae8859 Mon Sep 17 00:00:00 2001 From: Vladislav Shkodin Date: Sun, 13 Dec 2020 13:35:07 +0200 Subject: [PATCH] refactor(typings): migrate 'auth' state slice to TypeScript (#4698) --- .../src/actions/{auth.js => auth.ts} | 45 ++++++++++++------- .../src/reducers/__tests__/auth.spec.js | 33 -------------- .../src/reducers/__tests__/auth.spec.ts | 32 +++++++++++++ .../netlify-cms-core/src/reducers/auth.js | 21 --------- .../netlify-cms-core/src/reducers/auth.ts | 42 +++++++++++++++++ packages/netlify-cms-core/src/types/redux.ts | 2 + 6 files changed, 104 insertions(+), 71 deletions(-) rename packages/netlify-cms-core/src/actions/{auth.js => auth.ts} (69%) delete mode 100644 packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js create mode 100644 packages/netlify-cms-core/src/reducers/__tests__/auth.spec.ts delete mode 100644 packages/netlify-cms-core/src/reducers/auth.js create mode 100644 packages/netlify-cms-core/src/reducers/auth.ts diff --git a/packages/netlify-cms-core/src/actions/auth.js b/packages/netlify-cms-core/src/actions/auth.ts similarity index 69% rename from packages/netlify-cms-core/src/actions/auth.js rename to packages/netlify-cms-core/src/actions/auth.ts index b6c1e201..0ba23cc6 100644 --- a/packages/netlify-cms-core/src/actions/auth.js +++ b/packages/netlify-cms-core/src/actions/auth.ts @@ -1,5 +1,9 @@ import { actions as notifActions } from 'redux-notifications'; -import { currentBackend } from 'coreSrc/backend'; +import { Credentials, User } from 'netlify-cms-lib-util'; +import { ThunkDispatch } from 'redux-thunk'; +import { AnyAction } from 'redux'; +import { currentBackend } from '../backend'; +import { State } from '../types/redux'; const { notifSend, notifClear } = notifActions; @@ -13,50 +17,49 @@ export const LOGOUT = 'LOGOUT'; export function authenticating() { return { type: AUTH_REQUEST, - }; + } as const; } -export function authenticate(userData) { +export function authenticate(userData: User) { return { type: AUTH_SUCCESS, payload: userData, - }; + } as const; } -export function authError(error) { +export function authError(error: Error) { return { type: AUTH_FAILURE, error: 'Failed to authenticate', payload: error, - }; + } as const; } export function doneAuthenticating() { return { type: AUTH_REQUEST_DONE, - }; + } as const; } export function useOpenAuthoring() { return { type: USE_OPEN_AUTHORING, - }; + } as const; } export function logout() { return { type: LOGOUT, - }; + } as const; } // Check if user data token is cached and is valid export function authenticateUser() { - return (dispatch, getState) => { + return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); dispatch(authenticating()); - return backend - .currentUser() + return Promise.resolve(backend.currentUser()) .then(user => { if (user) { if (user.useOpenAuthoring) { @@ -67,15 +70,15 @@ export function authenticateUser() { dispatch(doneAuthenticating()); } }) - .catch(error => { + .catch((error: Error) => { dispatch(authError(error)); dispatch(logoutUser()); }); }; } -export function loginUser(credentials) { - return (dispatch, getState) => { +export function loginUser(credentials: Credentials) { + return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); @@ -88,7 +91,7 @@ export function loginUser(credentials) { } dispatch(authenticate(user)); }) - .catch(error => { + .catch((error: Error) => { console.error(error); dispatch( notifSend({ @@ -106,7 +109,7 @@ export function loginUser(credentials) { } export function logoutUser() { - return (dispatch, getState) => { + return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); Promise.resolve(backend.logout()).then(() => { @@ -115,3 +118,11 @@ export function logoutUser() { }); }; } + +export type AuthAction = ReturnType< + | typeof authenticating + | typeof authenticate + | typeof authError + | typeof doneAuthenticating + | typeof logout +>; diff --git a/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js deleted file mode 100644 index 452494d9..00000000 --- a/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Immutable from 'immutable'; -import { authenticating, authenticate, authError, logout } from 'Actions/auth'; -import auth from '../auth'; - -describe('auth', () => { - it('should handle an empty state', () => { - expect(auth(undefined, {})).toEqual(null); - }); - - it('should handle an authentication request', () => { - expect(auth(undefined, authenticating())).toEqual(Immutable.Map({ isFetching: true })); - }); - - it('should handle authentication', () => { - expect(auth(undefined, authenticate({ email: 'joe@example.com' }))).toEqual( - Immutable.fromJS({ user: { email: 'joe@example.com' } }), - ); - }); - - it('should handle an authentication error', () => { - expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual( - Immutable.Map({ - error: 'Error: Bad credentials', - }), - ); - }); - - it('should handle logout', () => { - const initialState = Immutable.fromJS({ user: { email: 'joe@example.com' } }); - const newState = auth(initialState, logout()); - expect(newState.get('user')).toBeUndefined(); - }); -}); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.ts b/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.ts new file mode 100644 index 00000000..7cc7fde4 --- /dev/null +++ b/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.ts @@ -0,0 +1,32 @@ +import { fromJS } from 'immutable'; +import { authenticating, authenticate, authError, logout } from '../../actions/auth'; +import auth, { defaultState } from '../auth'; + +describe('auth', () => { + it('should handle an empty state', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + expect(auth(undefined, {})).toEqual(defaultState); + }); + + it('should handle an authentication request', () => { + expect(auth(undefined, authenticating())).toEqual(defaultState.set('isFetching', true)); + }); + + it('should handle authentication', () => { + const user = { name: 'joe', token: 'token' }; + expect(auth(undefined, authenticate(user))).toEqual(defaultState.set('user', fromJS(user))); + }); + + it('should handle an authentication error', () => { + expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual( + defaultState.set('error', 'Error: Bad credentials'), + ); + }); + + it('should handle logout', () => { + const user = { name: 'joe', token: 'token' }; + const newState = auth(defaultState.set('user', fromJS(user)), logout()); + expect(newState.get('user')).toBeUndefined(); + }); +}); diff --git a/packages/netlify-cms-core/src/reducers/auth.js b/packages/netlify-cms-core/src/reducers/auth.js deleted file mode 100644 index d4879743..00000000 --- a/packages/netlify-cms-core/src/reducers/auth.js +++ /dev/null @@ -1,21 +0,0 @@ -import Immutable from 'immutable'; -import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE, AUTH_REQUEST_DONE, LOGOUT } from 'Actions/auth'; - -const auth = (state = null, action) => { - switch (action.type) { - case AUTH_REQUEST: - return Immutable.Map({ isFetching: true }); - case AUTH_SUCCESS: - return Immutable.fromJS({ user: action.payload }); - case AUTH_FAILURE: - return Immutable.Map({ error: action.payload && action.payload.toString() }); - case AUTH_REQUEST_DONE: - return state.remove('isFetching'); - case LOGOUT: - return state.remove('user').remove('isFetching'); - default: - return state; - } -}; - -export default auth; diff --git a/packages/netlify-cms-core/src/reducers/auth.ts b/packages/netlify-cms-core/src/reducers/auth.ts new file mode 100644 index 00000000..c2a755c9 --- /dev/null +++ b/packages/netlify-cms-core/src/reducers/auth.ts @@ -0,0 +1,42 @@ +import { fromJS } from 'immutable'; +import { User } from 'netlify-cms-lib-util'; +import { + AUTH_REQUEST, + AUTH_SUCCESS, + AUTH_FAILURE, + AUTH_REQUEST_DONE, + LOGOUT, + AuthAction, +} from '../actions/auth'; +import { StaticallyTypedRecord } from '../types/immutable'; + +export type Auth = StaticallyTypedRecord<{ + isFetching: boolean; + user: StaticallyTypedRecord | undefined; + error: string | undefined; +}>; + +export const defaultState = fromJS({ + isFetching: false, + user: undefined, + error: undefined, +}); + +const auth = (state = defaultState as Auth, action: AuthAction) => { + switch (action.type) { + case AUTH_REQUEST: + return state.set('isFetching', true); + case AUTH_SUCCESS: + return state.set('user', fromJS(action.payload)); + case AUTH_FAILURE: + return state.set('error', action.payload && action.payload.toString()); + case AUTH_REQUEST_DONE: + return state.set('isFetching', false); + case LOGOUT: + return state.set('user', undefined).set('isFetching', false); + default: + return state; + } +}; + +export default auth; diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index f4487cd3..2eb5b174 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -3,6 +3,7 @@ import { StaticallyTypedRecord } from './immutable'; import { Map, List, OrderedMap, Set } from 'immutable'; import AssetProxy from '../valueObjects/AssetProxy'; import { MediaFile as BackendMediaFile } from '../backend'; +import { Auth } from '../reducers/auth'; export type SlugConfig = StaticallyTypedRecord<{ encoding: string; @@ -295,6 +296,7 @@ export type Status = StaticallyTypedRecord<{ }>; export interface State { + auth: Auth; config: Config; cursors: Cursors; collections: Collections;