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:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

View 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);

View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}