fp-ts: Filter out certain left values, and error on right - fp-ts

I'd like to ignore certain errors using fp-ts (if they happen, it means everything went well, i.e. missing account during signup process).
I have the following code:
export const handleSignup = async (server: FastifyInstance): Promise<void> => {
server.post('/signup', async (req, res) => {
const {email} = req.body as SignupPostData
const {redirectUri} = req.query as Record<'redirectUri', string>
return pipe(
withDb(lookupAccountByEmail)(email),
TE.chain(() => flow(generateMagicLinkToken, TE.fromEither)(email)),
TE.chain(sendSignupEmail(email, redirectUri))
)().then(foldReply<SignupApiResponse>(res))
})
}
The lookupAccountByEmail function will either return an Account, or will return an error object.
If an account is returned, I need to return an error with code 'account-exists'. If an error object with the code 'account-not-found' is returned, I'd like everything to continue as if there were no problem. If an error object with any other code is returned, it should still error.
What's the best way to handle this in fp-ts?

You can use TE.fold.
const doSignup = pipe(
generateMagicLinkToken(email),
TE.fromEither,
TE.chain(sendSignupEmail(email, redirectUri))
)
return pipe(
email,
withDb(lookupAccountByEmail),
TE.fold(
left => left.error === 'account-not-found' ? doSignup : TE.left(left)
right => TE.left({error: 'account-exits'})
),
T.map(foldReply<SignupApiResponse>(res))
)()

Related

How do you access query arguments in getSelectors() when using createEntityAdapter with RTK Query

I've been following along the REDUX essentials guide and I'm at part 8, combining RTK Query with the createEntityAdapter. I'm using the guide to implement it in a personal project where my getUni endpoint has an argument named country, as you can see from the code snippet below.
I'm wondering is there anyway to access the country argument value from the state in universityAdaptor.getSelector(state => ) at the bottom of the snippet, as the query key name keeps changing.
import {
createEntityAdapter,
createSelector,
nanoid
} from "#reduxjs/toolkit";
import {
apiSlice
} from "../api/apiSlice";
const universityAdapter = createEntityAdapter({})
const initialState = universityAdapter.getInitialState();
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUni: builder.query({
query: country => ({
url: `http://universities.hipolabs.com/search?country=${country}`,
}),
transformResponse: responseData => {
let resConvert = responseData.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(each => {
return { ...each,
id: nanoid()
}
});
return universityAdapter.setAll(initialState, resConvert)
}
})
})
});
export const {
useGetUniQuery
} = extendedApiSlice;
export const {
selectAll: getAllUniversity
} = universityAdapter.getSelectors(state => {
return Object.keys({ ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }).length === 0
? initialState : { ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }
})
UPDATE: I got it working with a turnery operator due to the multiple redux Actions created when RTK Query handles fetching. Wondering if this is best practice as I still haven't figured out how to access the country argument.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
return !Object.values(state.api.queries)[0]
? initialState : Object.values(state.api.queries)[0].status !== 'fulfilled'
? initialState : Object.values(state.api.queries)[0].data
})
I wrote that "Essentials" tutorial :)
I'm actually a bit confused what your question is - can you clarify what specifically you're trying to do?
That said, I'll try to offer some hopefully relevant info.
First, you don't need to manually call someEndpoint.select() most of the time - instead, call const { data } = useGetThingQuery("someArg"), and RTKQ will fetch and return it. You only need to call someEndpoint.select() if you're manually constructing a selector for use elsewhere.
Second, if you are manually trying to construct a selector, keep in mind that the point of someEndpoint.select() is to construct "a selector that gives you back the entire cache entry for that cache key". What you usually want from that cache entry is just the received value, which is stored as cacheEntry.data, and in this case that will contain the normalized { ids : [], entities: {} } lookup table you returned from transformResponse().
Notionally, you might be able to do something like this:
const selectNormalizedPokemonData = someApi.endpoints.getAllPokemon.select();
// These selectors expect the entity state as an arg,
// not the entire Redux root state:
// https://redux-toolkit.js.org/api/createEntityAdapter#selector-functions
const localizedPokemonSelectors = pokemonAdapter.getSelectors();
const selectPokemonEntryById = createSelector(
selectNormalizedPokemonData ,
(state, pokemonId) => pokemonId,
(pokemonData, pokemonId) => {
return localizedPokemonSelectors.selectById(pokemonData, pokemonId);
}
)
Some more info that may help see what's happening with the code in the Essentials tutorial, background - getLists endpoint takes 1 parameter, select in the service:
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId) : [];
};
And my selector in the slice:
export const selectAllLists = createSelector(getListsResult, (listsResult) => {
console.log('inside of selectAllLists selector = ', listsResult);
return listsResult.data;
// return useSelector(listsResult) ?? [];
});
Now this console logs listsResult as ƒ memoized() { function! Not something that can have .data property as tutorial suggests. Additionally return useSelector(listsResult) - makes it work, by executing the memoized function.
This is how far I got, but from what I understand, the code in the Essentials tutorial does not work as it is...
However going here https://codesandbox.io/s/distracted-chandrasekhar-r4mcn1?file=/src/features/users/usersSlice.js and adding same console log:
const selectUsersData = createSelector(selectUsersResult, (usersResult) => {
console.log("usersResult", usersResult);
return usersResult.data;
});
Shows it is not returning a memorised function, but an object with data on it instead.
Wonder if the difference happening because I have a parameter on my endpoint...
select returns a memoized curry function. Thus, call it with first with corresponding arg aka tribeId in your case and then with state. This will give you the result object back for corresponding chained selectors.
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId)(state) : [];
};
The intention of the getUni endpoint was to produce an array of university data. To implement the .getSelector function to retrieve that array, I looped over all query values, searching for a getUni query and ensuring they were fulfilled. The bottom turnery operator confirms the getUni endpoint was fired at least once otherwise, it returns the initialState value.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
let newObj = {};
for (const value of Object.values(state.api.queries)) {
if (value?.endpointName === 'getUni' && value?.status === 'fulfilled') {
newObj = value.data;
}
}
return !Object.values(newObj)[0] ? initialState : newObj;
})

