How I can make asynchronous queries in GraphQL? - javascript

I'm calling 1 query and mutation. Mutation works fine, but when I get response from my query I need to redirect user to another page, but In my case, the function is triggered before I get response. How can I prevent this?
const renderData = async () => {
const currentUserId = await data?.signInUserSession?.idToken
?.payload?.sub;
const isAdmin = await data?.signInUserSession?.idToken?.payload[
"custom:role"
];
localStorage.setItem("userId", currentUserId);
if (
currentUserId !== null &&
currentUserId !== undefined &&
currentUserId !== ""
) {
Auth.currentSession().then((data) => {
setData({
variables: {
updateUserInput: {
id: currentUserId,
firstName: data.getIdToken().payload.given_name,
lastName: data.getIdToken().payload.family_name,
},
},
});
});
isCodeValid({
variables: {
validateUserVerificationCodeInput: {
user: {
id: currentUserId,
},
},
},
});
if (isAdmin === "admin" && isUserCodeValid) {
history.push("/managements");
} else if (
isUserCodeValid !== undefined &&
isUserCodeValid === true
) {
history.push("/verification");
} else if (isUserCodeValid) {
history.push("/stripe");
}
}
};
isUserCodeValid - is a response from query

useMutation has onCompleted and refetchQueries options for such cases. It is hard to write an exact solution for your case since not all code is visible but an example like below can help, I believe:
const [addProduct, { data, loading, error }] = useMutation(
createProductMutation
);
const onFinish = async (fieldNames) => {
await addSpending({
variables: { ...others, ...fieldNames},
refetchQueries: [{ query: calledQuery }],
onCompleted: (data) => {
// your logic
},
});
if (!error) {
form.resetFields();
onFinishSave(true);
}
};

Related

Trying to access state in oncompleted method

I have API query and getting the result and setting those in a state variable in Oncompleted method of API query, Now i am updating the same state variable in another api query "onCompleted method.
I am not able to access the result from state what i have set before in first api query and below is my code
Query 1:
const designHubQueryOnCompleted = designHubProject => {
if (designHubProject) {
const {
name,
spaceTypes
} = designHubProject;
updateState(draft => { // setting state here
draft.projectName = name;
draft.spaceTypes = (spaceTypes || []).map(po => {
const obj = getTargetObject(po);
return {
id: po.id,
name: obj.name,
category: obj.librarySpaceTypeCategory?.name,
description: obj.description,
warning: null // trying to modify this variable result in another query
};
});
});
}
};
const { projectDataLoading, projectDataError } = useProjectDataQuery(
projectNumber,
DESIGNHUB_PROJECT_SPACE_TYPES_MIN,
({ designHubProjects }) => designHubQueryOnCompleted(designHubProjects[0])
);
Query 2:
const {
// data: designhubProjectSpaceTypeWarnings,
loading: designhubProjectSpaceTypeWarningsLoading,
error: designhubProjectSpaceTypeWarningsError
} = useQuery(DESIGNHUB_PROJECT_LINKED_SPACETYPE_WARNINGS, {
variables: {
where: {
projectNumber: { eq: projectNumber }
}
},
onCompleted: data => {
const projectSpaceTypeWarnings = data.designHubProjectLinkedSpaceTypeWarnings[0];
const warnings = projectSpaceTypeWarnings.spaceTypeWarnings.reduce((acc, item) => {
const spaceTypeIdWithWarningState = {
spaceTypeId: item.spaceTypeProjectObjectId,
isInWarningState: item.isInWarningState
};
acc.push(spaceTypeIdWithWarningState);
return acc;
}, []);
console.log(state.spaceTypes); // trying to access the state here but getting empty array
if (state.spaceTypes.length > 0) {
const updatedSpaceTypes = state.spaceTypes;
updatedSpaceTypes.forEach(item => {
const spaceTypeWarning = { ...item };
spaceTypeWarning.warning = warnings?.filter(
w => w.spaceTypeId === spaceTypeWarning.id
).isInWarningState;
return spaceTypeWarning;
});
updateState(draft => {
draft.spaceTypes = updatedSpaceTypes;
});
}
}
});
Could any one please let me know where I am doing wrong with above code Or any other approach to modify the state, Many thanks in advance!!

Force computed variable to update (Firebase VueJS)

