build: switch from yarn to npm to manage js dependencies and move js contents to root
yarn v1 is being deprecated and starts to have some issues Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
25
src/apollo/absinthe-socket-link.ts
Normal file
25
src/apollo/absinthe-socket-link.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Socket as PhoenixSocket } from "phoenix";
|
||||
import { create } from "@framasoft/socket";
|
||||
import { createAbsintheSocketLink } from "@framasoft/socket-apollo-link";
|
||||
import { AUTH_ACCESS_TOKEN } from "@/constants";
|
||||
import { GRAPHQL_API_ENDPOINT } from "@/api/_entrypoint";
|
||||
|
||||
const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
|
||||
|
||||
const webSocketPrefix = import.meta.env.PROD ? "wss" : "ws";
|
||||
const wsEndpoint = `${webSocketPrefix}${httpServer.substring(
|
||||
httpServer.indexOf(":")
|
||||
)}/graphql_socket`;
|
||||
|
||||
const phoenixSocket = new PhoenixSocket(wsEndpoint, {
|
||||
params: () => {
|
||||
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
if (token) {
|
||||
return { token };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
const absintheSocket = create(phoenixSocket);
|
||||
export default createAbsintheSocketLink(absintheSocket);
|
||||
20
src/apollo/absinthe-upload-socket-link.ts
Normal file
20
src/apollo/absinthe-upload-socket-link.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import fetch from "unfetch";
|
||||
import { createLink } from "apollo-absinthe-upload-link";
|
||||
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from "@/api/_entrypoint";
|
||||
|
||||
// Endpoints
|
||||
const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
|
||||
const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`;
|
||||
|
||||
const customFetch = async (uri: string, options: any) => {
|
||||
const response = await fetch(uri, options);
|
||||
if (response.status >= 400) {
|
||||
return Promise.reject(response.status);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
export const uploadLink = createLink({
|
||||
uri: httpEndpoint,
|
||||
fetch: customFetch,
|
||||
});
|
||||
23
src/apollo/auth.ts
Normal file
23
src/apollo/auth.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AUTH_ACCESS_TOKEN } from "@/constants";
|
||||
import { ApolloLink } from "@apollo/client/core";
|
||||
|
||||
export 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
|
||||
operation.setContext({
|
||||
headers: {
|
||||
authorization: generateTokenHeader(),
|
||||
},
|
||||
});
|
||||
|
||||
if (forward) return forward(operation);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export { authMiddleware };
|
||||
101
src/apollo/error-link.ts
Normal file
101
src/apollo/error-link.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { logout } from "@/utils/auth";
|
||||
import { onError } from "@apollo/client/link/error";
|
||||
import { fromPromise } from "@apollo/client/core";
|
||||
import { refreshAccessToken } from "./utils";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { generateTokenHeader } from "./auth";
|
||||
|
||||
let isRefreshing = false;
|
||||
let pendingRequests: any[] = [];
|
||||
|
||||
const resolvePendingRequests = () => {
|
||||
pendingRequests.map((callback) => callback());
|
||||
pendingRequests = [];
|
||||
};
|
||||
|
||||
const isAuthError = (graphQLError: GraphQLError | undefined) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return graphQLError && [403, 401].includes(graphQLError.status_code);
|
||||
};
|
||||
|
||||
const errorLink = onError(
|
||||
({ graphQLErrors, networkError, forward, operation }) => {
|
||||
console.debug("We have an apollo error", [graphQLErrors, networkError]);
|
||||
if (
|
||||
graphQLErrors?.some((graphQLError) => isAuthError(graphQLError)) ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
networkError === 401
|
||||
) {
|
||||
console.debug("It's a authorization error (statusCode 401)");
|
||||
let forwardOperation;
|
||||
|
||||
if (!isRefreshing) {
|
||||
console.debug("Setting isRefreshing to true");
|
||||
isRefreshing = true;
|
||||
|
||||
forwardOperation = fromPromise(
|
||||
refreshAccessToken()
|
||||
.then((res) => {
|
||||
if (res !== true) {
|
||||
// failed to refresh the token
|
||||
throw "Failed to refresh the token";
|
||||
}
|
||||
resolvePendingRequests();
|
||||
|
||||
const context = operation.getContext();
|
||||
const oldHeaders = context.headers;
|
||||
|
||||
operation.setContext({
|
||||
headers: {
|
||||
...oldHeaders,
|
||||
authorization: generateTokenHeader(),
|
||||
},
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.debug("Something failed, let's logout", e);
|
||||
pendingRequests = [];
|
||||
// don't perform a logout since we don't have any working access/refresh tokens
|
||||
logout(false);
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false;
|
||||
})
|
||||
).filter((value) => Boolean(value));
|
||||
} else {
|
||||
forwardOperation = fromPromise(
|
||||
new Promise((resolve) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
pendingRequests.push(() => resolve());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return forwardOperation.flatMap(() => forward(operation));
|
||||
}
|
||||
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.map(
|
||||
(graphQLError: GraphQLError & { status_code?: number }) => {
|
||||
if (graphQLError?.status_code !== 401) {
|
||||
console.debug(
|
||||
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
console.error(`[Network error]: ${networkError}`);
|
||||
console.debug(JSON.stringify(networkError));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default errorLink;
|
||||
40
src/apollo/link.ts
Normal file
40
src/apollo/link.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { split } from "@apollo/client/core";
|
||||
import { RetryLink } from "@apollo/client/link/retry";
|
||||
import { getMainDefinition } from "@apollo/client/utilities";
|
||||
import absintheSocketLink from "./absinthe-socket-link";
|
||||
import { authMiddleware } from "./auth";
|
||||
import errorLink from "./error-link";
|
||||
import { uploadLink } from "./absinthe-upload-socket-link";
|
||||
|
||||
let link;
|
||||
|
||||
// The Absinthe socket Apollo link relies on an old library
|
||||
// (@jumpn/utils-composite) which itself relies on an old
|
||||
// Babel version, which is incompatible with Histoire.
|
||||
// We just don't use the absinthe apollo socket link
|
||||
// in this case.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (!import.meta.env.VITE_HISTOIRE_ENV) {
|
||||
// const absintheSocketLink = await import("./absinthe-socket-link");
|
||||
|
||||
link = split(
|
||||
// split based on operation type
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === "OperationDefinition" &&
|
||||
definition.operation === "subscription"
|
||||
);
|
||||
},
|
||||
absintheSocketLink,
|
||||
uploadLink
|
||||
);
|
||||
}
|
||||
|
||||
const retryLink = new RetryLink();
|
||||
|
||||
export const fullLink = authMiddleware
|
||||
.concat(retryLink)
|
||||
.concat(errorLink)
|
||||
.concat(link ?? uploadLink);
|
||||
14
src/apollo/memory.ts
Normal file
14
src/apollo/memory.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defaultDataIdFromObject, InMemoryCache } from "@apollo/client/core";
|
||||
import { possibleTypes, typePolicies } from "./utils";
|
||||
|
||||
export const cache = new InMemoryCache({
|
||||
addTypename: true,
|
||||
typePolicies,
|
||||
possibleTypes,
|
||||
dataIdFromObject: (object: any) => {
|
||||
if (object.__typename === "Address") {
|
||||
return object.origin_id;
|
||||
}
|
||||
return defaultDataIdFromObject(object);
|
||||
},
|
||||
});
|
||||
137
src/apollo/user.ts
Normal file
137
src/apollo/user.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import { CURRENT_USER_LOCATION_CLIENT } from "@/graphql/location";
|
||||
import { CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { ApolloCache, NormalizedCacheObject } from "@apollo/client/cache";
|
||||
import { Resolvers } from "@apollo/client/core/types";
|
||||
|
||||
export default function buildCurrentUserResolver(
|
||||
cache: ApolloCache<NormalizedCacheObject>
|
||||
): Resolvers {
|
||||
cache?.writeQuery({
|
||||
query: CURRENT_USER_CLIENT,
|
||||
data: {
|
||||
currentUser: {
|
||||
__typename: "CurrentUser",
|
||||
id: null,
|
||||
email: null,
|
||||
isLoggedIn: false,
|
||||
role: ICurrentUserRole.USER,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cache?.writeQuery({
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
data: {
|
||||
currentActor: {
|
||||
__typename: "CurrentActor",
|
||||
id: null,
|
||||
preferredUsername: null,
|
||||
name: null,
|
||||
avatar: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cache?.writeQuery({
|
||||
query: CURRENT_USER_LOCATION_CLIENT,
|
||||
data: {
|
||||
currentUserLocation: {
|
||||
lat: null,
|
||||
lon: null,
|
||||
accuracy: null,
|
||||
isIPLocation: null,
|
||||
name: null,
|
||||
picture: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
Mutation: {
|
||||
updateCurrentUser: (
|
||||
_: any,
|
||||
{
|
||||
id,
|
||||
email,
|
||||
isLoggedIn,
|
||||
role,
|
||||
}: { id: string; email: string; isLoggedIn: boolean; role: string },
|
||||
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
||||
) => {
|
||||
const data = {
|
||||
currentUser: {
|
||||
id,
|
||||
email,
|
||||
isLoggedIn,
|
||||
role,
|
||||
__typename: "CurrentUser",
|
||||
},
|
||||
};
|
||||
|
||||
localCache.writeQuery({ data, query: CURRENT_USER_CLIENT });
|
||||
},
|
||||
updateCurrentActor: (
|
||||
_: any,
|
||||
{
|
||||
id,
|
||||
preferredUsername,
|
||||
avatar,
|
||||
name,
|
||||
}: {
|
||||
id: string;
|
||||
preferredUsername: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
},
|
||||
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
||||
) => {
|
||||
const data = {
|
||||
currentActor: {
|
||||
id,
|
||||
preferredUsername,
|
||||
avatar,
|
||||
name,
|
||||
__typename: "CurrentActor",
|
||||
},
|
||||
};
|
||||
|
||||
localCache.writeQuery({ data, query: CURRENT_ACTOR_CLIENT });
|
||||
},
|
||||
updateCurrentUserLocation: (
|
||||
_: any,
|
||||
{
|
||||
lat,
|
||||
lon,
|
||||
accuracy,
|
||||
isIPLocation,
|
||||
name,
|
||||
picture,
|
||||
}: {
|
||||
lat: number;
|
||||
lon: number;
|
||||
accuracy: number;
|
||||
isIPLocation: boolean;
|
||||
name: string;
|
||||
picture: any;
|
||||
},
|
||||
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
||||
) => {
|
||||
const data = {
|
||||
currentUserLocation: {
|
||||
lat,
|
||||
lon,
|
||||
accuracy,
|
||||
isIPLocation,
|
||||
name,
|
||||
picture,
|
||||
__typename: "CurrentUserLocation",
|
||||
},
|
||||
};
|
||||
|
||||
localCache.writeQuery({ data, query: CURRENT_USER_LOCATION_CLIENT });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
212
src/apollo/utils.ts
Normal file
212
src/apollo/utils.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
|
||||
import { REFRESH_TOKEN } from "@/graphql/auth";
|
||||
import { IFollower } from "@/types/actor/follower.model";
|
||||
import { IParticipant } from "@/types/participant.model";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { saveTokenData } from "@/utils/auth";
|
||||
import { FieldPolicy, Reference, TypePolicies } from "@apollo/client/core";
|
||||
import introspectionQueryResultData from "../../fragmentTypes.json";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { IComment } from "@/types/comment.model";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { IActivity } from "@/types/activity.model";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import { provideApolloClient, useMutation } from "@vue/apollo-composable";
|
||||
import { apolloClient } from "@/vue-apollo";
|
||||
import { IToken } from "@/types/login.model";
|
||||
|
||||
type possibleTypes = { name: string };
|
||||
type schemaType = {
|
||||
kind: string;
|
||||
name: string;
|
||||
possibleTypes: possibleTypes[];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const types = introspectionQueryResultData.__schema.types as schemaType[];
|
||||
export const possibleTypes = types.reduce(
|
||||
(acc, type) => {
|
||||
if (type.kind === "INTERFACE") {
|
||||
acc[type.name] = type.possibleTypes.map(({ name }) => name);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string[]>
|
||||
);
|
||||
|
||||
const replaceMergePolicy = <TExisting = any, TIncoming = any>(
|
||||
_existing: TExisting,
|
||||
incoming: TIncoming
|
||||
): TIncoming => incoming;
|
||||
|
||||
export const typePolicies: TypePolicies = {
|
||||
Discussion: {
|
||||
fields: {
|
||||
comments: paginatedLimitPagination<IComment>(),
|
||||
},
|
||||
},
|
||||
Conversation: {
|
||||
fields: {
|
||||
comments: paginatedLimitPagination<IComment>(),
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
fields: {
|
||||
organizedEvents: paginatedLimitPagination([
|
||||
"afterDatetime",
|
||||
"beforeDatetime",
|
||||
]),
|
||||
activity: paginatedLimitPagination<IActivity>(["type", "author"]),
|
||||
},
|
||||
},
|
||||
Person: {
|
||||
fields: {
|
||||
organizedEvents: paginatedLimitPagination<IEvent>(),
|
||||
participations: paginatedLimitPagination<IParticipant>(["eventId"]),
|
||||
memberships: paginatedLimitPagination<IMember>(["group"]),
|
||||
},
|
||||
},
|
||||
Event: {
|
||||
fields: {
|
||||
participants: paginatedLimitPagination<IParticipant>(["roles"]),
|
||||
comments: pageLimitPagination<IComment>(),
|
||||
relatedEvents: pageLimitPagination<IEvent>(),
|
||||
options: { merge: replaceMergePolicy },
|
||||
participantStats: { merge: replaceMergePolicy },
|
||||
},
|
||||
},
|
||||
Instance: {
|
||||
keyFields: ["domain"],
|
||||
},
|
||||
Config: {
|
||||
merge: true,
|
||||
},
|
||||
Address: {
|
||||
keyFields: ["id", "originId"],
|
||||
},
|
||||
RootQueryType: {
|
||||
fields: {
|
||||
relayFollowers: paginatedLimitPagination<IFollower>(),
|
||||
relayFollowings: paginatedLimitPagination<IFollower>([
|
||||
"orderBy",
|
||||
"direction",
|
||||
]),
|
||||
events: paginatedLimitPagination(),
|
||||
groups: paginatedLimitPagination([
|
||||
"preferredUsername",
|
||||
"name",
|
||||
"domain",
|
||||
"local",
|
||||
"suspended",
|
||||
]),
|
||||
persons: paginatedLimitPagination([
|
||||
"preferredUsername",
|
||||
"name",
|
||||
"domain",
|
||||
"local",
|
||||
"suspended",
|
||||
]),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function refreshAccessToken(): Promise<boolean> {
|
||||
// Remove invalid access token, so the next request is not authenticated
|
||||
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
||||
|
||||
const refreshToken = localStorage.getItem(AUTH_REFRESH_TOKEN);
|
||||
|
||||
if (!refreshToken) {
|
||||
console.debug("Refresh token not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.debug("Refreshing access token.");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { mutate, onDone, onError } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation<{ refreshToken: IToken }>(REFRESH_TOKEN)
|
||||
);
|
||||
|
||||
mutate({
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
onDone(({ data }) => {
|
||||
if (data?.refreshToken) {
|
||||
saveTokenData(data?.refreshToken);
|
||||
resolve(true);
|
||||
}
|
||||
reject(false);
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
console.debug("Failed to refresh token", err);
|
||||
reject(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type KeyArgs = FieldPolicy<any>["keyArgs"];
|
||||
|
||||
export function pageLimitPagination<T = Reference>(
|
||||
keyArgs: KeyArgs = false
|
||||
): FieldPolicy<T[]> {
|
||||
return {
|
||||
keyArgs,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
merge(existing, incoming, { args }) {
|
||||
if (!incoming) return existing;
|
||||
if (!existing) return incoming; // existing will be empty the first time
|
||||
|
||||
return doMerge(existing as Array<T>, incoming as Array<T>, args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function paginatedLimitPagination<T = Paginate<any>>(
|
||||
keyArgs: KeyArgs = false
|
||||
): FieldPolicy<Paginate<T>> {
|
||||
return {
|
||||
keyArgs,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
merge(existing, incoming, { args }) {
|
||||
if (!incoming) return existing;
|
||||
if (!existing) return incoming; // existing will be empty the first time
|
||||
|
||||
return {
|
||||
total: incoming.total,
|
||||
elements: doMerge(existing.elements, incoming.elements, args),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function doMerge<T = any>(
|
||||
existing: Array<T>,
|
||||
incoming: Array<T>,
|
||||
args: Record<string, any> | null
|
||||
): Array<T> {
|
||||
const merged = existing && Array.isArray(existing) ? existing.slice(0) : [];
|
||||
const previous = incoming && Array.isArray(incoming) ? incoming.slice(0) : [];
|
||||
let res;
|
||||
if (args) {
|
||||
// Assume an page of 1 if args.page omitted.
|
||||
const { page = 1, limit = 10 } = args;
|
||||
for (let i = 0; i < previous.length; ++i) {
|
||||
merged[(page - 1) * limit + i] = previous[i];
|
||||
}
|
||||
res = merged;
|
||||
} else {
|
||||
// It's unusual (probably a mistake) for a paginated field not
|
||||
// to receive any arguments, so you might prefer to throw an
|
||||
// exception here, instead of recovering by appending incoming
|
||||
// onto the existing array.
|
||||
res = [...merged, ...previous];
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
res = uniqBy(res, (elem: any) => elem.__ref);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user