ReactJS context-api won't render after I get data

This is a next.js site, since both my Navbar component and my cart page should have access to my cart's content I created a context for them. If I try to render the page, I get:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'key')
obs: The cartContent array exists and has length 1, I can get it by delaying when the data's rendered by using setTimeout, but, can't get it to render right after it's fetched.
I need to make it render after the data from firebase is returned, but always met with the mentioned error.
This is my _app.tsx file
function MyApp({ Component, pageProps }) {
// set user for context
const userContext = startContext();
return (
<UserContext.Provider value = { userContext }>
<Navbar />
<Component {...pageProps} />
<Toaster />
</UserContext.Provider>
);
}
export default MyApp
This file has the startContext function that returns the context so it can be used.
export const startContext = () => {
const [user] = useAuthState(auth);
const [cart, setCart] = useState(null);
const [cartContent, setCartContent] = useState(null);
useEffect(() => {
if (!user) {
setCart(null);
setCartContent(null);
}
else {
getCart(user, setCart, setCartContent);
}
}, [user]);
return { user, cart, setCart, cartContent, setCartContent };
}
This file contains the getCart function.
export const getCart = async (user, setCart, setCartContent) => {
if (user) {
try {
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
if (new_cart) {
let new_cartContent = []
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
console.log(new_cartContent);
setCartContent(new_cartContent);
console.log(new_cartContent);
setCart(new_cart);
}
else {
setCart(null);
setCartContent(null);
}
} catch (err) {
console.error(err);
}
}
}
This is the cart.tsx webpage. When I load it I get the mentioned error.
export default () => {
const { user, cart, cartContent } = useContext(UserContext);
return (
<AuthCheck>
<div className="grid grid-cols-1 gap-4">
{cartContent && cartContent[0].key}
</div>
</AuthCheck>
)
}
I've tried to render the cart's content[0].key in many different ways, but couldn't do it. Always get error as if it were undefined. Doing a setTimeout hack works, but, I really wanted to solve this in a decent manner so it's at least error proof in the sense of not depending on firebase's response time/internet latency.
Edit:
Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it will consider the state cartContent as undefined and won't trigger again later.
Try changing
{cartContent && cartContent[0].key}
to
{cartContent?.length > 0 && cartContent[0].key}
Edit:: The actual problem is in getCart function in line
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
This is either set to an empty array or an empty object. So try changing your if (new_cart) condition to
if (Object.keys(new_cart).length > 0) {
Now you wont get the undefined error
Since there seemed to be a race condition, I figured the setCartContent was executing before its content was fetched. So I changed in the getCart function the map loop with an async function for a for loop
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
to
for (const key of Object.keys(new_cart)) {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
}
I can't make a map function with await in it without making it asynchronous so I the for loop made it work. Hope someone finds some alternatives to solving this, I could only come up with a for loop so the code is synchronous.

NextJs How to trigger a function when a user leaves a page

I've found this comment to match what I'm looking for. It seems to work, for me, perfectly with "console.log". I want to know when a user has left the page:
const getChatRoomId = () => {}
useEffect(() => {
const chatRoomId = getChatRoomId()
if (chatRoomId) {
console.log('----HAS ENTERED /messages/* ------', chatRoomId)
}
// I believe this is not important?
const routeChangeStart = url => {}
// This is very important
const beforeunload = e => {
if (chatRoomId) {
console.log(`---- IS LEAVING /messages/${chatRoomId} ------`)
}
}
window.addEventListener('beforeunload', beforeunload);
Router.events.on('routeChangeStart', routeChangeStart);
return () => {
window.removeEventListener('beforeunload', beforeunload);
Router.events.off('routeChangeStart', routeChangeStart);
};
}, [someStateChangeVariable])
The above works great but using a function inside beforeunload does not work. I've made some searches and one states to "return a string":
[..]
// This is very important
const beforeunload = e => {
if (chatRoomId) {
alert(`---- IS LEAVING /messages/${chatRoomId} ------`)
}
return 'GoodBye!'
}
[..]
Blocked alert('---- IS LEAVING /messages/1 ------') during beforeunload.
I'm using NextJs catch all routes /pages/[...messages] where you can have a page id of "new" or "123" where "123" is the message id and "new" to create a new message. How to trigger a function when a user leaves a page?
You can use router.events methods: NextJS docs.
Also this topic might be relevant.

How do I handle possible null values in TypeScript when I can't access the defintions

I'm writing a React App with TypeScript, and using Firebase, and I have a couple of errors which are similar.
I've written my Firebase class:
class Firebase {
constructor() {
app.initializeApp(config);
}
// *** Auth API ***
doSignInWithEmailAndPassword = (email, password) => app.auth().signInWithEmailAndPassword(email, password)
doEmailUpdate = (email:string) => app.auth().currentUser.updateEmail(email)
doPasswordUpdate = (password:string) => app.auth().currentUser.updatePassword(password)
doPasswordReset = (email:string) => app.auth().sendPasswordResetEmail(email)
doSignOut = () => app.auth().signOut();
getUserIdToken = () => app.auth().currentUser.getIdToken();
}
export default Firebase;
Problem 1: doEmailUpdate and doPasswordUpdateare both highlighted with the error "Object is possibly null". This is because app.auth().currentUser is null if the user isn't signed in. I can get around this by using app.auth().currentUser? and making sure it's only called if the user is signed in, but is there a better way? I tried doing
if(app.auth().currentUser !== null) ...
but still got the error.
Problem 2: getUserIdToken also highlights that app.auth().currentUser may be null, but again I can get around that using `app.auth().currentUser?. However, I'm calling this in a component using:
const MyComponent= ({ firebase}) => {
useEffect(() => {
(firebase.getUserIdToken())
.then((token:string) => { ... })
})
})
and I get the error Object is possibly 'undefined'. This is because the return type of getUserIdToken is Promise<string> | undefined
Obviously I can't change the return type of a Firebase function, so how do I get around this?
Use Promise.reject() if app.auth().currentUser is undefined || null, that way you're returning a Promise<string | undefined> as opposed to Promise<string> | undefined.
I'd refactor getUserIdToken to look like this
getUserIdToken = () => {
if (app.auth().currentUser) {
return app.auth().currentUser.getIdToken()
}
return Promise.reject();
};

Why do I get undefined value from async function?

I have been using Google firestore as a database for my projet.
In the collection "paths", I store all the paths I have in my app, which are composed of 2 fields : name, and coordinates (which is an array of objects with coordinates of points).
Anyway, i created a utility file in utils/firebase.js
In the file, i have this function which gets all the paths in my collection and return an array of all documents found :
export const fetchPaths = () => {
let pathsRef = db.collection('paths');
let pathsArray = []
pathsRef.get().then((response) => {
response.docs.forEach(path => {
const {nom, coordonnees } = path.data();
pathsArray.push({ nom: nom, coordonnees: coordonnees})
})
console.log(pathsArray)
return pathsArray;
});
};
In my react component, What i want to do is to load this function in useEffect to have all the data, and then display them. Here is the code I use :
import { addPath, fetchPaths } from './Utils/firebase';
//rest of the code
useEffect(() => {
let paths = fetchPaths()
setLoadedPaths(paths);
}, [loadedPaths])
//.......
The issue here is if I console log pathsArray in the function it's correct, but it never gets to the state.
When i console log paths in the component file, i get undefined.
I am quite new with react, i tried different things with await/async, etc. But I don't know what i am doing wrong here / what i misunderstand.
I know that because of my dependency, i would be supposed to have an infinite loop, but it's not even happening
Thank you for your help
Have a nice day
fetchPaths does not return any result. It should be:
export const fetchPaths = () => {
let pathsRef = db.collection('paths');
let pathsArray = []
return pathsRef.get().then((response) => {
response.docs.forEach(path => {
const {nom, coordonnees } = path.data();
pathsArray.push({ nom: nom, coordonnees: coordonnees})
})
console.log(pathsArray)
return pathsArray;
});
};
note the return statement.
Since the fetchPaths returns a promise, in the effect it should be like following:
useEffect(() => {
fetchPaths().then(paths =>
setLoadedPaths(paths));
}, [loadedPaths])

Resources