1
0
Fork 0
forked from OpenNeo/impress
impress/app/javascript/wardrobe-2020/apolloClient.js

237 lines
7.4 KiB
JavaScript

import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { setContext } from "@apollo/client/link/context";
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
import { getAuthModeFeatureFlag } from "./components/useCurrentUser";
// Use Apollo's error messages in development.
if (process.env["NODE_ENV"] === "development") {
loadErrorMessages();
loadDevMessages();
}
// Teach Apollo to load certain fields from the cache, to avoid extra network
// requests. This happens a lot - e.g. reusing data from item search on the
// outfit immediately!
const typePolicies = {
Query: {
fields: {
closetList: (_, { args, toReference }) => {
return toReference({ __typename: "ClosetList", id: args.id }, true);
},
items: (_, { args, toReference }) => {
return args.ids.map((id) =>
toReference({ __typename: "Item", id }, true)
);
},
item: (_, { args, toReference }) => {
return toReference({ __typename: "Item", id: args.id }, true);
},
petAppearanceById: (_, { args, toReference }) => {
return toReference({ __typename: "PetAppearance", id: args.id }, true);
},
species: (_, { args, toReference }) => {
return toReference({ __typename: "Species", id: args.id }, true);
},
color: (_, { args, toReference }) => {
return toReference({ __typename: "Color", id: args.id }, true);
},
outfit: (_, { args, toReference }) => {
return toReference({ __typename: "Outfit", id: args.id }, true);
},
user: (_, { args, toReference }) => {
return toReference({ __typename: "User", id: args.id }, true);
},
},
},
Item: {
fields: {
appearanceOn: (appearance, { args, readField, toReference }) => {
// If we already have this exact appearance in the cache, serve it!
if (appearance) {
return appearance;
}
// Otherwise, we're going to see if this is a standard color, in which
// case we can reuse the standard color appearance if we already have
// it! This helps for fast loading when switching between standard
// colors.
const { speciesId, colorId } = args;
console.debug(
"[appearanceOn] seeking cached appearance",
speciesId,
colorId,
readField("id")
);
const speciesStandardBodyId = readField(
"standardBodyId",
toReference({ __typename: "Species", id: speciesId })
);
const colorIsStandard = readField(
"isStandard",
toReference({ __typename: "Color", id: colorId })
);
if (speciesStandardBodyId == null || colorIsStandard == null) {
// We haven't loaded all the species/colors into cache yet. We might
// be loading them, depending on the page? Either way, return
// `undefined`, meaning we don't know how to serve this from cache.
// This will cause us to start loading it from the server.
console.debug("[appearanceOn] species/colors not loaded yet");
return undefined;
}
if (colorIsStandard) {
const itemId = readField("id");
console.debug(
"[appearanceOn] standard color, will read:",
`item-${itemId}-body-${speciesStandardBodyId}`
);
return toReference({
__typename: "ItemAppearance",
id: `item-${itemId}-body-${speciesStandardBodyId}`,
});
} else {
console.debug("[appearanceOn] non-standard color, failure");
// This isn't a standard color, so we don't support special
// cross-color caching for it. Return `undefined`, meaning we don't
// know how to serve this from cache. This will cause us to start
// loading it from the server.
return undefined;
}
},
currentUserOwnsThis: (cachedValue, { readField }) => {
if (cachedValue != null) {
return cachedValue;
}
// Do we know what items this user owns? If so, scan for this item.
const currentUserRef = readField("currentUser", {
__ref: "ROOT_QUERY",
});
if (!currentUserRef) {
return undefined;
}
const thisItemId = readField("id");
const itemsTheyOwn = readField("itemsTheyOwn", currentUserRef);
if (!itemsTheyOwn) {
return undefined;
}
const theyOwnThisItem = itemsTheyOwn.some(
(itemRef) => readField("id", itemRef) === thisItemId
);
return theyOwnThisItem;
},
currentUserWantsThis: (cachedValue, { readField }) => {
if (cachedValue != null) {
return cachedValue;
}
// Do we know what items this user owns? If so, scan for this item.
const currentUserRef = readField("currentUser", {
__ref: "ROOT_QUERY",
});
if (!currentUserRef) {
return undefined;
}
const thisItemId = readField("id");
const itemsTheyWant = readField("itemsTheyWant", currentUserRef);
if (!itemsTheyWant) {
return undefined;
}
const theyWantThisItem = itemsTheyWant.some(
(itemRef) => readField("id", itemRef) === thisItemId
);
return theyWantThisItem;
},
},
},
ClosetList: {
fields: {
// When loading the updated contents of a list, replace it entirely.
items: { merge: false },
},
},
};
const httpLink = createHttpLink({
uri: "https://impress-2020.openneo.net/api/graphql",
});
const buildAuthLink = (getAuth0) =>
setContext(async (_, { headers = {}, sendAuth = false }) => {
if (!sendAuth) {
return;
}
const token = await getAccessToken(getAuth0);
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
}
});
// This is a temporary way to pass the DTIAuthMode feature flag back to the
// server!
const authModeLink = setContext((_, { headers = {} }) => {
const authMode = getAuthModeFeatureFlag();
return {
headers: {
...headers,
"DTI-Auth-Mode": authMode,
},
};
});
async function getAccessToken(getAuth0) {
// Wait for auth0 to stop loading, so we can maybe get a token!
// We'll do this hackily by checking every 100ms until it's true.
await new Promise((resolve) => {
function check() {
if (getAuth0().isLoading) {
setTimeout(check, 100);
} else {
resolve();
}
}
check();
});
const { isAuthenticated, getAccessTokenSilently } = getAuth0();
if (isAuthenticated) {
const token = await getAccessTokenSilently();
return token;
}
}
const buildLink = (getAuth0) =>
buildAuthLink(getAuth0)
.concat(authModeLink)
.concat(
createPersistedQueryLink({
useGETForHashedQueries: true,
})
)
.concat(httpLink);
/**
* apolloClient is the global Apollo Client instance we use for GraphQL
* queries. This is how we communicate with the server!
*/
const buildClient = ({ getAuth0, initialCacheState }) => {
return new ApolloClient({
link: buildLink(getAuth0),
cache: new InMemoryCache({ typePolicies }).restore(initialCacheState),
connectToDevTools: true,
});
};
export default buildClient;