2020-07-31 23:10:34 -07:00
|
|
|
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
2020-09-04 05:59:35 -07:00
|
|
|
import { setContext } from "@apollo/client/link/context";
|
2020-05-14 15:51:08 -07:00
|
|
|
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
|
|
|
|
2020-09-01 18:02:59 -07:00
|
|
|
import cachedZones from "./cached-data/zones.json";
|
2021-04-19 02:16:31 -07:00
|
|
|
import { readCypressLoginData } from "./components/useCurrentUser";
|
2020-08-19 17:50:05 -07:00
|
|
|
|
2020-08-31 18:25:42 -07:00
|
|
|
// 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!
|
2020-07-31 23:10:34 -07:00
|
|
|
const typePolicies = {
|
2020-05-14 15:51:08 -07:00
|
|
|
Query: {
|
2020-07-31 23:10:34 -07:00
|
|
|
fields: {
|
2020-09-01 18:59:05 -07:00
|
|
|
allZones: (_, { toReference }) => {
|
|
|
|
return cachedZones.map((z) =>
|
|
|
|
toReference({ __typename: "Zone", id: z.id }, true)
|
|
|
|
);
|
|
|
|
},
|
2020-07-31 23:21:09 -07:00
|
|
|
items: (_, { args, toReference }) => {
|
|
|
|
return args.ids.map((id) =>
|
|
|
|
toReference({ __typename: "Item", id }, true)
|
|
|
|
);
|
|
|
|
},
|
2020-08-27 23:09:07 -07:00
|
|
|
item: (_, { args, toReference }) => {
|
|
|
|
return toReference({ __typename: "Item", id: args.id }, true);
|
|
|
|
},
|
2020-08-29 13:23:41 -07:00
|
|
|
petAppearanceById: (_, { args, toReference }) => {
|
|
|
|
return toReference({ __typename: "PetAppearance", id: args.id }, true);
|
|
|
|
},
|
2020-08-31 18:25:42 -07:00
|
|
|
species: (_, { args, toReference }) => {
|
|
|
|
return toReference({ __typename: "Species", id: args.id }, true);
|
|
|
|
},
|
|
|
|
color: (_, { args, toReference }) => {
|
|
|
|
return toReference({ __typename: "Color", id: args.id }, true);
|
|
|
|
},
|
2021-01-04 22:39:12 -08:00
|
|
|
outfit: (_, { args, toReference }) => {
|
|
|
|
return toReference({ __typename: "Outfit", id: args.id }, true);
|
|
|
|
},
|
2020-11-18 07:42:40 -08:00
|
|
|
user: (_, { args, toReference }) => {
|
|
|
|
return toReference({ __typename: "User", id: args.id }, true);
|
|
|
|
},
|
2020-08-31 18:25:42 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
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;
|
2020-09-05 16:37:17 -07:00
|
|
|
const speciesStandardBodyId = readField(
|
|
|
|
"standardBodyId",
|
|
|
|
toReference({ __typename: "Species", id: speciesId })
|
|
|
|
);
|
|
|
|
const colorIsStandard = readField(
|
|
|
|
"isStandard",
|
|
|
|
toReference({ __typename: "Color", id: colorId })
|
|
|
|
);
|
|
|
|
if (speciesStandardBodyId == null || colorIsStandard == null) {
|
2020-09-12 17:56:31 -07:00
|
|
|
// 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.
|
|
|
|
return undefined;
|
2020-08-31 18:25:42 -07:00
|
|
|
}
|
|
|
|
|
2020-09-05 16:37:17 -07:00
|
|
|
if (colorIsStandard) {
|
2020-08-31 18:25:42 -07:00
|
|
|
const itemId = readField("id");
|
|
|
|
return toReference({
|
|
|
|
__typename: "ItemAppearance",
|
2020-09-05 16:37:17 -07:00
|
|
|
id: `item-${itemId}-body-${speciesStandardBodyId}`,
|
2020-08-31 18:25:42 -07:00
|
|
|
});
|
|
|
|
} else {
|
2020-09-12 17:56:31 -07:00
|
|
|
// 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.
|
2020-08-31 18:25:42 -07:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
},
|
2020-09-12 22:39:38 -07:00
|
|
|
|
|
|
|
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;
|
|
|
|
},
|
2020-05-14 15:51:08 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const httpLink = createHttpLink({ uri: "/api/graphql" });
|
2020-09-04 05:59:35 -07:00
|
|
|
const buildAuthLink = (getAuth0) =>
|
2021-01-21 14:57:21 -08:00
|
|
|
setContext(async (_, { headers = {}, sendAuth = false }) => {
|
|
|
|
if (!sendAuth) {
|
2021-01-18 07:15:00 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-19 02:16:31 -07:00
|
|
|
const token = await getAccessToken(getAuth0);
|
|
|
|
if (token) {
|
2020-09-04 05:59:35 -07:00
|
|
|
return {
|
|
|
|
headers: {
|
|
|
|
...headers,
|
|
|
|
authorization: token ? `Bearer ${token}` : "",
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
2020-05-14 15:51:08 -07:00
|
|
|
|
2021-04-19 02:16:31 -07:00
|
|
|
async function getAccessToken(getAuth0) {
|
|
|
|
// Our Cypress tests store login data separately. Use it if available!
|
|
|
|
const cypressToken = readCypressLoginData()?.encodedToken;
|
|
|
|
if (cypressToken) {
|
|
|
|
return cypressToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-01 18:02:59 -07:00
|
|
|
const initialCache = {};
|
|
|
|
for (const zone of cachedZones) {
|
|
|
|
initialCache[`Zone:${zone.id}`] = { __typename: "Zone", ...zone };
|
|
|
|
}
|
|
|
|
|
2021-02-07 00:18:01 -08:00
|
|
|
const buildLink = (getAuth0) => {
|
|
|
|
let link = buildAuthLink(getAuth0);
|
|
|
|
if (process.env.NODE_ENV === "production") {
|
|
|
|
// In production, we send GET requests for queries, to enable CDN and
|
|
|
|
// browser caching. But in development, we skip it, because our dev server
|
|
|
|
// reloads the route from scratch on each request, so queries never get
|
|
|
|
// persisted, and the GET always fails and falls back to POST anyway.
|
|
|
|
link = link.concat(
|
|
|
|
createPersistedQueryLink({
|
|
|
|
useGETForHashedQueries: true,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
link = link.concat(httpLink);
|
|
|
|
return link;
|
|
|
|
};
|
|
|
|
|
2020-05-14 15:51:08 -07:00
|
|
|
/**
|
|
|
|
* apolloClient is the global Apollo Client instance we use for GraphQL
|
|
|
|
* queries. This is how we communicate with the server!
|
|
|
|
*/
|
2020-09-05 16:37:17 -07:00
|
|
|
const buildClient = (getAuth0) =>
|
|
|
|
new ApolloClient({
|
2021-02-07 00:18:01 -08:00
|
|
|
link: buildLink(getAuth0),
|
2020-09-04 05:59:35 -07:00
|
|
|
cache: new InMemoryCache({ typePolicies }).restore(initialCache),
|
|
|
|
connectToDevTools: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
export default buildClient;
|