I have a navigation link that I want to update when I have signed a user into the app through firebase auth.
I'm managing the user login changes through .onAuthStateChanged in the created hook ()
data () {
return {
user: null,
additionaluserinfo: null,
isAdmin: false
}
},
created () {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.user = user
// call for additional user information from firebase
db.collection('users').where('user_id', '==', user.uid)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.data())
this.additionaluserinfo = doc.data()
this.$store.dispatch('setAdditionalUserInfo', doc.data())
})
})
.catch((error) => {
console.log('Error getting documents: ', error)
})
this.additionaluserinfo = this.$store.state.additionaluserinfo
if (this.$store.state.additionaluserinfo.role === 'admin' || this.$store.state.additionaluserinfo.role === 'superadmin') {
this.isAdmin = true
}
if (this.$store.state.additionaluserinfo.role === 'superadmin') {
this.isSuperAdmin = true
}
} else {
this.user = null
this.additionalUserInfo = null
}
})
I'm checking to see if the user has user rights on the onAuthSateChanged.
The issue that I have is that I have to refresh the page in order for my navigation bar to update (which displays a div based on v-if="isAdmin" / "isSuperAdmin". Is there a way to force this to update from within onAuthStateChanged?
Either move the block
this.additionaluserinfo = this.$store.state.additionaluserinfo
if (this.$store.state.additionaluserinfo.role === 'admin' || this.$store.state.additionaluserinfo.role === 'superadmin') {
this.isAdmin = true
}
if (this.$store.state.additionaluserinfo.role === 'superadmin') {
this.isSuperAdmin = true
}
inside this.$store.dispatch('setAdditionalUserInfo', doc.data()).then(() => { /* */ }) or else that code would be evaluated before data are dispatched into the store (data fetching is async)
Or, better, just remove it and then add:
computed: {
role: () => this.$store.state.additionaluserinfo ? this.$store.state.additionaluserinfo.role : '',
isAdmin: () => this.role === 'admin' || this.role === 'superadmin', // remove isAdmin from data()
isSuperAdmin: () => this.role === 'superadmin',
}

Use async API call inside loop in computed property with vue-async-computed

I have a computed property inside a Vue component that looks like so:
allUsers() {
const vm = this;
const users = this.getAll.map((item) => {
let newUser = {};
if (typeof item !== 'object') {
newUser = {
id: vm.userModel.id,
userId: vm.userModel.userId,
data: null,
tenantData: null,
};
} else {
newUser = item;
}
return newUser;
});
return users;
},
I need to insert some additional data into each newUser object, but getting that data requires 1) looping through another set of data for each newUser item and 2) getting the data returned from an axios call to a REST API endpoint:
async delete({ device, personId }) {
return await super.perform(axios.delete(ServiceUrlProvider.gmiUrl()
.concat('/person/')
.concat(personId)
.concat('/device/')
.concat(device.deviceId)));
}
Ideally I would be able to do something like this:
allUsers() {
const vm = this;
const users = this.getAll.map((item) => {
let newUser = {};
if (typeof item !== 'object') {
newUser = {
id: vm.userModel.id,
userId: vm.userModel.userId,
data: null,
tenantData: null,
};
} else {
newUser = item;
}
this.tenantApps.forEach((app) => {
userDeviceService.fetchPersonAppDevice({
id: item.id,
appCode: app.code,
})
.then((resp) => {
// Code here to add returned value to newUser object.
});
});
return newUser;
});
return users;
},
but since it is bad practice to allow async actions in computed properties, I have to try something else. Based on what I found, I'm trying vue-async-computed, and I've moved my method to the separate asyncComputed object:
asyncComputed: {
allUsers() {
const vm = this;
const users = this.getAll.map((item) => {
let newUser = {};
if (typeof item !== 'object') {
newUser = {
id: vm.userModel.id,
userId: vm.userModel.userId,
data: null,
tenantData: null,
};
} else {
newUser = item;
}
this.tenantApps.forEach((app) => {
userDeviceService.fetchPersonAppDevice({
id: item.id,
appCode: app.code,
})
.then((resp) => {
if (Array.isArray(resp.data) && resp.data.length > 0) {
newUser.hasDevice = true;
} else {
newUser.hasDevice = false;
}
});
});
return newUser;
});
return users;
},
},
My problem is getting allUsers() to wait for the returned call to userDeviceService.fetchPersonAppDevice(), since as it is now, it finishes and returns without waiting. I can't just use await on the forEach loop. How do I need to implement that call so that I can add the returned data to my newUser object?
UPDATE: Per comment by Estus Flask below, I've modified my allUsers computed value like so:
asyncComputed: {
async allUsers() {
const vm = this;
const users = this.getAll.map((item) => {
let newUser = {};
if (typeof item !== 'object') {
newUser = {
id: vm.userModel.id,
userId: vm.userModel.userId,
data: null,
tenantData: null,
};
} else {
newUser = item;
}
const devicePromises = [];
this.tenantApps.forEach((app) => {
devicePromises.push(userDeviceService.fetchPersonAppDevice({
id: item.id,
appCode: app.code,
}));
});
const devices = await Promise.all(devicePromises);
return newUser;
});
return users;
},
},
However, I get an error on the await Promise.all(devicePromises); call saying that the await operator can only be used in an async function. I've changed allUsers to be asynchronous, so why the error?

