diff --git a/.changeset/eleven-elephants-raise.md b/.changeset/eleven-elephants-raise.md
new file mode 100644
index 000000000..df7427a08
--- /dev/null
+++ b/.changeset/eleven-elephants-raise.md
@@ -0,0 +1,29 @@
+---
+'@faustwp/core': minor
+---
+
+Added the ability to provide multiple queries to a given Faust Template:
+
+```js
+import {GET_POST, GET_LAYOUT} from './queries.js'
+
+export default function Component(props) {
+}
+
+Component.queries = [
+ {
+ query: GET_LAYOUT
+ },
+ {
+ query: GET_POST,
+ variables: (seedNode, ctx) {
+ return {
+ id: seedNode.databaseId,
+ asPreview: ctx?.asPreview
+ }
+ }
+ }
+]
+```
+
+**Note:** Your Faust template can use either `Component.queries` or `Component.query`, but not both.
diff --git a/examples/next/faustwp-getting-started/wp-templates/single.js b/examples/next/faustwp-getting-started/wp-templates/single.js
index a5edac820..50a66ddf8 100644
--- a/examples/next/faustwp-getting-started/wp-templates/single.js
+++ b/examples/next/faustwp-getting-started/wp-templates/single.js
@@ -1,17 +1,58 @@
import { gql } from '@apollo/client';
-import * as MENUS from '../constants/menus';
-import { BlogInfoFragment } from '../fragments/GeneralSettings';
+import { useFaustQuery } from '@faustwp/core';
import {
- Header,
- Footer,
- Main,
Container,
- EntryHeader,
- NavigationMenu,
ContentWrapper,
+ EntryHeader,
FeaturedImage,
+ Footer,
+ Header,
+ Main,
+ NavigationMenu,
SEO,
} from '../components';
+import * as MENUS from '../constants/menus';
+import { BlogInfoFragment } from '../fragments/GeneralSettings';
+
+const GET_LAYOUT_QUERY = gql`
+ ${BlogInfoFragment}
+ ${NavigationMenu.fragments.entry}
+ query GetLayout(
+ $headerLocation: MenuLocationEnum
+ $footerLocation: MenuLocationEnum
+ ) {
+ generalSettings {
+ ...BlogInfoFragment
+ }
+ headerMenuItems: menuItems(where: { location: $headerLocation }) {
+ nodes {
+ ...NavigationMenuItemFragment
+ }
+ }
+ footerMenuItems: menuItems(where: { location: $footerLocation }) {
+ nodes {
+ ...NavigationMenuItemFragment
+ }
+ }
+ }
+`;
+
+const GET_POST_QUERY = gql`
+ ${FeaturedImage.fragments.entry}
+ query GetPost($databaseId: ID!, $asPreview: Boolean = false) {
+ post(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) {
+ title
+ content
+ date
+ author {
+ node {
+ name
+ }
+ }
+ ...FeaturedImageFragment
+ }
+ }
+`;
export default function Component(props) {
// Loading state for previews
@@ -19,11 +60,14 @@ export default function Component(props) {
return <>Loading...>;
}
- const { title: siteTitle, description: siteDescription } =
- props?.data?.generalSettings;
- const primaryMenu = props?.data?.headerMenuItems?.nodes ?? [];
- const footerMenu = props?.data?.footerMenuItems?.nodes ?? [];
- const { title, content, featuredImage, date, author } = props.data.post;
+ const { post } = useFaustQuery(GET_POST_QUERY);
+ const { generalSettings, headerMenuItems, footerMenuItems } =
+ useFaustQuery(GET_LAYOUT_QUERY);
+
+ const { title: siteTitle, description: siteDescription } = generalSettings;
+ const primaryMenu = headerMenuItems?.nodes ?? [];
+ const footerMenu = footerMenuItems?.nodes ?? [];
+ const { title, content, featuredImage, date, author } = post ?? {};
return (
<>
@@ -55,48 +99,19 @@ export default function Component(props) {
);
}
-Component.query = gql`
- ${BlogInfoFragment}
- ${NavigationMenu.fragments.entry}
- ${FeaturedImage.fragments.entry}
- query GetPost(
- $databaseId: ID!
- $headerLocation: MenuLocationEnum
- $footerLocation: MenuLocationEnum
- $asPreview: Boolean = false
- ) {
- post(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) {
- title
- content
- date
- author {
- node {
- name
- }
- }
- ...FeaturedImageFragment
- }
- generalSettings {
- ...BlogInfoFragment
- }
- headerMenuItems: menuItems(where: { location: $headerLocation }) {
- nodes {
- ...NavigationMenuItemFragment
- }
- }
- footerMenuItems: menuItems(where: { location: $footerLocation }) {
- nodes {
- ...NavigationMenuItemFragment
- }
- }
- }
-`;
-
-Component.variables = ({ databaseId }, ctx) => {
- return {
- databaseId,
- headerLocation: MENUS.PRIMARY_LOCATION,
- footerLocation: MENUS.FOOTER_LOCATION,
- asPreview: ctx?.asPreview,
- };
-};
+Component.queries = [
+ {
+ query: GET_LAYOUT_QUERY,
+ variables: (seedNode, ctx) => ({
+ headerLocation: MENUS.PRIMARY_LOCATION,
+ footerLocation: MENUS.FOOTER_LOCATION,
+ }),
+ },
+ {
+ query: GET_POST_QUERY,
+ variables: ({ databaseId }, ctx) => ({
+ databaseId,
+ asPreview: ctx?.asPreview,
+ }),
+ },
+];
diff --git a/package-lock.json b/package-lock.json
index 00a79d079..b7f694368 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,9 +51,9 @@
"dependencies": {
"@apollo/client": "^3.8.0",
"@apollo/experimental-nextjs-app-support": "^0.5.1",
- "@faustwp/cli": "^1.2.0",
+ "@faustwp/cli": "^1.2.1",
"@faustwp/core": "^1.2.0",
- "@faustwp/experimental-app-router": "^0.2.1",
+ "@faustwp/experimental-app-router": "^0.2.2",
"graphql": "^16.7.1",
"next": "^14.0.1",
"react": "^18.3.0-canary-ce2bc58a9-20231102",
@@ -253,7 +253,7 @@
"dependencies": {
"@apollo/client": "^3.8.8",
"@faustwp/blocks": "2.0.0",
- "@faustwp/cli": "^1.2.0",
+ "@faustwp/cli": "^1.2.1",
"@faustwp/core": "^1.2.0",
"@wordpress/base-styles": "^4.38.0",
"@wordpress/block-library": "^8.24.0",
@@ -2108,7 +2108,7 @@
"version": "0.1.0",
"dependencies": {
"@apollo/client": "^3.6.6",
- "@faustwp/cli": "^1.2.0",
+ "@faustwp/cli": "^1.2.1",
"@faustwp/core": "^1.2.0",
"@wordpress/base-styles": "^4.36.0",
"@wordpress/block-library": "^7.19.0",
@@ -30799,12 +30799,12 @@
},
"packages/experimental-app-router": {
"name": "@faustwp/experimental-app-router",
- "version": "0.2.1",
+ "version": "0.2.2",
"license": "MIT",
"devDependencies": {
"@apollo/client": "^3.8.0",
"@apollo/experimental-nextjs-app-support": "^0.5.1",
- "@faustwp/cli": "^1.1.3",
+ "@faustwp/cli": "^1.2.1",
"@faustwp/core": "^1.1.2",
"@testing-library/jest-dom": "^5.17.0",
"@types/node": "^20.4.6",
@@ -30856,9 +30856,9 @@
"dev": true
},
"packages/experimental-app-router/node_modules/@next/swc-darwin-arm64": {
- "version": "14.0.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz",
- "integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==",
+ "version": "12.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz",
+ "integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==",
"cpu": [
"arm64"
],
@@ -31293,7 +31293,7 @@
},
"packages/faustwp-cli": {
"name": "@faustwp/cli",
- "version": "1.2.0",
+ "version": "1.2.1",
"license": "MIT",
"dependencies": {
"archiver": "^6.0.1",
@@ -35720,7 +35720,7 @@
},
"plugins/faustwp": {
"name": "@faustwp/wordpress-plugin",
- "version": "1.1.1"
+ "version": "1.1.2"
}
}
}
diff --git a/packages/faustwp-core/src/components/FaustProvider.tsx b/packages/faustwp-core/src/components/FaustProvider.tsx
index 5a2310653..54680acde 100644
--- a/packages/faustwp-core/src/components/FaustProvider.tsx
+++ b/packages/faustwp-core/src/components/FaustProvider.tsx
@@ -1,17 +1,16 @@
+import React, { useState } from 'react';
import { ApolloProvider } from '@apollo/client';
-import React from 'react';
// eslint-disable-next-line import/extensions
import { useRouter } from 'next/router';
// eslint-disable-next-line import/extensions
import { AppProps } from 'next/app';
import { useApollo } from '../client.js';
import { Toolbar } from './Toolbar/index.js';
-import { SeedNode } from '../queries/seedQuery.js';
import { getConfig } from '../config/index.js';
+import { FaustContext, FaustQueries } from '../store/FaustContext.js';
+import { FaustProps } from './WordPressTemplate.js';
-export type FaustPageProps = AppProps['pageProps'] & {
- __SEED_NODE__?: SeedNode;
-};
+export type FaustPageProps = AppProps['pageProps'] & FaustProps;
export function FaustProvider(props: {
children: React.ReactNode;
@@ -22,16 +21,34 @@ export function FaustProvider(props: {
const router = useRouter();
const apolloClient = useApollo(pageProps);
+ const setQueries = (newQueries: FaustQueries) => {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ setFaustContext((prevContext) => {
+ return {
+ ...prevContext,
+ queries: newQueries,
+ };
+ });
+ };
+
+ const [faustContext, setFaustContext] = useState({
+ // eslint-disable-next-line no-underscore-dangle
+ queries: pageProps.__FAUST_QUERIES__,
+ setQueries,
+ });
+
return (
-
- {experimentalToolbar && (
-
- )}
- {children}
-
+
+
+ {experimentalToolbar && (
+
+ )}
+ {children}
+
+
);
}
diff --git a/packages/faustwp-core/src/components/Toolbar/Toolbar.tsx b/packages/faustwp-core/src/components/Toolbar/Toolbar.tsx
index c88126f5f..bceeaa665 100644
--- a/packages/faustwp-core/src/components/Toolbar/Toolbar.tsx
+++ b/packages/faustwp-core/src/components/Toolbar/Toolbar.tsx
@@ -59,7 +59,7 @@ export type FaustToolbarContext = {
* Toolbar props.
*/
export type ToolbarProps = {
- seedNode?: SeedNode;
+ seedNode?: SeedNode | null;
};
/**
* The component to actually render the toolbar. At this point we can assume
diff --git a/packages/faustwp-core/src/components/Toolbar/nodes/Edit.tsx b/packages/faustwp-core/src/components/Toolbar/nodes/Edit.tsx
index 002022fcb..8655d7aac 100644
--- a/packages/faustwp-core/src/components/Toolbar/nodes/Edit.tsx
+++ b/packages/faustwp-core/src/components/Toolbar/nodes/Edit.tsx
@@ -5,7 +5,7 @@ import { getAdminUrl } from '../../../lib/getAdminUrl.js';
import { ToolbarItem } from '../index.js';
type Props = {
- seedNode?: SeedNode;
+ seedNode?: SeedNode | null;
};
export function Edit({ seedNode }: Props) {
diff --git a/packages/faustwp-core/src/components/WordPressTemplate.tsx b/packages/faustwp-core/src/components/WordPressTemplate.tsx
index d620a9a5e..1146748a1 100644
--- a/packages/faustwp-core/src/components/WordPressTemplate.tsx
+++ b/packages/faustwp-core/src/components/WordPressTemplate.tsx
@@ -1,17 +1,34 @@
import { QueryOptions } from '@apollo/client';
-import React, { PropsWithChildren, useEffect, useState } from 'react';
-import { ensureAuthorization, getAccessToken } from '../auth/index.js';
-import { getApolloClient } from '../client.js';
+// eslint-disable-next-line import/extensions
+import { print } from '@apollo/client/utilities';
+import { sha256 } from 'js-sha256';
+import React, {
+ PropsWithChildren,
+ useContext,
+ useEffect,
+ useState,
+} from 'react';
+import { getApolloAuthClient, getApolloClient } from '../client.js';
import { getConfig } from '../config/index.js';
import { getTemplate } from '../getTemplate.js';
-import { SeedNode, SEED_QUERY } from '../queries/seedQuery.js';
+import { useAuth } from '../hooks/useAuth.js';
+import { SEED_QUERY, SeedNode } from '../queries/seedQuery.js';
+import { FaustContext, FaustQueries } from '../store/FaustContext.js';
import { getQueryParam } from '../utils/convert.js';
-export type WordPressTemplateProps = PropsWithChildren<{
- __SEED_NODE__: SeedNode | null;
- __TEMPLATE_QUERY_DATA__: any | null;
-}>;
+export type FaustProps = {
+ __SEED_NODE__?: SeedNode | null;
+ __FAUST_QUERIES__?: FaustQueries | null;
+ __TEMPLATE_QUERY_DATA__?: any | null;
+ __TEMPLATE_VARIABLES__?: { [key: string]: any } | null;
+};
+
+export type WordPressTemplateProps = PropsWithChildren;
+/**
+ * This is an external type for end users.
+ * @external
+ */
export type FaustTemplateProps> = Props & {
data?: Data;
loading?: boolean;
@@ -20,6 +37,139 @@ export type FaustTemplateProps> = Props & {
__TEMPLATE_VARIABLES__?: { [key: string]: any };
};
+export function WordPressTemplateInternal(
+ props: WordPressTemplateProps & {
+ seedNode: SeedNode;
+ isPreview: boolean;
+ isAuthenticated: boolean;
+ loading: boolean;
+ setLoading: (loading: boolean) => void;
+ },
+) {
+ const { templates } = getConfig();
+
+ if (!templates) {
+ throw new Error('Templates are required. Please add them to your config.');
+ }
+
+ const {
+ seedNode,
+ isAuthenticated,
+ isPreview,
+ __TEMPLATE_QUERY_DATA__: templateQueryDataProp,
+ loading,
+ setLoading,
+ ...wordpressTemplateProps
+ } = props;
+ const template = getTemplate(seedNode, templates);
+ const [data, setData] = useState(templateQueryDataProp);
+ const { setQueries } = useContext(FaustContext) || {};
+
+ if (template && template.queries && template.query) {
+ throw new Error(
+ '`Only either `Component.query` or `Component.queries` can be provided, but not both.',
+ );
+ }
+
+ /**
+ * Fetch the template's queries if defined.
+ */
+ useEffect(() => {
+ void (async () => {
+ const client = isPreview ? getApolloAuthClient() : getApolloClient();
+
+ if (!template) {
+ return;
+ }
+
+ if (template.query) {
+ return;
+ }
+
+ if (!template.queries) {
+ return;
+ }
+
+ if (!setQueries) {
+ return;
+ }
+
+ let queries: FaustQueries | null = null;
+
+ const queryCalls = template.queries.map(({ query, variables }) => {
+ const queryVariables = variables
+ ? variables(seedNode, { asPreview: isPreview })
+ : undefined;
+ return client.query({
+ query,
+ variables: queryVariables,
+ });
+ });
+
+ const queriesRes = await Promise.all(queryCalls);
+
+ queries = {};
+
+ queriesRes.forEach((queryRes, index) => {
+ if (queries && template.queries) {
+ queries[sha256(print(template.queries[index].query))] = queryRes.data;
+ }
+ });
+
+ setQueries(queries);
+
+ setLoading(false);
+ })();
+ }, [isAuthenticated, isPreview, seedNode, template, setQueries, setLoading]);
+
+ /**
+ * Fetch the template's query if defined.
+ */
+ useEffect(() => {
+ void (async () => {
+ const client = isPreview ? getApolloAuthClient() : getApolloClient();
+
+ if (!template || !template?.query || template?.queries || !seedNode) {
+ return;
+ }
+
+ if (data) {
+ return;
+ }
+
+ setLoading(true);
+
+ const queryArgs: QueryOptions = {
+ query: template?.query,
+ variables: template?.variables
+ ? template?.variables(seedNode, { asPreview: isPreview })
+ : undefined,
+ };
+
+ const templateQueryRes = await client.query(queryArgs);
+
+ setData(templateQueryRes.data);
+
+ setLoading(false);
+ })();
+ }, [data, template, seedNode, isPreview, isAuthenticated, setLoading]);
+
+ if (!template) {
+ return null;
+ }
+
+ const Component = template as React.FC<{ [key: string]: any }>;
+
+ const newProps = {
+ ...wordpressTemplateProps,
+ __TEMPLATE_QUERY_DATA__: templateQueryDataProp,
+ data,
+ loading,
+ };
+
+ return React.createElement(Component, newProps, null);
+}
+
export function WordPressTemplate(props: WordPressTemplateProps) {
const { basePath, templates } = getConfig();
@@ -32,21 +182,18 @@ export function WordPressTemplate(props: WordPressTemplateProps) {
__TEMPLATE_QUERY_DATA__: templateQueryDataProp,
} = props;
- const [seedNode, setSeedNode] = useState(seedNodeProp);
+ const [seedNode, setSeedNode] = useState(
+ seedNodeProp ?? null,
+ );
const template = getTemplate(seedNode, templates);
- const [data, setData] = useState(templateQueryDataProp);
const [loading, setLoading] = useState(template === null);
const [isPreview, setIsPreview] = useState(
templateQueryDataProp ? false : null,
);
- const [isAuthenticated, setIsAuthenticated] = useState<
- | true
- | {
- redirect?: string | undefined;
- login?: string | undefined;
- }
- | null
- >(null);
+ const { isAuthenticated, loginUrl } = useAuth({
+ strategy: 'redirect',
+ shouldRedirect: false,
+ });
/**
* Determine if the URL we are on is for previews
@@ -60,25 +207,18 @@ export function WordPressTemplate(props: WordPressTemplateProps) {
}, []);
/**
- * If the URL we are on is for previews, ensure we are authenticated.
+ * If we are on a preview route and there is no authenticated user, redirect
+ * them to the login page
*/
useEffect(() => {
- if (isPreview === null || isPreview === false) {
+ if (!window) {
return;
}
- void (async () => {
- const ensureAuthRes = await ensureAuthorization({
- redirectUri: window.location.href,
- });
-
- if (ensureAuthRes !== true && ensureAuthRes?.redirect) {
- window.location.replace(ensureAuthRes.redirect);
- }
-
- setIsAuthenticated(ensureAuthRes);
- })();
- }, [isPreview]);
+ if (isPreview && isAuthenticated === false && loginUrl) {
+ window.location.assign(loginUrl);
+ }
+ }, [isAuthenticated, isPreview, loginUrl]);
/**
* Execute the seed query.
@@ -95,8 +235,12 @@ export function WordPressTemplate(props: WordPressTemplateProps) {
return;
}
+ if (seedNode) {
+ return;
+ }
+
void (async () => {
- const client = getApolloClient();
+ const client = isPreview ? getApolloAuthClient() : getApolloClient();
let seedQueryUri = window.location.href.replace(
window.location.origin,
@@ -121,7 +265,7 @@ export function WordPressTemplate(props: WordPressTemplateProps) {
}
}
- let queryArgs: QueryOptions = {
+ const queryArgs: QueryOptions = {
query: SEED_QUERY,
variables: {
// Conditionally add relevant query args.
@@ -131,92 +275,31 @@ export function WordPressTemplate(props: WordPressTemplateProps) {
},
};
- if (isPreview) {
- queryArgs = {
- ...queryArgs,
- context: {
- headers: {
- /**
- * We know the access token is available here since we ensured
- * authorization in the useEffect above
- */
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Authorization: `bearer ${getAccessToken()!}`,
- },
- },
- };
- }
-
- if (!seedNode) {
- setLoading(true);
+ setLoading(true);
- const seedQueryRes = await client.query(queryArgs);
+ const seedQueryRes = await client.query(queryArgs);
- const node = isPreview
- ? (seedQueryRes?.data?.contentNode as SeedNode)
- : (seedQueryRes?.data?.nodeByUri as SeedNode);
+ const node = isPreview
+ ? (seedQueryRes?.data?.contentNode as SeedNode)
+ : (seedQueryRes?.data?.nodeByUri as SeedNode);
- setSeedNode(node);
- }
+ setSeedNode(node);
})();
}, [seedNode, isPreview, isAuthenticated, basePath]);
- /**
- * Finally, get the template's query data.
- */
- useEffect(() => {
- // We don't know yet if this is a preview route or not
- if (isPreview === null) {
- return;
- }
-
- // This is a preview route, but we are not authenticated yet.
- if (isPreview === true && isAuthenticated !== true) {
- return;
- }
-
- void (async () => {
- const client = getApolloClient();
-
- if (!template || !template?.query || !seedNode) {
- return;
- }
-
- if (!data) {
- setLoading(true);
-
- let queryArgs: QueryOptions = {
- query: template?.query,
- variables: template?.variables
- ? template?.variables(seedNode, { asPreview: isPreview })
- : undefined,
- };
-
- if (isPreview) {
- queryArgs = {
- ...queryArgs,
- context: {
- headers: {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Authorization: `bearer ${getAccessToken()!}`,
- },
- },
- };
- }
-
- const templateQueryRes = await client.query(queryArgs);
-
- setData(templateQueryRes.data);
-
- setLoading(false);
- }
- })();
- }, [data, template, seedNode, isPreview, isAuthenticated]);
-
- if (!template) {
+ if (seedNode === null || isPreview === null || isAuthenticated === null) {
return null;
}
- const Component = template as React.FC<{ [key: string]: any }>;
- return React.createElement(Component, { ...props, data, loading }, null);
+ return (
+
+ );
}
diff --git a/packages/faustwp-core/src/getWordPressProps.tsx b/packages/faustwp-core/src/getWordPressProps.tsx
index aad6aae30..c66ebb730 100644
--- a/packages/faustwp-core/src/getWordPressProps.tsx
+++ b/packages/faustwp-core/src/getWordPressProps.tsx
@@ -1,12 +1,16 @@
-import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
+// eslint-disable-next-line import/extensions
+import { print } from '@apollo/client/utilities';
import type { DocumentNode } from 'graphql';
-import { SeedNode, SEED_QUERY } from './queries/seedQuery.js';
-import { getPossibleTemplates, getTemplate } from './getTemplate.js';
-import { FaustTemplateProps } from './components/WordPressTemplate.js';
+import { sha256 } from 'js-sha256';
+import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { addApolloState, getApolloClient } from './client.js';
+import { FaustTemplateProps } from './components/WordPressTemplate.js';
import { getConfig } from './config/index.js';
+import { getPossibleTemplates, getTemplate } from './getTemplate.js';
+import { SEED_QUERY, SeedNode } from './queries/seedQuery.js';
+import { debugLog, infoLog } from './utils/log.js';
import { hooks } from './wpHooks/index.js';
-import { infoLog, debugLog } from './utils/log.js';
+import { FaustQueries } from './store/FaustContext.js';
export const DEFAULT_ISR_REVALIDATE = 60 * 15; // 15 minutes
@@ -16,6 +20,14 @@ function isSSR(
return (ctx as GetServerSidePropsContext).req !== undefined;
}
+type QueryVariablesArgs = [
+ seedNode: SeedNode,
+ context?: {
+ asPreview?: boolean;
+ locale?: string;
+ },
+ extra?: Record,
+];
const createNotFound = (
ctx: GetStaticPropsContext,
revalidate?: number | boolean,
@@ -26,14 +38,15 @@ const createNotFound = (
export type WordPressTemplate = React.FC & {
query?: DocumentNode;
- variables?: (
- seedNode: SeedNode,
- context?: {
- asPreview?: boolean;
- locale?: string;
- },
- extra?: Record,
- ) => { [key: string]: any };
+ queries?: {
+ query: DocumentNode;
+ variables?: (...args: QueryVariablesArgs) => {
+ [key: string]: any;
+ };
+ }[];
+ variables?: (...args: QueryVariablesArgs) => {
+ [key: string]: any;
+ };
};
export interface FaustTemplate
@@ -125,6 +138,12 @@ export async function getWordPressProps(
return createNotFound(ctx, revalidate);
}
+ if (template.query && template.queries) {
+ throw new Error(
+ '`Only either `Component.query` or `Component.queries` can be provided, but not both.',
+ );
+ }
+
let templateQueryRes;
const templateVariables = template?.variables
? template?.variables(
@@ -144,6 +163,35 @@ export async function getWordPressProps(
});
}
+ let queries: FaustQueries | null = null;
+ if (template.queries) {
+ const queryCalls = template.queries.map(({ query, variables }) => {
+ const queryVariables = variables
+ ? variables(
+ seedNode,
+ {
+ asPreview: false,
+ locale: ctx.locale,
+ },
+ extra,
+ )
+ : undefined;
+ return client.query({
+ query,
+ variables: queryVariables,
+ });
+ });
+ const queriesRes = await Promise.all(queryCalls);
+
+ queries = {};
+
+ queriesRes.forEach((queryRes, index) => {
+ if (queries && template.queries) {
+ queries[sha256(print(template.queries[index].query))] = queryRes.data;
+ }
+ });
+ }
+
const appProps = addApolloState(client, {
props: {
/**
@@ -153,6 +201,7 @@ export async function getWordPressProps(
__SEED_NODE__: seedNode ?? null,
__TEMPLATE_QUERY_DATA__: templateQueryRes?.data ?? null,
__TEMPLATE_VARIABLES__: templateVariables ?? null,
+ __FAUST_QUERIES__: queries ?? null,
...props,
},
});
diff --git a/packages/faustwp-core/src/hooks/useFaustContext.tsx b/packages/faustwp-core/src/hooks/useFaustContext.tsx
new file mode 100644
index 000000000..e9dcd2727
--- /dev/null
+++ b/packages/faustwp-core/src/hooks/useFaustContext.tsx
@@ -0,0 +1,19 @@
+import { DocumentNode } from '@apollo/client';
+// eslint-disable-next-line import/extensions
+import { print } from '@apollo/client/utilities';
+import { sha256 } from 'js-sha256';
+import { useContext } from 'react';
+import { FaustContext } from '../store/FaustContext.js';
+
+export function useFaustQuery(query: DocumentNode): TData {
+ const context = useContext(FaustContext);
+
+ if (context === undefined) {
+ throw new Error('useFaustQuery must be used within a FaustProvider');
+ }
+
+ const sha = sha256(print(query));
+
+ // eslint-disable-next-line no-underscore-dangle
+ return context?.queries?.[sha] as TData;
+}
diff --git a/packages/faustwp-core/src/index.ts b/packages/faustwp-core/src/index.ts
index ad001eab1..ae322d2ed 100644
--- a/packages/faustwp-core/src/index.ts
+++ b/packages/faustwp-core/src/index.ts
@@ -31,6 +31,8 @@ import {
import { useAuth } from './hooks/useAuth.js';
import { useLogin } from './hooks/useLogin.js';
import { useLogout } from './hooks/useLogout.js';
+import { useFaustQuery } from './hooks/useFaustContext.js';
+import { FaustContext } from './store/FaustContext.js';
import {
FaustToolbarNodes,
@@ -83,4 +85,6 @@ export {
FaustTemplate,
FaustPage,
hooks,
+ useFaustQuery,
+ FaustContext,
};
diff --git a/packages/faustwp-core/src/store/FaustContext.tsx b/packages/faustwp-core/src/store/FaustContext.tsx
new file mode 100644
index 000000000..65a955f11
--- /dev/null
+++ b/packages/faustwp-core/src/store/FaustContext.tsx
@@ -0,0 +1,11 @@
+import { createContext } from 'react';
+
+export type FaustQueries = { [key: string]: any };
+
+export const FaustContext = createContext<
+ | {
+ queries?: FaustQueries | null;
+ setQueries: (newQueries: FaustQueries) => void;
+ }
+ | undefined
+>(undefined);
diff --git a/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx b/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx
new file mode 100644
index 000000000..4036e4843
--- /dev/null
+++ b/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx
@@ -0,0 +1,394 @@
+/** @jest-environment jsdom */
+import React from 'react';
+import { act, render, screen } from '@testing-library/react';
+import * as getConfig from '../../src/config/index.js';
+import * as WordPressTemplate from '../../src/components/WordPressTemplate.js';
+import * as useAuth from '../../src/hooks/useAuth.js';
+import * as client from '../../src/client.js';
+import { SEED_QUERY } from '../../src/queries/seedQuery.js';
+import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client';
+import * as getTemplate from '../../src/getTemplate.js';
+import * as FaustProvider from '../../src/components/FaustProvider.js';
+
+describe('', () => {
+ const windowBackup = window;
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ window = windowBackup;
+ });
+
+ test('it throws an error if templates are not defined in config', () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({});
+
+ expect(() => render()).toThrow(
+ 'Templates are required. Please add them to your config.',
+ );
+ });
+
+ test('Properly determines whether or not the given URL is a preview or not', async () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const useAuthSpy = jest.spyOn(useAuth, 'useAuth').mockReturnValue({
+ isAuthenticated: false,
+ isReady: true,
+ loginUrl: null,
+ });
+
+ delete (window as any).location;
+ window.location = new URL('http://localhost:3000') as any as Location;
+
+ const stringIncludesSpy = jest.spyOn(String.prototype, 'includes');
+
+ await act(async () => {
+ render(
+ ,
+ );
+ });
+
+ expect(window.location.search.includes).toHaveBeenLastCalledWith(
+ 'preview=true',
+ );
+ expect(window.location.search.includes).toReturnWith(false);
+
+ delete (window as any).location;
+ window.location = new URL(
+ 'http://localhost:3000?preview=true&p=40',
+ ) as any as Location;
+
+ await act(async () => {
+ render(
+ ,
+ );
+ });
+
+ expect(window.location.search.includes).toHaveBeenLastCalledWith(
+ 'preview=true',
+ );
+ expect(window.location.search.includes).toReturnWith(true);
+ });
+
+ test('Properly redirects to login URL on preview route with no logged in user', async () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const loginUrl = 'http://testing.local/wp-login.php';
+
+ delete (window as any).location;
+ // Preview route
+ window.location = new URL(
+ 'http://localhost:3000/?preview=true&p=40',
+ ) as any as Location;
+ window.location.assign = jest.fn();
+
+ const windowLocationAssignSpy = jest.spyOn(window.location, 'assign');
+
+ const useAuthSpy = jest.spyOn(useAuth, 'useAuth').mockReturnValue({
+ isAuthenticated: false,
+ isReady: true,
+ loginUrl,
+ });
+
+ await act(async () => {
+ render(
+ ,
+ );
+ });
+
+ expect(windowLocationAssignSpy).toHaveBeenCalledWith(loginUrl);
+ });
+
+ test('makes a request for the seed query if one does not exist', async () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const useAuthSpy = jest.spyOn(useAuth, 'useAuth').mockReturnValue({
+ isAuthenticated: false,
+ isReady: true,
+ loginUrl: null,
+ });
+
+ const clientQueryMock = jest.fn();
+ const getApolloClientSpy = jest
+ .spyOn(client, 'getApolloClient')
+ .mockImplementation(
+ () =>
+ ({
+ query: clientQueryMock,
+ } as any as ApolloClient),
+ );
+
+ delete (window as any).location;
+ window.location = new URL(
+ 'http://localhost:3000/my-page',
+ ) as any as Location;
+
+ await act(async () => {
+ render();
+ });
+
+ expect(clientQueryMock).toBeCalledWith({
+ query: SEED_QUERY,
+ variables: {
+ uri: '/my-page',
+ },
+ });
+ });
+
+ test('makes a request for the preview seed query if one does not exist', async () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const useAuthSpy = jest.spyOn(useAuth, 'useAuth').mockReturnValue({
+ isAuthenticated: true,
+ isReady: true,
+ loginUrl: null,
+ });
+
+ const clientQueryMock = jest.fn();
+ const getApolloAuthClientSpy = jest
+ .spyOn(client, 'getApolloAuthClient')
+ .mockImplementation(
+ () =>
+ ({
+ query: clientQueryMock,
+ } as any as ApolloClient),
+ );
+
+ delete (window as any).location;
+ window.location = new URL(
+ 'http://localhost:3000/preview?preview=true&p=40&previewPathname=%2Fhello-world%2F',
+ ) as any as Location;
+
+ await act(async () => {
+ render();
+ });
+
+ expect(clientQueryMock).toBeCalledWith({
+ query: SEED_QUERY,
+ variables: {
+ asPreview: true,
+ id: '40',
+ },
+ });
+ });
+});
+
+describe('', () => {
+ const windowBackup = window;
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ window = windowBackup;
+ });
+
+ const setLoadingMock = jest.fn();
+
+ const props = {
+ seedNode: { databaseId: '1' },
+ setLoading: setLoadingMock,
+ loading: false,
+ isPreview: false,
+ isAuthenticated: false,
+ __TEMPLATE_QUERY_DATA__: null,
+ };
+
+ const GET_TITLE_QUERY = gql`
+ query GetTitle {
+ generalSettings {
+ title
+ }
+ }
+ `;
+
+ const GET_POST_QUERY = gql`
+ query GetPost($postId: ID!) {
+ post(id: $postId, idType: DATABASE_ID) {
+ title
+ }
+ }
+ `;
+
+ test('it throws an error if templates are not defined in config', () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({});
+ expect(() =>
+ render(),
+ ).toThrow('Templates are required. Please add them to your config.');
+ });
+
+ test('it throws an error if the template has both queries and query defined', () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const MyTemplate = () => {
+ return <>My component>;
+ };
+
+ MyTemplate.query = GET_TITLE_QUERY;
+ MyTemplate.variables = () => ({
+ databaseId: '5',
+ });
+
+ MyTemplate.queries = [
+ {
+ query: GET_POST_QUERY,
+ variables: () => ({
+ postId: '50',
+ }),
+ },
+ ];
+
+ const templateSpy = jest
+ .spyOn(getTemplate, 'getTemplate')
+ .mockReturnValue(MyTemplate);
+
+ expect(() =>
+ render(),
+ ).toThrow(
+ '`Only either `Component.query` or `Component.queries` can be provided, but not both.',
+ );
+ });
+
+ test('it queries the template.query properly', async () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const GET_TITLE_QUERY = gql`
+ query GetTitle {
+ generalSettings {
+ title
+ }
+ }
+ `;
+
+ const MyTemplate = () => {
+ return <>My component>;
+ };
+
+ MyTemplate.query = GET_TITLE_QUERY;
+ MyTemplate.variables = () => ({
+ databaseId: '5',
+ });
+
+ const templateSpy = jest
+ .spyOn(getTemplate, 'getTemplate')
+ .mockReturnValue(MyTemplate);
+
+ const clientQueryMock = jest.fn();
+ const getApolloClientSpy = jest
+ .spyOn(client, 'getApolloClient')
+ .mockImplementation(
+ () =>
+ ({
+ query: clientQueryMock,
+ } as any as ApolloClient),
+ );
+ clientQueryMock.mockImplementation(() => ({
+ data: {
+ generalSettings: {
+ title: 'testing',
+ },
+ },
+ }));
+
+ await act(async () => {
+ render();
+ });
+
+ expect(clientQueryMock).toHaveBeenCalledWith({
+ query: GET_TITLE_QUERY,
+ variables: {
+ databaseId: '5',
+ },
+ });
+ });
+
+ test('it queries the template.queries properly', async () => {
+ const getConfigSpy = jest.spyOn(getConfig, 'getConfig').mockReturnValue({
+ templates: {},
+ });
+
+ const MyTemplate = () => {
+ return <>My component>;
+ };
+
+ MyTemplate.queries = [
+ {
+ query: GET_TITLE_QUERY,
+ },
+ {
+ query: GET_POST_QUERY,
+ variables: () => ({
+ postId: '50',
+ }),
+ },
+ ];
+
+ const templateSpy = jest
+ .spyOn(getTemplate, 'getTemplate')
+ .mockReturnValue(MyTemplate);
+
+ const clientQueryMock = jest.fn();
+ const getApolloClientSpy = jest
+ .spyOn(client, 'getApolloClient')
+ .mockImplementation(
+ () =>
+ ({
+ query: clientQueryMock,
+ } as any as ApolloClient),
+ );
+
+ clientQueryMock.mockImplementationOnce(() => ({
+ data: {
+ generalSettings: {
+ title: 'testing',
+ },
+ },
+ }));
+
+ clientQueryMock.mockImplementationOnce(() => ({
+ data: {
+ post: {
+ databaseId: '50',
+ },
+ },
+ }));
+
+ await act(async () => {
+ render(
+
+
+ ,
+ );
+ });
+
+ expect(clientQueryMock).toHaveBeenCalledWith({
+ query: GET_TITLE_QUERY,
+ });
+
+ expect(clientQueryMock).toHaveBeenCalledWith({
+ query: GET_POST_QUERY,
+ variables: {
+ postId: '50',
+ },
+ });
+ });
+});