How to update RTK Query cache when Firebase RTDB change event fired (update, write, create, delete) - javascript

I am using redux-tookit, rtk-query (for querying other api's and not just Firebase) and Firebase (for authentication and db).
The code below works just fine for retrieving and caching the data but I wish to take advantage of both rtk-query caching as well as Firebase event subscribing, so that when ever a change is made in the DB (from any source even directly in firebase console) the cache is updated.
I have tried both updateQueryCache and invalidateTags but so far I am not able to find an ideal approach that works.
Any assistance in pointing me in the right direction would be greatly appreciated.
// firebase.ts
export const onRead = (
collection: string,
callback: (snapshort: DataSnapshot) => void,
options: ListenOptions = { onlyOnce: false }
) => onValue(ref(db, collection), callback, options);
export async function getCollection<T>(
collection: string,
onlyOnce: boolean = false
): Promise<T> {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => reject('Request timed out!'), ASYNC_TIMEOUT);
onRead(collection, (snapshot) => resolve(snapshot.val()), { onlyOnce });
}).finally(() => clearTimeout(timeout));
}
// awards.ts
const awards = dbApi
.enhanceEndpoints({ addTagTypes: ['Themes'] })
.injectEndpoints({
endpoints: (builder) => ({
getThemes: builder.query<ThemeData[], void>({
async queryFn(arg, api) {
try {
const { auth } = api.getState() as RootState;
const programme = auth.user?.unit.guidingProgramme!;
const path = `/themes/${programme}`;
const themes = await getCollection<ThemeData[]>(path, true);
return { data: themes };
} catch (error) {
return { error: error as FirebaseError };
}
},
providesTags: ['Themes'],
keepUnusedDataFor: 1000 * 60
}),
getTheme: builder.query<ThemeData, string | undefined>({
async queryFn(slug, api) {
try {
const initiate = awards.endpoints.getThemes.initiate;
const getThemes = api.dispatch(initiate());
const { data } = (await getThemes) as ApiResponse<ThemeData[]>;
const name = slug
?.split('-')
.map(
(value) =>
value.substring(0, 1).toUpperCase() +
value.substring(1).toLowerCase()
)
.join(' ');
return { data: data?.find((theme) => theme.name === name) };
} catch (error) {
return { error: error as FirebaseError };
}
},
keepUnusedDataFor: 0
})
})
});

Related

How to use encrypt-storage with zustand in react?

I'm using zustand with persist plugin to store the state of my application. I want to use localstorage but the cache has to be encrypted.
For encryption, I'm using encrypt-storage. For encryption keys, I want to make an API call to the backend and initialise the encrypt storage.
The problem is while the API call is being made, the storage is still undefined. How to properly initialise zustand with encrypt-storage ?
Here is what I have tried :
import { EncryptStorage } from "encrypt-storage";
import { create } from "zustand";
import { devtools, persist, } from "zustand/middleware";
import { createJSONStorage } from "zustand/middleware"
const fake_api = (ms: number) => new Promise(resolve => {
setTimeout(resolve, ms)
})
export function KeyResolver(callback: () => void) {
const fn = async () => {
//
await fake_api(2000);
console.log("encryption key retrieved")
encryptStorage.EKEY = 'secret-key-value';
encryptStorage.storage = new EncryptStorage(encryptStorage.EKEY, {
stateManagementUse: true,
});
callback();
};
if (!encryptStorage.EKEY) {
fn();
}
}
interface IEncryptStorage {
storage: undefined | EncryptStorage,
EKEY: null | string,
}
export const encryptStorage: IEncryptStorage = {
storage: undefined,
EKEY: null,
}
const useOptimiserStore = create<IOptimiserStore>()(
devtools(
persist(
(set) => ({
...initialOtimiserStoreState,
_hasHydrated: false,
setHyderated: (val) => set({ _hasHydrated: val })
}),
{
name: "optimiser-storage",
// #ts-expect-error
storage: createJSONStorage(() => encryptStorage.storage),
onRehydrateStorage: () => {
KeyResolver(() => {
useOptimiserStore.getState().setHyderated(true)
});
}
}
),
{
name: "optimiser-storage",
}
)
);
// And i'm using it inside my component like this:
const Component = () => {
const hasHyderated = useOptimiserStore(state => state._hasHydrated);
if (!hasHyderated) {
return <>retreiving encryption keys </>
}
return <div> ... </div>
}
But I get the following error:
Uncaught TypeError: can't access property "setItem", storage is undefined
I managed to make it work by implementing a custom storage engine.
https://docs.pmnd.rs/zustand/integrations/persisting-store-data#how-can-i-use-a-custom-storage-engine