Issues when testing Epic with TestScheduler

I'm using an rxjs epic as a middleware for an async action in a react-redux app.
I'm trying to simulate an ajax request (through a dependency injection) and test the behavior of this epic based on the response.
This is my epic :
export const loginEpic = (action$, store$, { ajax }) => { // Ajax method is injected
return action$.ofType(LoginActions.LOGIN_PENDING).pipe(
mergeMap(action => {
if (action.mail.length === 0) {
return [ loginFailure(-1) ]; // This action is properly returned while testing
} else {
return ajax({ ... }).pipe(
mergeMap(response => {
if (response.code !== 0) {
console.log(response.code); // This is logged
return [ loginFailure(response.code) ]; // This action is expected
} else {
return [ loginSuccess() ];
}
}),
catchError(() => {
return [ loginFailure(-2) ];
})
);
}
})
);
};
This part test if the mail adress is empty and works just fine (Or at least just as expected):
it("empty mail address", () => {
testScheduler.run(({ hot, expectObservable }) => {
let action$ = new ActionsObservable(
hot("a", {
a: {
type: LoginActions.LOGIN_PENDING,
mail: ""
}
})
);
let output$ = loginEpic(action$, undefined, { ajax: () => ({}) });
expectObservable(output$).toBe("a", {
a: {
type: LoginActions.LOGIN_FAILURE,
code: -1
}
});
});
});
However, I have this second test that fails because the actual value is an empty array (There is no login failed returned):
it("wrong credentials", () => {
testScheduler.run(({ hot, cold, expectObservable }) => {
let action$ = new ActionsObservable(
hot("a", {
a: {
type: LoginActions.LOGIN_PENDING,
mail: "foo#bar.com"
}
})
);
let dependencies = {
ajax: () =>
from(
new Promise(resolve => {
let response = {
code: -3
};
resolve(response);
})
)
};
let output$ = loginEpic(action$, undefined, dependencies);
expectObservable(output$).toBe("a", {
a: {
type: LoginActions.LOGIN_FAILURE,
code: -3
}
});
});
});
Any idea on what I'm doing wrong / why this part returns an empty array (The console.log does actually log the code):
if (response.code !== 0) {
console.log(response.code);
return [ loginFailure(response.code) ];
}
While this part returns a populated array:
if (action.mail.length === 0) {
return [ loginFailure(-1) ];
}
I'm guessing the use of Promise is causing the test to actually be asynchronous. Try changing the stub of ajax to use of(response) instead of from

Vue test utils: TypeError mounted

