fix: prevent metadata queries from short-circuiting (#10312)

* fix: prevent metadata queries from short-circuiting

* fix: use correct type definitions
This commit is contained in:
Michael Smith
2023-10-17 10:20:56 -06:00
committed by GitHub
parent 8f39ec5cc3
commit 0f2d4fdb6d
8 changed files with 92 additions and 44 deletions
+17 -8
View File
@@ -1,21 +1,30 @@
import { QueryClient } from "react-query";
import { QueryClient, type UseQueryOptions } from "react-query";
import * as API from "api/api";
import { AppearanceConfig } from "api/typesGenerated";
import { type AppearanceConfig } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
export const appearance = () => {
const initialAppearanceData = getMetadataAsJSON<AppearanceConfig>("appearance");
const appearanceConfigKey = ["appearance"] as const;
export const appearance = (queryClient: QueryClient) => {
return {
queryKey: ["appearance"],
queryFn: async () =>
getMetadataAsJSON<AppearanceConfig>("appearance") ?? API.getAppearance(),
};
queryKey: appearanceConfigKey,
queryFn: async () => {
const cachedData = queryClient.getQueryData(appearanceConfigKey);
if (cachedData === undefined && initialAppearanceData !== undefined) {
return initialAppearanceData;
}
return API.getAppearance();
},
} satisfies UseQueryOptions<AppearanceConfig>;
};
export const updateAppearance = (queryClient: QueryClient) => {
return {
mutationFn: API.updateAppearance,
onSuccess: (newConfig: AppearanceConfig) => {
queryClient.setQueryData(["appearance"], newConfig);
queryClient.setQueryData(appearanceConfigKey, newConfig);
},
};
};
+16 -6
View File
@@ -1,11 +1,21 @@
import { QueryClient, type UseQueryOptions } from "react-query";
import { type BuildInfoResponse } from "api/typesGenerated";
import * as API from "api/api";
import { BuildInfoResponse } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
export const buildInfo = () => {
const initialBuildInfoData = getMetadataAsJSON<BuildInfoResponse>("build-info");
const buildInfoKey = ["buildInfo"] as const;
export const buildInfo = (queryClient: QueryClient) => {
return {
queryKey: ["buildInfo"],
queryFn: async () =>
getMetadataAsJSON<BuildInfoResponse>("build-info") ?? API.getBuildInfo(),
};
queryKey: buildInfoKey,
queryFn: async () => {
const cachedData = queryClient.getQueryData(buildInfoKey);
if (cachedData === undefined && initialBuildInfoData !== undefined) {
return initialBuildInfoData;
}
return API.getBuildInfo();
},
} satisfies UseQueryOptions<BuildInfoResponse>;
};
+16 -6
View File
@@ -1,11 +1,21 @@
import * as API from "api/api";
import { Experiments } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
import { type Experiments } from "api/typesGenerated";
import { QueryClient, type UseQueryOptions } from "react-query";
export const experiments = () => {
const initialExperimentsData = getMetadataAsJSON<Experiments>("experiments");
const experimentsKey = ["experiments"] as const;
export const experiments = (queryClient: QueryClient) => {
return {
queryKey: ["experiments"],
queryFn: async () =>
getMetadataAsJSON<Experiments>("experiments") ?? API.getExperiments(),
};
queryKey: experimentsKey,
queryFn: async () => {
const cachedData = queryClient.getQueryData(experimentsKey);
if (cachedData === undefined && initialExperimentsData !== undefined) {
return initialExperimentsData;
}
return API.getExperiments();
},
} satisfies UseQueryOptions<Experiments>;
};
+16 -7
View File
@@ -1,4 +1,4 @@
import { QueryClient, QueryOptions } from "react-query";
import { QueryClient, type UseQueryOptions } from "react-query";
import * as API from "api/api";
import {
AuthorizationRequest,
@@ -11,7 +11,7 @@ import {
import { getMetadataAsJSON } from "utils/metadata";
import { getAuthorizationKey } from "./authCheck";
export const users = (req: UsersRequest): QueryOptions<GetUsersResponse> => {
export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => {
return {
queryKey: ["users", req],
queryFn: ({ signal }) => API.getUsers(req, signal),
@@ -89,12 +89,21 @@ export const authMethods = () => {
};
};
export const me = () => {
const initialMeData = getMetadataAsJSON<User>("user");
const meKey = ["me"] as const;
export const me = (queryClient: QueryClient) => {
return {
queryKey: ["me"],
queryFn: async () =>
getMetadataAsJSON<User>("user") ?? API.getAuthenticatedUser(),
};
queryKey: meKey,
queryFn: async () => {
const cachedData = queryClient.getQueryData(meKey);
if (cachedData === undefined && initialMeData !== undefined) {
return initialMeData;
}
return API.getAuthenticatedUser();
},
} satisfies UseQueryOptions<User>;
};
export const hasFirstUser = () => {
@@ -45,7 +45,9 @@ type AuthContextValue = {
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
const meOptions = me();
const queryClient = useQueryClient();
const meOptions = me(queryClient);
const userQuery = useQuery(meOptions);
const authMethodsQuery = useQuery(authMethods());
const hasFirstUserQuery = useQuery(hasFirstUser());
@@ -54,7 +56,6 @@ export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
enabled: userQuery.data !== undefined,
});
const queryClient = useQueryClient();
const loginMutation = useMutation(
login({ checks: permissionsToCheck }, queryClient),
);
@@ -1,4 +1,4 @@
import { useQuery } from "react-query";
import { useQuery, useQueryClient } from "react-query";
import { buildInfo } from "api/queries/buildInfo";
import { experiments } from "api/queries/experiments";
import { entitlements } from "api/queries/entitlements";
@@ -30,8 +30,8 @@ interface Appearance {
interface DashboardProviderValue {
buildInfo: BuildInfoResponse;
entitlements: Entitlements;
appearance: Appearance;
experiments: Experiments;
appearance: Appearance;
}
export const DashboardProviderContext = createContext<
@@ -39,10 +39,12 @@ export const DashboardProviderContext = createContext<
>(undefined);
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
const buildInfoQuery = useQuery(buildInfo());
const queryClient = useQueryClient();
const buildInfoQuery = useQuery(buildInfo(queryClient));
const entitlementsQuery = useQuery(entitlements());
const experimentsQuery = useQuery(experiments());
const appearanceQuery = useQuery(appearance());
const experimentsQuery = useQuery(experiments(queryClient));
const appearanceQuery = useQuery(appearance(queryClient));
const isLoading =
!buildInfoQuery.data ||
!entitlementsQuery.data ||
+2 -1
View File
@@ -6,7 +6,8 @@ import { getMetadataAsJSON } from "utils/metadata";
// so you can just set this to true.
export const experimentalTheme =
typeof document !== "undefined" &&
getMetadataAsJSON("experiments")?.includes("dashboard_theme");
(getMetadataAsJSON<string[]>("experiments")?.includes("dashboard_theme") ??
false);
export const colors = {
white: "hsl(0, 0%, 100%)",
+15 -9
View File
@@ -1,18 +1,24 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- It can be any
export const getMetadataAsJSON = <T extends Record<string, any>>(
export const getMetadataAsJSON = <T extends NonNullable<unknown>>(
property: string,
): T | undefined => {
const appearance = document.querySelector(`meta[property=${property}]`);
if (appearance) {
const rawContent = appearance.getAttribute("content");
try {
return JSON.parse(rawContent as string);
} catch (ex) {
// In development the metadata is always going to be empty throwing this
// error
if (process.env.NODE_ENV === "production") {
console.warn(`Failed to parse ${property} metadata`);
if (rawContent) {
try {
return JSON.parse(rawContent);
} catch (err) {
// In development, the metadata is always going to be empty; error is
// only a concern for production
if (process.env.NODE_ENV === "production") {
console.warn(`Failed to parse ${property} metadata. Error message:`);
console.warn(err);
}
}
}
}
return undefined;
};