ApolloGraphQL not triggering query when landing on page after a mutation that changes data

Intended result
I have two routes: /test/ and /test/:id.
On /test/ I have a list of events and it's only made of events that haven't been resolved
On /test/:id I have a mutation to mark an event as resolved, and, on success, I'm redirecting the user back to /test/.
This success means that the event should no longer appear on /test/ and I'm expecting a new request to get the list of events.
// my file with the mutation
const [eventResolveMutation] = useEventResolveMutation({
onCompleted: () => {
showSuccessToast(
`${t('form:threat-resolved')}! ${t('general:threat')} ${t(
'form:has-been-resolved'
)}.`
)
setTimeout(() => {
navigate('/threats/live')
}, 2000)
},
onError: (error: ApolloError) => {
showErrorToast(error.message)
},
})
const handleEventResolveClick = (id: string) => {
eventResolveMutation({ variables: { id: id, isResolved: true } })
}
return (
<button onClick={() => handleEventResolveClick(id)}>Press</button>
)
// my file with the `events` query
// the results are displayed in a table, which is way I have `currentPage` and `pageSize` in them
const [getEvents, { loading, data }] = useEventsLazyQuery()
useEffect(() => {
getEvents({
variables: {
page: currentPage,
pageSize: paginationSizeOptions[chosenDropdownIndex],
isThreat: true,
isResolved: false,
},
})
}, [chosenDropdownIndex, currentPage, getEvents])
Actual outcome:
Once I press the button that triggers the mutation and I'm redirected to the /tests, I can see that I'm landing inside the useEffect by logging something. What I don't see is a request made via getEvents, which is expected to happen since all the functionalities with the page work
Extra info:
// my graphqlclient.ts
import {
ApolloClient,
ApolloLink,
createHttpLink,
InMemoryCache,
} from '#apollo/client'
const serverUrl = () => {
switch (process.env.REACT_APP_ENVIRONMENT) {
case 'staging':
return 'env'
case 'production':
return 'env'
default:
return 'env'
}
}
const cleanTypeName = new ApolloLink((operation, forward) => {
if (operation.variables) {
const omitTypename = (key: string, value: any) =>
key === '__typename' ? undefined : value
operation.variables = JSON.parse(
JSON.stringify(operation.variables),
omitTypename
)
}
return forward(operation).map((data) => data)
})
const httpLink = createHttpLink({
uri: serverUrl(),
credentials: 'include',
})
const httpLinkWithTypenameHandling = ApolloLink.from([cleanTypeName, httpLink])
const client = new ApolloClient({
link: httpLinkWithTypenameHandling,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
})
export default client
// my mutation
// this mutation will mark an `id` as `resolved` and that means that it should disappear from the list above
mutation EventResolve($id: ID!, $isResolved: Boolean!) {
eventResolve(id: $id, isResolved: $isResolved) {
id
sequence
}
}

Calling .subscribe() after .pipe(); not executing my redirection code

I have an AuthService with these methods:
signUp = (data: SignUp): Observable<AuthResponseData> => {
const endpoint = `${env.authBaseUrl}:signUp?key=${env.firebaseKey}`;
return this._signInOrSignUp(endpoint, data);
};
signIn = (data: SignIn): Observable<AuthResponseData> => {
const endpoint = `${env.authBaseUrl}:signInWithPassword?key=${env.firebaseKey}`;
return this._signInOrSignUp(endpoint, data);
};
private _signInOrSignUp = (endpoint: string, data: SignIn | SignUp): Observable<AuthResponseData> => {
return this.http.post<AuthResponseData>(endpoint, {
...data,
returnSecureToken: true
}).pipe(
catchError(error => this._throwError(error)),
tap(response => this._createAndEmitUserSubject(response)),
);
}
private _throwError = error => {
const errorMessage = error.error.error.message;
return throwError(errorMessage);
};
private _createAndEmitUserSubject = (response: AuthResponseData) => {
const expirationDate = new Date(new Date().getTime() + +response.expiresIn * 1000);
const user = new User(
response.idToken,
response.email,
response.idToken,
expirationDate
);
localStorage.setItem("user", JSON.stringify(user));
this.user$.next(user);
};
And in the sign-in and sign-up components, I call the those in following way:
submit = (): void => {
if (this.loginForm.invalid) {
return;
}
this.authService
.signIn(this.loginForm.value)
.subscribe({
next: () => this.router.navigate(["/recipes"]),
error: error => this.error = error
});
};
But the code within next is not executed. If I remove .pipe() it is. I was hoping I did not need to use .pipe() in two places.
Looks like this._throwError doesn't throw an error. If it was, next couldn't be called later. You are probably not throw'ing error in it and instead returning some value that goes into next
Removing this.user$.next(user) solves it, and I did't realize I did not need this at all, but I'd like to know why it blocked the flow.
private _createAndEmitUserSubject = (response: AuthResponseData) => {
// ...
localStorage.setItem("user", JSON.stringify(user));
};

