Vue3 Composition API Apollo Client userMutation() in watch function - vue.js

I am using Vue3 with the composition API and Apollo client.
I use an useQuery() function to query some data. With it I use a watcher like watch(result, (newVal) => {})
What I want to do is, using the useMutation() inside this watcher. But it does not work at all.
This is the error message I get:
Uncaught (in promise) Error: Apollo client with id default not found. Use provideApolloClient() if you are outside of a component setup.
at resolveClient (useApolloClient.ts:68:13)
at mutate (useMutation.ts:78:20)
at VerifyEmailView.vue:31:9
at callWithErrorHandling (runtime-core.esm-bundler.js:155:22)
at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:164:21)
at job (runtime-core.esm-bundler.js:1776:17)
at callWithErrorHandling (runtime-core.esm-bundler.js:155:36)
at flushJobs (runtime-core.esm-bundler.js:388:17)
This is the code I use
import { useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import { useMutation, useQuery } from '#vue/apollo-composable'
import { useSessionStore } from '#/stores/SessionStore'
import UPDATE_NFCUSER_EMAIL_VERIFICATION_CODE from '#/graphql/UpdateNfcUserEmailVeriifcationCode.mutation.gql'
import GET_NFCUSER_ID_BY_EMAIL_VERIFICATION_CODE from '#/graphql/GetNfcUserIdByEmailVerificationCode.query.gql'
import { watch, ref } from 'vue'
const sessionStore = useSessionStore()
const router = useRouter()
const route = useRoute()
const route_verification_code = route.params.verification_code
const code_not_verified = ref(false)
const { result } = useQuery(GET_NFCUSER_ID_BY_EMAIL_VERIFICATION_CODE, {
verification_code: route_verification_code,
})
watch(result, (newVal) => {
if (newVal?.getNfcUserIdByEmailVerificationCode?.status === 200) {
const nfc_user_id = newVal?.getNfcUserIdByEmailVerificationCode?.nfc_user_id
if (nfc_user_id) {
console.log('Verification Code is valid')
sessionStore.setCurrentUserVerified(true)
sessionStore.setCurrentNfcUserId(nfc_user_id)
const { mutate } = useMutation(UPDATE_NFCUSER_EMAIL_VERIFICATION_CODE)
mutate({
nfcUserId: nfc_user_id,
})
} else {
console.log('Code is not correct')
code_not_verified.value = true
}
} else {
console.log('Code is not correct')
code_not_verified.value = true
}
})

Looking at the error it seems you would want to set the mutation variable outside of the watch() the same way you're using the query;
const { mutate: updateNfcUserEmailVerificationCode } = useMutation(UPDATE_NFCUSER_EMAIL_VERIFICATION_CODE);
// watch function
updateNfcUserEmailVerificationCode({ nfcUserId: nfc_user_id });
If you did want to use it in the watch() function it looks like you'll have to use the Apollo client directly: https://v4.apollo.vuejs.org/guide-option/mutations.html
// watch function
this.$apollo.mutate({
mutation: UPDATE_NFCUSER_EMAIL_VERIFICATION_CODE,
variables: { nfcUserId: nfc_user_id },
});

Related

CASL - Vue 3 - Element not showing for role

I am having a bit of a challenge implementing CASL in my app.
I have created the following composable useAppAbility ("hook") that defines all the rules:
import { AbilityBuilder, createMongoAbility, subject } from "#casl/ability";
import { useAbility } from "#casl/vue";
const service = {};
const user = {};
const subscription = {};
const invoice = {};
const account = {};
const ability = createMongoAbility();
const ROLES = ["admin", "account_owner", "beneficiary", "super_admin"];
const defineAbilityFor = (role: Object) => {
const { can, rules } = new AbilityBuilder(createMongoAbility);
const is = (r: string) => {
return ROLES.indexOf(r) >= ROLES.indexOf(role);
};
if (is("admin")) {
can("add", subject("User", user));
can("remove", subject("User", user));
}
return ability.update(rules);
};
export { defineAbilityFor, ability, subject };
export const useAppAbility = () => useAbility();
Added the plugin to the main.ts:
import { ability } from "#/composables/useAppAbility";
import { abilitiesPlugin } from "#casl/vue";
createApp(App)
.use(abilitiesPlugin, ability, {
useGlobalProperties: true,
})
//stuff
.mount("#app");
And then, I found that using the beforeEach hook in the router and passing in the user before each route was the simplest way to deal with page load and SPA routing.
I have therefore added the following to my router/index.ts:
import { ability, defineAbilityFor } from "#/composables/useAppAbility";
import useAuth from "#/composables/useAuth";
const {
getUserByClaims,
} = useAuth();
// routes
router.beforeEach(async (to, _from, next) => {
defineAbilityFor(getUserByClaims.value.roles)
})
At this stage I can verify that the user is being passed properly to the defineAbilityFor function and when using the ability.on("update") hook to log the rules object, I have the following output:
Which seems to confirm that the rules for this user are built and updated correctly?
However, when trying to display a button for the said admin in a component, the button does not show.
MyComponent.vue:
<script setup lang="ts">
import { useAppAbility, subject } from "#/composables/useAppAbility";
const { can } = useAppAbility();
</script>
<template>
<div v-if="can('add', subject('User', {}))">TEST FOR CASL</div> <!-- DOES NOT SHOW-->
</template>
Not sure where to go from there, any help would be appreciated.
Thanks

How to import a utility function that sets cookies into multiple Vue components

I have several components that I need to check if the user logged on/has valid access token
I currently do check this inside a Vue component method using the contents of isLoggedOut function below. I am thinking that I might need to create an external js file and import this js everywhere that I need to check of credentials. So js function will look sthg like below. However this function also resets the cookies in the component. see this.$cookies. I don't think this is possible due to scoping.
So how can I import functions (like from a utility js file) that also changes this objects? Or is there a better way of what avoiding code duplication in Vue/check for log out in multiple components using same code
import axios from "axios";
function isLoggedOut() {
axios.defaults.withCredentials = true;
const isLoggedOut = True;
const path = `/user_authentication/protected`;
axios
.get(path, { withCredentials: true })
.then((response) => {
message = response.data["user"];
isLoggedOut = false;
return isLoggedOut;
})
.catch((error) => {
console.error(error);
this.$cookies.remove("csrf_access_token");
isLoggedOut = true;
return isLoggedOut;
});
}
Create an index.ts file in a folder named utils and export the funtion isLoggedOut.
Pass the Vue app to the function isLoggedOut as a prop and call the vue methods.
import Vue from 'vue'
import axios from "axios";
export function isLoggedOut(app: Vue) {
axios.defaults.withCredentials = true;
const isLoggedOut = True;
const path = `/user_authentication/protected`;
axios
.get(path, { withCredentials: true })
.then((response) => {
message = response.data["user"];
isLoggedOut = false;
return isLoggedOut;
})
.catch((error) => {
console.error(error);
app.$cookies.remove("csrf_access_token");
isLoggedOut = true;
return isLoggedOut;
});
}
Component
import { isLoggedOut } from '~/utils'
export default {
methods: {
logOut() {
// Passing the Vue app
isLoggedOut(this)
}
}
}

Upgraded to Vue 2.7 and now getting a bunch of warnings: [Vue warn]: Vue 2 does not support readonly arrays

Background
I recently upgraded from Vue v2.6.14 to Vue 2.7 by following this guide: https://blog.vuejs.org/posts/vue-2-7-naruto.html.
I made some changes (e.g., removing #vue/composition-api and vue-template-compiler, upgrading to vuex-composition-helpers#next, etc.).
Problem
The application loads for the most part, but now I get a ton of console errors:
[Vue warn]: Vue 2 does not support readonly arrays.
It looks like even just console.log(workspaces.value); (see code below) raises the warning.
Question
How do I resolve this issue?
Thank you!
Code
<script lang="ts">
import {
defineComponent,
onMounted,
computed,
} from 'vue';
import { createNamespacedHelpers } from 'vuex-composition-helpers';
import {
modules,
actionTypes,
getterTypes,
} from '#/store/types';
import _ from 'lodash';
const workspaceModule = createNamespacedHelpers(modules.WORKSPACE_MODULE);
export default defineComponent({
setup() {
const { newWorkspace, listWorkspaces } = workspaceModule.useActions([
actionTypes.WorkspaceModule.NEW_WORKSPACE,
actionTypes.WorkspaceModule.LIST_WORKSPACES,
]);
const { workspaces } = workspaceModule.useGetters([
getterTypes.WorkspaceModule.GET_WORKSPACES,
]);
onMounted(async () => {
await listWorkspaces({
Archived: false,
Removed: false,
});
console.log(workspaces.value);
});
return {
/*
workspacesSorted: computed(() => {
return _.orderBy(workspaces.value, ['LastUpdated'], ['desc']);
}),
*/
}
}
});
</script>
src/store/modules/workspace/getters.ts
import { GetterTree } from 'vuex';
import { WorkspaceState } from './types';
import { RootState } from '../../types';
import { getterTypes } from '../../types';
export const getters: GetterTree<WorkspaceState, RootState> = {
[getterTypes.WorkspaceModule.GET_WORKSPACES](context: WorkspaceState) {
return context.Workspaces;
},
[getterTypes.WorkspaceModule.GET_ALL_WORKSPACES](context: WorkspaceState) {
return context.AllWorkspaces;
}
}
src/store/modules/workspace/actions.ts
export const actions: ActionTree<WorkspaceState, RootState> = {
async [actionTypes.WorkspaceModule.LIST_WORKSPACES]({ commit }, payload: ListWorkspace) {
const wss = await list(payload.Archived, payload.Removed);
wss.forEach((ws) => {
ws.Archived = payload.Archived;
ws.Removed = payload.Removed;
});
commit(mutationTypes.WorkspaceModule.SET_WORKSPACES, wss);
},
};
src/store/modules/workspace/actions.ts
export const mutations: MutationTree<WorkspaceState> = {
[mutationTypes.WorkspaceModule.SET_WORKSPACES](ctx: WorkspaceState, wss: Workspace[]) {
ctx.Workspaces = wss;
},
};
src/service/useWorkspace.ts
const list = async(archived: boolean, removed: boolean) => {
const res = await get<Workspace[], AxiosResponse<Workspace[]>>('/workspace/list', {
params: {
archived,
removed,
}
});
return success(res);
};
When I call store.state.WorkspaceModule.Workspaces directly (either in the console or in computed), I get no errors:
import { useStore } from '#/store';
export default defineComponent({
setup() {
const store = useStore();
onMounted(async () => {
await listWorkspaces({
Archived: false,
Removed: false,
});
console.log(store.state.WorkspaceModule.Workspaces);
});
return {
workspacesSorted: computed(() =>
store.state.WorkspaceModule.Workspaces
),
}
}
});
This might be because workspaces is based on a getter, which are read-only. As mentioned in the blog you were referring to, readonly is not supported for arrays in Vue 2.7:
readonly() does create a separate object, but it won't track newly added properties and does not work on arrays.
It was (partially) supported for arrays in the Vue 2.6 Composition Api Plugin though:
readonly() provides only type-level readonly check.
So that might be causing the error. If it is mandatory for you, you might need to upgrade to vue3, or stick with 2.6 for a while. The composition Api plugin is maintained until the end of this year...
A workaround may be to skip the getter and access the state directly, since it is a quite simple getter which only returns the current state of Workspaces.
Hope this helps.

Cannot use Vue-Router to get the parameters in the URL

Today, when trying to use Vue-Router (in Vue-CLI) to get URL parameters, I encountered difficulties ($route.query is empty), the code is as follows.
Code purpose: Get the parameters carried after the URL (such as client_id in "http://localhost:8080/#/?client_id=00000000000077")
Project file structure:
router/index.js:
App.vue(Get part of the code for URL parameters):
The running result of this part of the code:
I'm not sure why $router.currentRoute and $route aren't matching up, but you could simply use $router.currentRoute.query.client_id if you need it in mounted().
Another workaround is to use a $watch on $route.query.client_id:
export default {
mounted() {
const unwatch = this.$watch('$route.query.client_id', clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}
Or watch in the Composition API:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
mounted() {
console.log({
route: this.$route,
router: this.$router,
})
},
setup() {
const route = useRoute()
const unwatch = watch(() => route.query.client_id, clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}

How to get item by Id using Vue 3 Composition API using Axios

Im struggling using Axios and useRoute using Axios with Composition API. Here is the code on how to do it in using the Option API, how do I recreate it, the Vue-router docs not well documented at all right now.
async created() {
const result = await axios.get(`https://localhost:5001/api/artists/${this.$route.params.id}`
);
const artist = result.data;
this.artist = artist;
},
To convert that code to Composition API:
created() hook is effectively the same timing as setup(), so put that code in setup().
this.$route can be accessed via useRoute() (from vue-router#4).
artist can be declared as a data ref, returning it from setup() if used in the template.
To reactively fetch data from the API based on the id parameter, use watch on route.params.id. This watch call returns a function that stops the watcher, which is useful if you need to conditionally unwatch the reference.
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
export default {
// 1
setup() {
const route = useRoute() // 2
const artist = ref(null) // 3
// 4
const unwatch = watch(() => route.params.id, (newId, oldId) => {
const result = await axios.get(`https://localhost:5001/api/artists/${newId}`)
artist.value = result.data
// run only once
unwatch()
})
return {
artist
}
}
}