Nuxt 3 - Server not ready on mount, Volar engine confused - vue.js

I have two issues that may or may not be related.
Overview
Folder Structure
pages
|---user.vue
server
|---api
|---profile.get.ts
|---profile.post.ts
The tech is Nuxt3 using the server and Supabase.
profile.get.ts
import { serverSupabaseClient, serverSupabaseUser } from "#supabase/server"
import { Database } from "~~/types/supabase"
export default defineEventHandler(async (event) => {
try {
const supabase = serverSupabaseClient<Database>(event)
const user = await serverSupabaseUser(event)
const query = getQuery(event)
const { data, error } = await supabase.from('profiles').select('*').eq('email', query.email).single()
if (error) throw { status: error.code, message: error.message }
return { displayName: data.display_name, avatarUrl: data.avatar_url }
} catch (err) {
console.error('Handled Error:', err)
}
})
profile.post.ts
import { serverSupabaseClient } from "#supabase/server"
import { Database } from "~~/types/supabase"
export default defineEventHandler(async (event) => {
const supabase = serverSupabaseClient<Database>(event)
const { displayName, avatarUrl, email }: { displayName: string, avatarUrl: string, email: string } = await readBody(event)
const { error } = await supabase.from('profiles').update({ display_name: displayName, avatar_url: avatarUrl }).match({ email })
if (error) throw new Error(error.message)
return { status: 200 }
})
user.vue Snippet
onMounted(() => {
setTimeout(() => {
getProfile()
}, 100) // Fails when around 50 or less
})
async function getProfile() {
const { data, error } = await useFetch('/api/profile', { method: 'GET', params: { email: user.value?.email } })
console.log(data.value)
console.log(error.value)
displayName.value = data.value!.displayName || ''
avatarUrl.value = data.value!.avatarUrl || ''
}
Problem 1
When user.vue mounts, I want to call my Nuxt API (profile.get.ts) and fetch user data (display name, avatar url) from the Supabase database. However, I receive this error when fetching on mount: FetchError: 404 Cannot find any route matching /api/profile. (/api/profile). However, if I use setTimeout to 100ms, it fetches fine. That makes me think the API server is simply not ready, but the documentation doesn't mention that and encourages fetching during lifecycle.
Problem 2
Volar seems to be confused about the typing of data from getProfile().
Property 'displayName' does not exist on type '{ status: number; } | { displayName: string | null; avatarUrl: string | null; }'.
Property 'displayName' does not exist on type '{ status: number; }'.ts(2339)
However, this is the typing from profile.post.ts even though I'm using profile.get.ts.
Current Behavior
Without setTimeout at 100ms or greater, it will fail with the 404 message
With setTimeout at 100ms or greater, or with getProfile() called from a button, there is no issue, even with the TypeScript errors, etc.
Desired Behavior
TypeScript correctly recognizes the proper endpoint (profiles.get.ts since I'm calling it with get)
Data can be fetched on mount from the API without the use of setTimeout

Related

Cancelling upload request before destroying object makes mobx-state-tree throw Cannot modify [dead] errors

I have a React Native app where I want to upload some files using Axios.
I've made a mobx-state-tree store for file uploads, and each file has its own CancelTokenSource, which is sent to the Axios network call.
When an upload is in progress, I try to cancel the upload, and then destroy the item.
The simplest way is like I show below, by destroying the item in the store, and then have an beforeDestroy() hook that cancels the upload. But that approach makes mobx-state-tree show the error in the screenshot.
I've also tried calling the file.cancelTokenSource.cancel() explicitly before destroying the item. Same error. I suspect that the operation is not fully cancelled when the cancel() returns, but since it's not an async function, I cannot await its completion.
When I just call the cancel() without destroying, it cancels just fine, so I'm pretty sure that it's a timing issue, where the destroy(file) is called too soon, before cancel() has cleaned up after itself.
What to do here?
file-upload-store.ts
import { destroy, flow, Instance, types } from 'mobx-state-tree'
import { FileUpload, IFileUpload } from '../entities/file-upload/file-upload'
import { getApi } from '../../store-environment'
/**
* Store for handling the FileUpload
*/
export const FileUploadStore = types
.model('FileUploadStore')
.props({
files: types.array(FileUpload),
})
.actions((self) => {
const api = getApi(self)
const add = (uri: string, name: string, type: string, size: number) => {
const file = FileUpload.create({
uri,
name,
type,
size,
})
self.files.push(file)
upload(file)
}
const remove = (file: IFileUpload) => {
destroy(file)
}
const cancel = (file: IFileUpload) => {
// also tried this - with no luck
// file.cancelTokenSource.cancel()
destroy(file)
}
const upload = flow(function* (file: IFileUpload) {
file.status = 'pending'
file.uploadedBytes = 0
const { uri, name, type } = file
try {
const id = yield api.uploadFile(uri, name, type, file.setProgress, file.cancelTokenSource.token)
file.status = 'completed'
file.fileUploadId = id
} catch (error) {
file.status = 'failed'
file.error = error.message
}
})
return {
afterCreate() {
// Avoid persistance
self.files.clear()
},
remove,
cancel,
retry: upload,
add,
}
})
export type IFileUploadStore = Instance<typeof FileUploadStore>
file-upload.ts
import { Instance, SnapshotIn, types } from 'mobx-state-tree'
import { CancelToken } from 'apisauce'
/**
* FileUpload contains the particular data of a file, and some flags describing its status.
*/
export const FileUpload = types
.model('FileUpload')
.props({
name: types.string,
type: types.string,
uri: types.string,
size: types.number,
// set if an arror occours
error: types.maybe(types.string),
status: types.optional(types.enumeration(['pending', 'completed', 'failed']), 'pending'),
// updated by progressCallback
uploadedBytes: types.optional(types.number, 0),
// assigned when response from backend is received
fileUploadId: types.maybe(types.string),
})
.volatile(() => ({
cancelTokenSource: CancelToken.source(),
}))
.actions((self) => ({
setProgress(event: ProgressEvent) {
self.uploadedBytes = event.loaded
},
beforeDestroy() {
self.cancelTokenSource?.cancel()
},
}))
export interface IFileUpload extends Instance<typeof FileUpload> {}
// SnapshotIn, used for creating input to store: {Model}.create({})
export interface IFileUploadSnapshotIn extends SnapshotIn<typeof FileUpload> {}
You are destroying the FileUpload node and cancelling the axios request nicely, but cancelling the request will throw an error, so you need to make sure that your FileUpload node is still alive before you try to update it in the catch.
import { destroy, flow, Instance, types, isAlive } from 'mobx-state-tree'
// ...
const upload = flow(function* (file: IFileUpload) {
const { uri, name, type } = file
file.status = "pending"
file.uploadedBytes = 0
try {
const id = yield api.uploadFile(
uri,
name,
type,
file.setProgress,
file.cancelTokenSource.token
)
file.status = "completed"
file.fileUploadId = id
} catch (error) {
if (isAlive(file)) {
file.status = "failed"
file.error = error.message
}
}
})

Conditionally execute a graphql mutation after a query is fetched

Scenario
When a user is authenticated (isAuthenticated booelan ref):
Check if a user has preferences by a graphql call to the backend (useViewerQuery)
If there are no preferences for the user set the default (useSetPreferenceDefaultMutation)
Problem
Both the query and the mutation work correctly in the graphql Playground and in the Vue app. They have been generated with the graphql codegenerator which uses useQuery and useMutation in the background.
The issue we're having is that we can't define the correct order. Sometimes useSetPreferenceDefaultMutation is executed before useViewerQuery. This resets the user's settings to the defaults and it not the desired behavior.
Also, on a page refresh all is working correctly. However, when closing an reopening the page it always calls useSetPreferenceDefaultMutation.
Code
export default defineComponent({
setup() {
const {
result: queryResult,
loading: queryLoading,
error: queryError,
} = useViewerQuery(() => ({
enabled: isAuthenticated.value,
}))
const {
mutate: setDefaultPreferences,
loading: mutationLoading,
error: mutationError,
called: mutationCalled,
} = useSetPreferenceDefaultMutation({
variables: {
language: 'en-us',
darkMode: false,
},
})
onMounted(() => {
watchEffect(() => {
if (
isAuthenticated.value &&
!queryLoading.value &&
!queryResult.value?.viewer?.preference &&
!mutationCalled.value
) {
void setDefaultPreferences()
}
})
})
return {
isAuthenticated,
loading: queryLoading || mutationLoading,
error: queryError || mutationError,
}
},
})
Failed efforts
We opened an issue here and here to have extra options on useQuery or useMutation which could help in our scenario but no luck.
Use fetch option with sync or post on watchEffect
Use watch instead of watchEffect
Thanks to comment from #xadm it's fixed now by using the onResult event hook on the query, so it will execute the mutation afterwards.
onResult(handler): Event hook called when a new result is available.
export default defineComponent({
setup(_, { root }) {
const {
loading: queryLoading,
error: queryError,
onResult: onQueryResult,
} = useViewerQuery(() => ({
enabled: isAuthenticated.value,
}))
const {
mutate: setDefaultPreferences,
loading: mutationLoading,
error: mutationError,
} = useSetPreferenceDefaultMutation({
variables: {
language: 'en-us',
darkMode: false,
},
})
onQueryResult((result) => {
if (!result.data.viewer.preference) {
void setDefaultPreferences()
}
})
return {
isAuthenticated,
loading: queryLoading || mutationLoading,
error: queryError || mutationError,
}
},
})

How to use Nuxt Context to call Axios request with param

so I'm trying to get my Axios to do a get request with a param that'll end the url in
'/?user= {id}'
the id is passed in by my loggedInUser.id from Vuex. I know that async functions won't accept 'this' inside the call so I included store as a parameter. Something's still off with how I passed the data around thought I think. Would appreciate any help, thanks!
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters(["loggedInUser"])
},
head() {
return {
title: "Actors list"
};
},
components: {
EditProfile
},
async asyncData({ store }) {
try {
const body = { data: store.getters.loggedInUser.id };
const { actors } = await $axios.$get(`/api/v1/actors/`, {
params: {
user: body
}
});
return { actors };
} catch (e) {
return { actors: [] };
}
},
data() {
return {
actors: []
};
Edit
I got it to work when I removed the data: from 'const body' and removed the brackets as well around 'actor'
try {
const body = store.getters.loggedInUser.id;
const actors = await $axios.$get(`/api/v1/actors/`, {
params: {
user: body
}
});
You can access your params from Context.
Context is available in special nuxt lifecycle areas like asyncData, fetch, plugins, middleware and nuxtServerInit.
In Nuxt, with asyncData hook you can get query parameters from the route context key.
Please read the Nuxt.js Context documentation. The context provides additional objects/params from Nuxt to Vue components
With your-domain/?user=wonderman
asyncData({ route: { query: queryParams} }) {},
variable queryParams is an object:
{ user: "wonderman" }

relay subscription onNext not triggered on react-native

I am a subscription setup but onNext is not getting triggered I am not sure why since this is my first time implementing subscription and docs was not much help with the issue.
Here are the code implementations:
import {
graphql,
requestSubscription
} from 'react-relay'
import environment from '../network';
const subscription = graphql`
subscription chatCreatedSubscription{
chatCreated{
id
initiate_time
update_time
support_id
category_id
email
name
}
}
`;
function chatCreated(callback) {
const variables = {};
requestSubscription(environment, {
subscription,
variables,
onNext: () => {
console.log("onNext");
callback()
},
updater: () => {
console.log("updater");
}
});
}
module.exports = chatCreated;
and here is my network for the subscription
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import Expo from "expo";
import { SubscriptionClient } from "subscriptions-transport-ws";
import { WebSocketLink } from 'apollo-link-ws';
import { execute } from 'apollo-link';
import accessHelper from "../helper/accessToken";
const networkSubscriptions = async (operation, variables) => {
let token = await accessHelper();
if (token != null || token != undefined) {
const subscriptionClient = new SubscriptionClient("ws://localhost:3000/graphql",
{
reconnect: true,
connectionParams: {
Authorization: token,
},
});
execute(new WebSocketLink(subscriptionClient), {
query: operation.text,
variables,
});
}
}
const network = Network.create(fetchQuery, networkSubscriptions);
const store = new Store(new RecordSource());
const environment = new Environment({
network,
store
});
export default environment;
the subscription is called in a componentDidMount method on a component it executes but the onNext method inside the subscription is never triggered when new information is added to what the subscription is listening to.
so i figured out that my issue was the network js not being setup properly and the version of subscription-transport-ws. i added version 0.8.3 of the package and made the following changes to my network file:
const networkSubscriptions = async (config, variables, cacheConfig, observer) => {
const query = config.text;
let token = await accessHelper();
if (token != null || token != undefined) {
const subscriptionClient = new SubscriptionClient(`ws://${api}/graphql`,
{
reconnect: true,
connectionParams: {
Authorization: token,
},
});
subscriptionClient.subscribe({ query, variables }, (error, result) => {
observer.onNext({ data: result })
})
return {
dispose: subscriptionClient.unsubscribe
};
}
}
i hope this helps you if you get stuck with the same issue as mine.

Relay subscriptions not working with react-native

I'm using Express Graphql server with react native and Relay. My device does connects to the subscription but it does not subscribe to it. Here's my index.js on the server
const subscriptionServer = SubscriptionServer.create(
{
execute,
subscribe,
schema,
onOperation: (message, params, webSocket) => {
console.log(params)
return params;
},
onConnect: () => {
// My device does connects
console.log("client connected")
}
},
{
server,
path: '/subscriptions'
},
);
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}));
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
subscriptionsEndpoint: `ws://127.0.0.1:8080/subscriptions`
}));
server.listen(PORT, ()=> {
console.log("Groceries running on port " + PORT)
console.log(
`subscriptions is now running on ws://localhost:${PORT}/subscriptions'}`
);
});
The resolver for subscription on the server, it was quite troublesome to figure out since everyone is using executable schema from apolloGraphql.
export default {
type: OrderEdges,
args: {
ShopId: {type: GraphQLID},
},
subscribe: withFilter(() => pubsub.asyncIterator('orderConfirmed'), (payload, variables) => {
console.log(payload)
console.log(variables)
return payload.orderConfirmed.node.ShopId == variables.ShopId;
}),
}
Now the react-native client. My subscription setup with relay environment.
const setupSubscriptions = (config, variables, cacheConfig, observer) => {
const query = config.text; //does console logs the query
const subscriptionClient = new SubscriptionClient(`ws://192.168.0.100:8080/subscriptions`, {reconnect:true});
subscriptionClient.request({query, variables}, (err, result) => {
console.log(err) // doesn't get call inside the request method
observer.onNext(data:result)
})
}
My subscription method,
export default function() {
const variables = {
ShopId: shop.getShop()[0].id
}
requestSubscription(
environment,
{
subscription,
variables,
onCompleted: (res, err) => {
console.log(res)
console.log(err)
},
updater: (store) => {...},
onError: error => console.error(error),
onNext: (response) => {console.log(response)}
});
}
the component where I'm calling to subscribe,
import subscription from '../../GraphQLQueries/subscriptions/orderConfirmed';
class OrdersBox extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
//initializing subscription
orderSubscriptions();
}
When the device starts the app, my device is connected to the web socket as I can see the console.log statement inside the onConnect method in SubscriptionServer. But when the payload is published after a mutation, the subscribe method doesn't get called. I can't seem to figure out what I'm doing wrong. Maybe it's some react-native specific config that I'm missing cuz everything seems to work fine when I test it on graphiql.
I can't find any example of react-native and relay subscriptions used with express graphql.
note: Everything is working when I use subscription with graphiql. But not with react-native and relay.
Thanks in advance guys
....
I wasn't returning the subscriptionClient.request method. Adding a return statement solved the problem. You don't have to return when using subscribe method in subscriptions-transport-ws#0.8.3. But version 0.9.1 replaces the subscribe function with request which does require it to return.
try:
function setupSubscription(config, variables, cacheConfig, observer) {
const query = config.text;
const subscriptionClient = new SubscriptionClient(websocketURL, {
reconnect: true
});
const client = subscriptionClient.request({ query, variables }).subscribe({
next: result => {
observer.onNext({ data: result.data });
},
complete: () => {
observer.onCompleted();
},
error: error => {
observer.onError(error);
}
});
return {
dispose: client.unsubscribe
};
}
subscriptions-transport-ws#0.9.1