Converting from class to functional component with async state setting

I have a simple class-based component that I'm trying to convert to a function-based component, but am running into all kinds of dead ends.
My component is a straightforward adaptation of the boilerplate gifted-chat package, and uses Watson Assistant as a backend to provide responses. There's nothing complex about the backend part, these are just thin wrappers on Watson Assistants's API:
getSessionID = async (): Promise<string>
gets a session ID for use in communicating with the backend, and
sendReply = async (reply: string, sessionID: string): Promise<string>
returns Assistant's response to the string provided as a reply. These are not the source of the trouble I'm having (the bodies of both could be replaced with return await "some string" and I'd have the same issues): the class-based version (below) works perfectly.
But I'm at a loss to figure out how to convert this to a functional form, in particular:
I'm struggling to find a suitable replacement for componentWillMount. Using useEffect with sessionID as state results in errors: getMessage gets called (even if I await) before the required sessionID is set.
I can avoid this by not making sessionID state (which it arguably shouldn't be) and just making it a global (as in the functional attempt below). But even if I do this:
After each user reply, and receipt of a response, the user reply is removed from the conversation, so that the entire conversation just consists of generated replies.
Both of these problems are, I think, linked to the lack of callbacks in the hook-based state setting idiom, but the issue could also lie elsewhere. In any case, I'm at a loss to know what to do.
Chatter.tsx (working class based version)
import React from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
class Chatter extends React.Component {
state = {
messages: [],
sessionID: null,
}
componentWillMount() {
WatsonAssistant.getSessionID()
.then((sID) => {
this.setState( {
sessionID: sID,
} )
} )
.then(() => this.getMessage(''))
.catch((error) => {
console.error(error)
} )
}
onSend = (message = []): void => {
this.setState((previousState) => ( {
messages: GiftedChat.append(previousState.messages, message),
} ), () => {
this.getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
} )
}
getMessage = async (text: string): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, this.state.sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
this.setState((previousState) => ( {
messages: GiftedChat.append(previousState.messages, message),
} ))
}
render() {
return (
<GiftedChat
messages={ this.state.messages }
onSend={ messages => this.onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
}
export default Chatter
Chatter.tsx (failed function based attempt)
import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
let sessionID: string
const Chatter: FC = (): ReactElement => {
const [ messages, setMessages ] = useState([])
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => sessionID = sID )
.then(() => getMessage(''))
.catch((error) => {
console.error(error)
} )
}
fetchData()
}, [ ])
const onSend = async (message = []) => {
const newMessages = await GiftedChat.append(messages, message)
await setMessages(newMessages)
await getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
}
const getMessage = async (text: string): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
await setMessages(await GiftedChat.append(messages, message))
}
return (
<GiftedChat
messages={ messages }
onSend={ messages => onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
export default Chatter
Chatter.tsx (working function based version)
import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
let sessionID: string
const Chatter: FC = (): ReactElement => {
const [ messages, setMessages ] = useState([])
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => sessionID = sID )
.then(() => getMessage('', []))
.catch((error) => {
console.error(error)
} )
}
fetchData()
}, [ ])
const onSend = async (message = []) => {
const newMessages = await GiftedChat.append(messages, message)
await setMessages(newMessages) // Apparently, no waiting goes on here
await getMessage(message[0].text.replace(/[\n\r]+/g, ' '), newMessages)
}
const getMessage = async (text: string, currentMessages): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
await setMessages(await GiftedChat.append(currentMessages, message))
}
return (
<GiftedChat
messages={ messages }
onSend={ messages => onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
export default Chatter
Ok, since I don't have your full code I'm not sure this will just work as-is (in particular without the types from your dependencies I'm not sure if/how much the compiler will complain), but should give you something you can adapt easily enough.
const reducer = ({ messages }, action) => {
switch (action.type) {
case 'add message':
return {
messages: GiftedChat.append(messages, action.message),
};
case 'add sent message':
return {
// Not sure if .append is variadic, may need to adapt
messages: GiftedChat.append(messages, action.message, action.message[0].text.replace(/[\n\r]+/g, ' ')),
}
}
};
const Chatter = () => {
const [sessionID, setSessionID] = useState(null);
const [messages, dispatch] = useReducer(reducer, []);
const getMessage = async (text: string, sessionID: number, type: string = 'add message'): Promise<void> => {
const response = await WatsonAssistant.sendReply(text, sessionID);
const message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
};
dispatch({
type,
message,
});
};
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => (setSessionID(sID), sID))
.then(sID => getMessage('', sID))
.catch((error) => {
console.error(error)
});
}
fetchData();
}, []);
return (
<GiftedChat
messages={messages}
onSend={messages => getMessage(messages, sessionID, 'add sent message')}
user={{
_id: 1,
}}
/>
);
};
Main difference is useReducer. As far as I can tell in the original code you had two actions: append this message or append this message and then a copy of it with the text regex replaced. I've used different dispatches to the reducer to handle the cases rather than the callback to setState. I've modified your attempt at useEffect, here I'm (ab)using the comma operator to return the ID returned from the service so that it can be fed directly to getMessage as a parameter rather than relying on state that hasn't been updated yet.
I'm still kinda skeptical in general about the hooks API, but assuming this works I actually think it simplifies the code here.