I am having an issue with Vue Test Utils. When I run a unit test, I am always confronted with:
TypeError{line: 73983, sourceURL: 'http://localhost:9876/base/index.js?045b00affe888fcd6b346c4fe50eadd13d471383', stack: 'mounted#http://localhost:9876/base/index.js?045b00affe888fcd6b346c4fe50eadd13d471383:73983:30.....
This only happens when I have the mounted() function in the Vue component
Settings.vue
mounted() {
this.$refs.address.update(this.profile.address)
},
Settings.spec.js
it('calls updateUserInformation before mount', () => {
const spy = sinon.spy(Settings.methods, 'updateUserInformation')
shallow(Settings, { propsData })
Vue.nextTick().then(() => {
spy.should.have.calledOnce()
})
})
I am using Mocha & Chai with vue-test-utils. Does anyone know why this is happening?
Thank you in advance!
UPDATE
Settings.vue component HTML
<vue-google-autocomplete
ref="address"
id="map"
classname="input"
placeholder="Address"
v-on:placechanged="getAddressPlaceChanged"
v-on:inputChange="getAddressInputChange"
:country="['sg']"
>
</vue-google-autocomplete>
Settings.vue component Javascript
export default {
components: {
GoogleMaps,
AutoComplete,
VueGoogleAutocomplete,
Partner,
},
watch: {
// whenever school changes, this function will run
school() {
// Check if school value is an empty string or character is lesser than FIX_STR_LENGTH
if (this.school === '' || this.school.length < this.FIX_STR_LENGTH) {
this.removeMarker('school')
}
this.fetchSchools()
},
},
methods: {
async onSubmit() {
// Check if input fields are empty
if (this.address !== undefined && this.phoneNumber !== null && this.school !== '') {
const { placeResultData = {}, addressData = {} } = this.address
let isSuccessful = false
let tempLat = null
let tempLong = null
let tempAddress = null
// Check if address is an empty object
if (_.isEmpty(this.address)) {
const { latlong = {}, address = '' } = this.profile
const [lat, long] = latlong.coordinates
tempLat = lat
tempLong = long
tempAddress = address
} else {
// User changed address location
tempLat = addressData.latitude
tempLong = addressData.longitude
tempAddress = placeResultData.formatted_address
}
// Validate school address array
let tempSchoolAddress = []
if (this.selectedSchool !== null) {
tempSchoolAddress.push(this.selectedSchool.postal_code)
} else {
tempSchoolAddress = this.profile.schoolAddress
}
// Construct user object for registration/update
const user = new User(
this.profile.name,
tempAddress,
tempLat,
tempLong,
tempSchoolAddress,
)
// If user does not exist in database, perform a POST API registration request
if (this.userExist === false) {
// Add user properties for user registration
user.phoneNumber = this.phoneNumber
await UserSession.register(user, localStorage.getItem('id_token')).then((response) => {
const { data = {} } = response
const profile = data.user
this.updateUserInformation(profile)
isSuccessful = true
this.profile = profile
}).catch((error) => {
console.log(error.response)
})
}
// Perform a PUT API update request
await UserSession.update(user, localStorage.getItem('id_token')).then((response) => {
const { data = {} } = response
const profile = data.user
this.updateUserInformation(profile)
isSuccessful = true
this.profile = profile
}).catch((error) => {
console.log(error.response)
})
if (isSuccessful) {
this.profileChanged()
this.hasChanged = true
}
}
},
profileChanged() {
this.$emit('profileChanged', this.profile)
},
addMarker(name, params) {
if (params === null || params === '') {
return
}
gMapSession.default(params).then((response) => {
const { location = {} } = response.data.results[0].geometry
// Remove existing marker before replacing it
this.removeMarker(name)
this.markers.push({
position: location,
name,
})
this.zoom = 11
}).catch((error) => {
console.log(error.response)
})
},
removeMarker(name) {
let index = 0
let exist = false
for (let i = 0; i < this.markers.length; i++) {
if (this.markers[i].name === name) {
index = i
exist = true
break
}
}
if (exist) {
this.markers.splice(index, 1)
}
},
// Function called when user selects an option from the school autocomplete dropdown
getSelectedSchoolData(event) {
this.selectedSchool = event
// Check if selected school is defined
if (this.selectedSchool !== undefined && this.selectedSchool !== null) {
this.addMarker('school', this.selectedSchool.postal_code)
} else {
this.removeMarker('school')
}
},
// Function called when user types in the address autocomplete input field
getAddressInputChange(data) {
const { newVal = {} } = data
if (newVal === '' || newVal.length < this.FIX_STR_LENGTH) {
this.removeMarker('user')
}
},
// Function called when user selects an option from the address autocomplete dropdown
getAddressPlaceChanged(addressData, placeResultData) {
this.address = {
placeResultData,
addressData,
}
if (addressData !== undefined && addressData !== null) {
this.addMarker('user', addressData.postal_code)
} else {
this.removeMarker('user')
}
},
async updateUserInformation(profile) {
this.phoneNumber = profile.phoneNumber
this.addMarker('user', profile.address)
// TODO: schoolAddress is an array and UI must cater to multiple schools
await SchoolSession.default(profile.schoolAddress[0]).then(async (res) => {
const { result = {} } = res.data
const { records = [] } = result
// Assume that a single postal code contains only 1 school
this.school = records[0].school_name
this.addMarker('school', records[0].postal_code)
})
},
// Fetch school information base on school search query
fetchSchools: _.debounce(function getSchools() {
if (this.school.trim() === '') {
this.schools = []
return
}
const vm = this
SchoolSession.default(this.school).then((response) => {
// JSON responses are automatically parsed.
const { records = {} } = response.data.result
vm.schools = records
}).catch((error) => {
console.log(error.response)
})
}, 500),
},
data() {
return {
FIX_STR_LENGTH: 5,
school: '',
address: '',
schools: [],
markers: [],
phoneNumber: null,
selectedSchool: null,
userExist: false,
hasChanged: false,
center: { lat: 1.3521, lng: 103.8198 },
zoom: 7,
}
},
async created() {
this.profile = this.$attrs
// Check if user was a registered member by phone number
if (this.profile.phoneNumber === undefined) {
return
}
// User exist in the database
this.userExist = !this.userExist
// Update form information
this.updateUserInformation(this.profile)
},
mounted() {
this.$refs.address.update(this.profile.address)
},
}

Categories

Resources