Introduce group basic federation, event new page and notifications
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,57 +1,45 @@
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { ApolloLink, Observable, split } from 'apollo-link';
|
||||
import { defaultDataIdFromObject, InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
|
||||
import { onError } from 'apollo-link-error';
|
||||
import { createLink } from 'apollo-absinthe-upload-link';
|
||||
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { buildCurrentUserResolver } from '@/apollo/user';
|
||||
import { isServerError } from '@/types/apollo';
|
||||
import { REFRESH_TOKEN } from '@/graphql/auth';
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants';
|
||||
import { logout, saveTokenData } from '@/utils/auth';
|
||||
import { SnackbarProgrammatic as Snackbar } from 'buefy';
|
||||
import { defaultError, errors, IError, refreshSuggestion } from '@/utils/errors';
|
||||
import { Socket as PhoenixSocket } from 'phoenix';
|
||||
import * as AbsintheSocket from '@absinthe/socket';
|
||||
import { createAbsintheSocketLink } from '@absinthe/socket-apollo-link';
|
||||
import { getMainDefinition } from 'apollo-utilities';
|
||||
import Vue from "vue";
|
||||
import VueApollo from "vue-apollo";
|
||||
import { ApolloLink, Observable, split } from "apollo-link";
|
||||
import {
|
||||
defaultDataIdFromObject,
|
||||
InMemoryCache,
|
||||
NormalizedCacheObject,
|
||||
} from "apollo-cache-inmemory";
|
||||
import { onError } from "apollo-link-error";
|
||||
import { createLink } from "apollo-absinthe-upload-link";
|
||||
import { ApolloClient } from "apollo-client";
|
||||
import buildCurrentUserResolver from "@/apollo/user";
|
||||
import { isServerError } from "@/types/apollo";
|
||||
import { AUTH_ACCESS_TOKEN } from "@/constants";
|
||||
import { logout } from "@/utils/auth";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { Socket as PhoenixSocket } from "phoenix";
|
||||
import * as AbsintheSocket from "@absinthe/socket";
|
||||
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
|
||||
import { getMainDefinition } from "apollo-utilities";
|
||||
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from "./api/_entrypoint";
|
||||
import { computeErrorMessage, fragmentMatcher, refreshAccessToken } from "./apollo/utils";
|
||||
|
||||
// Install the vue plugin
|
||||
Vue.use(VueApollo);
|
||||
|
||||
// Endpoints
|
||||
const httpServer = GRAPHQL_API_ENDPOINT || 'http://localhost:4000';
|
||||
const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`;
|
||||
const webSocketPrefix = process.env.NODE_ENV === 'production' ? 'wss' : 'ws';
|
||||
const wsEndpoint = `${webSocketPrefix}${httpServer.substring(httpServer.indexOf(':'))}/graphql_socket`;
|
||||
let refreshingTokenPromise: Promise<boolean> | undefined;
|
||||
let alreadyRefreshedToken = false;
|
||||
|
||||
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData: {
|
||||
__schema: {
|
||||
types: [
|
||||
{
|
||||
kind: 'UNION',
|
||||
name: 'SearchResult',
|
||||
possibleTypes: [
|
||||
{ name: 'Event' },
|
||||
{ name: 'Person' },
|
||||
{ name: 'Group' },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'Actor',
|
||||
possibleTypes: [
|
||||
{ name: 'Person' },
|
||||
{ name: 'Group' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
// Endpoints
|
||||
const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
|
||||
const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`;
|
||||
const webSocketPrefix = process.env.NODE_ENV === "production" ? "wss" : "ws";
|
||||
const wsEndpoint = `${webSocketPrefix}${httpServer.substring(
|
||||
httpServer.indexOf(":")
|
||||
)}/graphql_socket`;
|
||||
|
||||
function generateTokenHeader() {
|
||||
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
|
||||
return token ? `Bearer ${token}` : null;
|
||||
}
|
||||
|
||||
const authMiddleware = new ApolloLink((operation, forward) => {
|
||||
// add the authorization to the headers
|
||||
@@ -66,11 +54,36 @@ const authMiddleware = new ApolloLink((operation, forward) => {
|
||||
return null;
|
||||
});
|
||||
|
||||
let refreshingTokenPromise: Promise<boolean> | undefined;
|
||||
let alreadyRefreshedToken = false;
|
||||
const uploadLink = createLink({
|
||||
uri: httpEndpoint,
|
||||
});
|
||||
|
||||
const phoenixSocket = new PhoenixSocket(wsEndpoint, {
|
||||
params: () => {
|
||||
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
if (token) {
|
||||
return { token };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
const absintheSocket = AbsintheSocket.create(phoenixSocket);
|
||||
const wsLink = createAbsintheSocketLink(absintheSocket);
|
||||
|
||||
const link = split(
|
||||
// split based on operation type
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return definition.kind === "OperationDefinition" && definition.operation === "subscription";
|
||||
},
|
||||
wsLink,
|
||||
uploadLink
|
||||
);
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => {
|
||||
if (isServerError(networkError) && networkError.statusCode === 401 && !alreadyRefreshedToken) {
|
||||
if (!refreshingTokenPromise) refreshingTokenPromise = refreshAccessToken();
|
||||
if (!refreshingTokenPromise) refreshingTokenPromise = refreshAccessToken(apolloClient);
|
||||
|
||||
return promiseToObservable(refreshingTokenPromise).flatMap(() => {
|
||||
refreshingTokenPromise = undefined;
|
||||
@@ -96,150 +109,56 @@ const errorLink = onError(({ graphQLErrors, networkError, forward, operation })
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
const computedMessage = computeErrorMessage(message);
|
||||
if (computedMessage) {
|
||||
console.log('computed message', computedMessage);
|
||||
console.log("computed message", computedMessage);
|
||||
messages.add(computedMessage);
|
||||
}
|
||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
|
||||
});
|
||||
|
||||
// for (const message of messages) {
|
||||
// Snackbar.open({ message, type: 'is-danger', position: 'is-bottom' });
|
||||
// }
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
console.log(`[Network error]: ${networkError}`);
|
||||
const computedMessage = computeErrorMessage(networkError);
|
||||
if (computedMessage) {
|
||||
Snackbar.open({ message: computedMessage, type: 'is-danger', position: 'is-bottom' });
|
||||
Snackbar.open({ message: computedMessage, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const computeErrorMessage = (message) => {
|
||||
const error: IError = errors.reduce((acc, error) => {
|
||||
if (RegExp(error.match).test(message)) {
|
||||
return error;
|
||||
}
|
||||
return acc;
|
||||
}, defaultError);
|
||||
|
||||
if (error.value === null) return null;
|
||||
return error.suggestRefresh === false ? error.value : `${error.value}<br>${refreshSuggestion}`;
|
||||
};
|
||||
|
||||
const uploadLink = createLink({
|
||||
uri: httpEndpoint,
|
||||
});
|
||||
|
||||
const phoenixSocket = new PhoenixSocket(wsEndpoint, {
|
||||
params: () => {
|
||||
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
if (token) {
|
||||
return { token };
|
||||
}
|
||||
return {};
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
const absintheSocket = AbsintheSocket.create(phoenixSocket);
|
||||
const wsLink = createAbsintheSocketLink(absintheSocket);
|
||||
|
||||
const link = split(
|
||||
// split based on operation type
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription';
|
||||
},
|
||||
wsLink,
|
||||
uploadLink,
|
||||
);
|
||||
|
||||
const fullLink = authMiddleware
|
||||
.concat(errorLink)
|
||||
.concat(link);
|
||||
const fullLink = authMiddleware.concat(errorLink).concat(link);
|
||||
|
||||
const cache = new InMemoryCache({
|
||||
fragmentMatcher,
|
||||
dataIdFromObject: object => {
|
||||
if (object.__typename === 'Address') {
|
||||
// @ts-ignore
|
||||
dataIdFromObject: (object: any) => {
|
||||
if (object.__typename === "Address") {
|
||||
return object.origin_id;
|
||||
}
|
||||
return defaultDataIdFromObject(object);
|
||||
},
|
||||
});
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
const apolloClient = new ApolloClient<NormalizedCacheObject>({
|
||||
cache,
|
||||
link: fullLink,
|
||||
connectToDevTools: true,
|
||||
resolvers: buildCurrentUserResolver(cache),
|
||||
});
|
||||
|
||||
export const apolloProvider = new VueApollo({
|
||||
export default new VueApollo({
|
||||
defaultClient: apolloClient,
|
||||
errorHandler(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message);
|
||||
console.log(
|
||||
"%cError",
|
||||
"background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;",
|
||||
error.message
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Manually call this when user log in
|
||||
export function onLogin(apolloClient) {
|
||||
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
|
||||
}
|
||||
|
||||
// Manually call this when user log out
|
||||
export async function onLogout() {
|
||||
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
|
||||
|
||||
// We don't reset store because we rely on currentUser & currentActor
|
||||
// which are in the cache (even null). Maybe try to rerun cache init after resetStore ?
|
||||
// try {
|
||||
// await apolloClient.resetStore();
|
||||
// } catch (e) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
|
||||
// }
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
// Remove invalid access token, so the next request is not authenticated
|
||||
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
||||
|
||||
const refreshToken = localStorage.getItem(AUTH_REFRESH_TOKEN);
|
||||
|
||||
console.log('Refreshing access token.');
|
||||
|
||||
try {
|
||||
const res = await apolloClient.mutate({
|
||||
mutation: REFRESH_TOKEN,
|
||||
variables: {
|
||||
refreshToken,
|
||||
},
|
||||
});
|
||||
|
||||
saveTokenData(res.data.refreshToken);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function generateTokenHeader() {
|
||||
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
|
||||
return token ? `Bearer ${token}` : null;
|
||||
}
|
||||
|
||||
// Thanks: https://github.com/apollographql/apollo-link/issues/747#issuecomment-502676676
|
||||
const promiseToObservable = <T> (promise: Promise<T>) => {
|
||||
return new Observable<T>((subscriber) => {
|
||||
const promiseToObservable = <T>(promise: Promise<T>) =>
|
||||
new Observable<T>((subscriber) => {
|
||||
promise.then(
|
||||
(value) => {
|
||||
if (subscriber.closed) {
|
||||
@@ -249,11 +168,28 @@ const promiseToObservable = <T> (promise: Promise<T>) => {
|
||||
subscriber.complete();
|
||||
},
|
||||
(err) => {
|
||||
console.error('Cannot refresh token.', err);
|
||||
console.error("Cannot refresh token.", err);
|
||||
|
||||
subscriber.error(err);
|
||||
logout(apolloClient);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Manually call this when user log in
|
||||
// export function onLogin(apolloClient) {
|
||||
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
|
||||
// }
|
||||
|
||||
// Manually call this when user log out
|
||||
// export async function onLogout() {
|
||||
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
|
||||
// We don't reset store because we rely on currentUser & currentActor
|
||||
// which are in the cache (even null). Maybe try to rerun cache init after resetStore?
|
||||
// try {
|
||||
// await apolloClient.resetStore();
|
||||
// } catch (e) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
|
||||
// }
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user