Vuex: Testing actions with API calls

I have been following these testing guidelines to test my vuex store.
But when I touched upon the actions part, I felt there is a lot going on that I couldn't understand.
The first part goes like:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// use require syntax for inline loaders.
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies.
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
I infer that this is to mock the service inside the action.
The part which follows is:
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
This is where it I find it difficult to follow.
My store looks like:
import * as NameSpace from '../NameSpace'
import { ParseService } from '../../Services/parse'
const state = {
[NameSpace.AUTH_STATE]: {
auth: {},
error: null
}
}
const getters = {
[NameSpace.AUTH_GETTER]: state => {
return state[NameSpace.AUTH_STATE]
}
}
const mutations = {
[NameSpace.AUTH_MUTATION]: (state, payload) => {
state[NameSpace.AUTH_STATE] = payload
}
}
const actions = {
[NameSpace.ASYNC_AUTH_ACTION]: ({ commit }, payload) => {
ParseService.login(payload.username, payload.password)
.then((user) => {
commit(NameSpace.AUTH_MUTATION, {auth: user, error: null})
})
.catch((error) => {
commit(NameSpace.AUTH_MUTATION, {auth: [], error: error})
})
}
}
export default {
state,
getters,
mutations,
actions
}
And This is how I am trying to test:
import * as NameSpace from 'src/store/NameSpace'
import AuthStore from 'src/store/modules/authorization'
const actionsInjector = require('inject!../../../../../src/store/modules/authorization')
// This file is present at: test/unit/specs/store/modules/authorization.spec.js
// src and test are siblings
describe('AuthStore Actions', () => {
const injectedAction = actionsInjector({
'../../Services/parse': {
login (username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({})
} else {
reject({})
}
}, 300)
})
}
}
})
it('Gets the user profile if the username and password matches', () => {
const testAction = (action, payload, state, mutations, done) => {
const commit = (payload) => {
if (payload) {
expect(mutations.payload).to.deep.equal(payload)
}
}
action({ commit, state }, payload)
.then(result => {
expect(state).to.deep.equal({auth: result, error: null})
})
.catch(error => {
expect(state).to.deep.equal({auth: [], error: error})
})
}
testAction(injectedAction.login, null, {}, [])
})
})
If I try to do this, I get:
"Gets the user profile if the username and password matches"
undefined is not a constructor (evaluating 'action({ commit: commit, state: state }, payload)')
"testAction#webpack:///test/unit/specs/store/modules/authorization.spec.js:96:13 <- index.js:26198:14
webpack:///test/unit/specs/store/modules/authorization.spec.js:104:15 <- index.js:26204:16"
I need help understanding what am I supposed to do to test such actions.
I know it's been awhile but I came across this question because I was having a similar problem. If you were to console.log injectedActions right before you make the testAction call you'd see that the injectedAction object actually looks like:
Object{default: Object{FUNC_NAME: function FUNC_NAME(_ref) { ... }}}
So the main solution here would be changing the testAction call to:
testAction(injectedAction.default.login, null, {}, [], done)
because you are exporting your action as defaults in your store.
A few other issues that are unrelated to your particular error... You do not need to manipulate the testAction boilerplate code. It will work as expected so long as you pass in the proper parameters. Also, be sure to pass done to testAction or your test will timeout. Hope this helps somebody else who comes across this!

Categories