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)
|
|
|
|
|
);
|
|
|
|
|
},
|
2021-06-18 17:29:44 -07:00
|
|
|
closetList: (_, { args, toReference }) => {
|
|
|
|
|
return toReference({ __typename: "ClosetList", id: args.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;
|
2021-07-21 16:03:24 -07:00
|
|
|
console.debug(
|
|
|
|
|
"[appearanceOn] seeking cached appearance",
|
|
|
|
|
speciesId,
|
|
|
|
|
colorId,
|
|
|
|
|
readField("id")
|
|
|
|
|
);
|
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.
|
2021-07-21 16:03:24 -07:00
|
|
|
console.debug("[appearanceOn] species/colors not loaded yet");
|
2020-09-12 17:56:31 -07:00
|
|
|
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");
|
2021-07-21 16:03:24 -07:00
|
|
|
console.debug(
|
|
|
|
|
"[appearanceOn] standard color, will read:",
|
|
|
|
|
`item-${itemId}-body-${speciesStandardBodyId}`
|
|
|
|
|
);
|
2020-08-31 18:25:42 -07:00
|
|
|
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 {
|
2021-07-21 16:03:24 -07:00
|
|
|
console.debug("[appearanceOn] non-standard color, failure");
|
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
|
|
|
},
|
|
|
|
|
},
|
2021-09-30 19:26:09 -07:00
|
|
|
|
|
|
|
|
ClosetList: {
|
|
|
|
|
fields: {
|
|
|
|
|
// When loading the updated contents of a list, replace it entirely.
|
|
|
|
|
items: { merge: false },
|
|
|
|
|
},
|
|
|
|
|
},
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
Use persisted queries in dev, bc Next.js support
We had previously configured the client to not bother to try a GET request for GraphQL queries, and just jump straight to POST instead, because the `vercel dev` server for create-react-app reloaded the backend code for every request anyway, which doubled the dev response time.
The Next.js server is more efficient than this, and keeps some memory, so GET requests work similarly in dev as on prod now! (i.e. it fails the first time, but then succeeds on the second)
In this change, we remove the code to skip `createPersistedQueryLink` in development, and instead always call it. We simplify the code accordingly, too.
2021-11-23 12:35:20 -08:00
|
|
|
const buildLink = (getAuth0) =>
|
|
|
|
|
buildAuthLink(getAuth0)
|
|
|
|
|
.concat(
|
2021-02-07 00:18:01 -08:00
|
|
|
createPersistedQueryLink({
|
|
|
|
|
useGETForHashedQueries: true,
|
|
|
|
|
})
|
Use persisted queries in dev, bc Next.js support
We had previously configured the client to not bother to try a GET request for GraphQL queries, and just jump straight to POST instead, because the `vercel dev` server for create-react-app reloaded the backend code for every request anyway, which doubled the dev response time.
The Next.js server is more efficient than this, and keeps some memory, so GET requests work similarly in dev as on prod now! (i.e. it fails the first time, but then succeeds on the second)
In this change, we remove the code to skip `createPersistedQueryLink` in development, and instead always call it. We simplify the code accordingly, too.
2021-11-23 12:35:20 -08:00
|
|
|
)
|
|
|
|
|
.concat(httpLink);
|
2021-02-07 00:18:01 -08:00
|
|
|
|
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;
|