From 452fe89f0e74f9ad507ee0b24cce5dd844b2c0f4 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 22 Oct 2026 15:24:54 -0400 Subject: [PATCH 1/7] feat(core): read results of the same query run with different variables in useFaustQuery --- .../src/components/WordPressTemplate.tsx | 32 ++--- .../faustwp-core/src/getWordPressProps.tsx | 23 ++-- .../src/hooks/useFaustContext.tsx | 23 ++-- .../faustwp-core/src/store/faustQueries.ts | 116 ++++++++++++++++++ .../tests/hooks/useFaustQuery.test.tsx | 58 +++++++++ .../tests/store/faustQueries.test.ts | 115 +++++++++++++++++ 6 files changed, 324 insertions(+), 43 deletions(-) create mode 100644 packages/faustwp-core/src/store/faustQueries.ts create mode 100644 packages/faustwp-core/tests/hooks/useFaustQuery.test.tsx create mode 100644 packages/faustwp-core/tests/store/faustQueries.test.ts diff --git a/packages/faustwp-core/src/components/WordPressTemplate.tsx b/packages/faustwp-core/src/components/WordPressTemplate.tsx index ad3ec5d00..ddfd836ce 100644 --- a/packages/faustwp-core/src/components/WordPressTemplate.tsx +++ b/packages/faustwp-core/src/components/WordPressTemplate.tsx @@ -1,7 +1,4 @@ import { QueryOptions } from '@apollo/client'; -// eslint-disable-next-line import/extensions -import { print } from '@apollo/client/utilities'; -import { sha256 } from 'js-sha256'; import { useRouter } from 'next/router.js'; import React, { PropsWithChildren, @@ -24,6 +21,7 @@ import { getPreviewSeedNode, } from '../queries/seedQuery.js'; import { FaustContext, FaustQueries } from '../store/FaustContext.js'; +import { storeFaustQueries } from '../store/faustQueries.js'; import { getQueryParam } from '../utils/convert.js'; import { getBodyClasses } from '../utils/getBodyClasses.js'; import { isWordPressPreview } from '../utils/isWordPressPreview.js'; @@ -155,29 +153,23 @@ export function WordPressTemplateInternal( return; } - let queries: FaustQueries | null = null; - const queryCalls = template.queries.map(({ query, variables }) => { const queryVariables = variables ? variables(seedNode, { asPreview: isPreview, locale }) : 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; - } + return client + .query({ + query, + variables: queryVariables, + }) + .then((result) => ({ + query, + variables: queryVariables, + data: result.data, + })); }); - setQueries(queries); + setQueries(storeFaustQueries(await Promise.all(queryCalls))); setLoading(false); })(); diff --git a/packages/faustwp-core/src/getWordPressProps.tsx b/packages/faustwp-core/src/getWordPressProps.tsx index 6b43bd8a2..97e25710a 100644 --- a/packages/faustwp-core/src/getWordPressProps.tsx +++ b/packages/faustwp-core/src/getWordPressProps.tsx @@ -1,7 +1,4 @@ -// eslint-disable-next-line import/extensions -import { print } from '@apollo/client/utilities'; import type { DocumentNode } from 'graphql'; -import { sha256 } from 'js-sha256'; import { GetServerSidePropsContext, GetStaticPropsContext } from 'next'; import { addApolloState, getApolloClient } from './client.js'; import { FaustTemplateProps } from './components/WordPressTemplate.js'; @@ -16,6 +13,7 @@ import { SEED_QUERY, SeedNode } from './queries/seedQuery.js'; import { debugLog, infoLog } from './utils/log.js'; import { hooks } from './wpHooks/index.js'; import { FaustQueries } from './store/FaustContext.js'; +import { storeFaustQueries } from './store/faustQueries.js'; export const DEFAULT_ISR_REVALIDATE = 60 * 15; // 15 minutes @@ -195,20 +193,15 @@ export async function getWordPressProps( extra, ) : undefined; - return client.query({ - query, - variables: queryVariables, - }); + return client + .query({ + query, + variables: queryVariables, + }) + .then(({ data }) => ({ query, variables: queryVariables, data })); }); - const queriesRes = await Promise.all(queryCalls); - queries = {}; - - queriesRes.forEach((queryRes, index) => { - if (queries && template.queries) { - queries[sha256(print(template.queries[index].query))] = queryRes.data; - } - }); + queries = storeFaustQueries(await Promise.all(queryCalls)); } const appProps = addApolloState(client, { diff --git a/packages/faustwp-core/src/hooks/useFaustContext.tsx b/packages/faustwp-core/src/hooks/useFaustContext.tsx index f8ebf68e0..4de9c4752 100644 --- a/packages/faustwp-core/src/hooks/useFaustContext.tsx +++ b/packages/faustwp-core/src/hooks/useFaustContext.tsx @@ -1,19 +1,26 @@ 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'; +import { readFaustQuery } from '../store/faustQueries.js'; -export function useFaustQuery(query: DocumentNode): TData { +export type UseFaustQueryOptions = { + /** + * The variables the query was run with in `Component.queries`. Pass them + * when a template runs the same query more than once with different + * variables, to get the result for these variables. + */ + variables?: { [key: string]: any }; +}; + +export function useFaustQuery( + query: DocumentNode, + options?: UseFaustQueryOptions, +): 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; + return readFaustQuery(context?.queries, query, options?.variables) as TData; } diff --git a/packages/faustwp-core/src/store/faustQueries.ts b/packages/faustwp-core/src/store/faustQueries.ts new file mode 100644 index 000000000..d2b079766 --- /dev/null +++ b/packages/faustwp-core/src/store/faustQueries.ts @@ -0,0 +1,116 @@ +import { DocumentNode } from '@apollo/client'; +// eslint-disable-next-line import/extensions +import { print } from '@apollo/client/utilities'; +import { sha256 } from 'js-sha256'; +import type { FaustQueries } from './FaustContext.js'; + +type Variables = { [key: string]: any }; + +// Suffix for the entry that records which variables the query-only key's +// result was fetched with. +const VARIABLES_SUFFIX = ':variables'; + +function sortKeys(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sortKeys); + } + + if (value !== null && typeof value === 'object') { + return Object.keys(value) + .sort() + .reduce((sorted, key) => { + // eslint-disable-next-line no-param-reassign + sorted[key] = sortKeys((value as Variables)[key]); + return sorted; + }, {}); + } + + return value; +} + +/** + * Get the key a query's result is stored under. With variables, the key also + * covers them, in any key order, so the same query can be stored once per set + * of variables. + */ +export function getFaustQueryKey( + query: DocumentNode, + variables?: Variables, +): string { + const printed = print(query); + + return variables === undefined + ? sha256(printed) + : sha256(`${printed}\n${JSON.stringify(sortKeys(variables))}`); +} + +/** + * Store the results of a template's `queries`. + * + * Each result is stored once. The query-only key holds the last result for + * each query, as it always has, so `useFaustQuery(query)` keeps working. + * Results with variables that aren't under the query-only key are stored + * under a key for the query and its variables. + */ +export function storeFaustQueries( + results: { + query: DocumentNode; + variables?: Variables; + data: any; + }[], +): FaustQueries { + const queries: FaustQueries = {}; + const lastIndex = new Map(); + + results.forEach(({ query }, index) => { + lastIndex.set(getFaustQueryKey(query), index); + }); + + results.forEach(({ query, variables, data }, index) => { + const queryKey = getFaustQueryKey(query); + + if (lastIndex.get(queryKey) === index) { + queries[queryKey] = data; + + if (variables !== undefined) { + queries[queryKey + VARIABLES_SUFFIX] = getFaustQueryKey( + query, + variables, + ); + } + + return; + } + + if (variables !== undefined) { + queries[getFaustQueryKey(query, variables)] = data; + } + }); + + return queries; +} + +/** + * Read a stored query result, optionally the one for specific variables. + */ +export function readFaustQuery( + queries: FaustQueries | null | undefined, + query: DocumentNode, + variables?: Variables, +): any { + const queryKey = getFaustQueryKey(query); + + if (variables === undefined) { + return queries?.[queryKey]; + } + + const variablesKey = getFaustQueryKey(query, variables); + + if (queries?.[variablesKey] !== undefined) { + return queries[variablesKey]; + } + + return queries?.[queryKey + VARIABLES_SUFFIX] === variablesKey + ? queries[queryKey] + : undefined; +} diff --git a/packages/faustwp-core/tests/hooks/useFaustQuery.test.tsx b/packages/faustwp-core/tests/hooks/useFaustQuery.test.tsx new file mode 100644 index 000000000..b9d5b49e6 --- /dev/null +++ b/packages/faustwp-core/tests/hooks/useFaustQuery.test.tsx @@ -0,0 +1,58 @@ +/** + * @jest-environment jsdom + */ +import { gql } from '@apollo/client'; +import { renderHook } from '@testing-library/react'; +import React from 'react'; +import { useFaustQuery } from '../../src/hooks/useFaustContext'; +import { FaustContext } from '../../src/store/FaustContext'; +import { storeFaustQueries } from '../../src/store/faustQueries'; + +const GET_POSTS = gql` + query GetPosts($category: String) { + posts(where: { categoryName: $category }) { + nodes { + title + } + } + } +`; + +describe('useFaustQuery()', () => { + const queries = storeFaustQueries([ + { + query: GET_POSTS, + variables: { category: 'sports' }, + data: { id: 'sports' }, + }, + { query: GET_POSTS, variables: { category: 'news' }, data: { id: 'news' } }, + ]); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + test('returns the result for the given variables', () => { + const { result } = renderHook( + () => useFaustQuery(GET_POSTS, { variables: { category: 'sports' } }), + { wrapper }, + ); + + expect(result.current).toEqual({ id: 'sports' }); + }); + + test('returns the last result without variables', () => { + const { result } = renderHook(() => useFaustQuery(GET_POSTS), { wrapper }); + + expect(result.current).toEqual({ id: 'news' }); + }); + + test('throws outside FaustProvider', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => renderHook(() => useFaustQuery(GET_POSTS))).toThrow( + 'useFaustQuery must be used within a FaustProvider', + ); + }); +}); diff --git a/packages/faustwp-core/tests/store/faustQueries.test.ts b/packages/faustwp-core/tests/store/faustQueries.test.ts new file mode 100644 index 000000000..2be742090 --- /dev/null +++ b/packages/faustwp-core/tests/store/faustQueries.test.ts @@ -0,0 +1,115 @@ +import { gql } from '@apollo/client'; +import { + getFaustQueryKey, + readFaustQuery, + storeFaustQueries, +} from '../../src/store/faustQueries'; + +const GET_POSTS = gql` + query GetPosts($category: String) { + posts(where: { categoryName: $category }) { + nodes { + title + } + } + } +`; + +const GET_LAYOUT = gql` + query GetLayout { + generalSettings { + title + } + } +`; + +const posts = (title: string) => ({ posts: { nodes: [{ title }] } }); + +describe('getFaustQueryKey()', () => { + test('ignores the order of variable keys', () => { + expect(getFaustQueryKey(GET_POSTS, { a: 1, b: { c: 2, d: 3 } })).toBe( + getFaustQueryKey(GET_POSTS, { b: { d: 3, c: 2 }, a: 1 }), + ); + }); + + test('differs by variables and from the query-only key', () => { + const sports = getFaustQueryKey(GET_POSTS, { category: 'sports' }); + + expect(sports).not.toBe(getFaustQueryKey(GET_POSTS, { category: 'news' })); + expect(sports).not.toBe(getFaustQueryKey(GET_POSTS)); + }); +}); + +describe('storeFaustQueries() and readFaustQuery()', () => { + test('reads queries without variables as before', () => { + const queries = storeFaustQueries([ + { query: GET_LAYOUT, data: { generalSettings: { title: 'Site' } } }, + { query: GET_POSTS, variables: { category: 'news' }, data: posts('A') }, + ]); + + expect(readFaustQuery(queries, GET_LAYOUT)).toEqual({ + generalSettings: { title: 'Site' }, + }); + expect(readFaustQuery(queries, GET_POSTS)).toEqual(posts('A')); + expect(readFaustQuery(queries, GET_POSTS, { category: 'news' })).toEqual( + posts('A'), + ); + }); + + test('keeps each result when the same query runs with different variables', () => { + const queries = storeFaustQueries([ + { + query: GET_POSTS, + variables: { category: 'featured' }, + data: posts('F'), + }, + { query: GET_POSTS, variables: { category: 'sports' }, data: posts('S') }, + { query: GET_POSTS, variables: { category: 'news' }, data: posts('N') }, + ]); + + expect( + readFaustQuery(queries, GET_POSTS, { category: 'featured' }), + ).toEqual(posts('F')); + expect(readFaustQuery(queries, GET_POSTS, { category: 'sports' })).toEqual( + posts('S'), + ); + expect(readFaustQuery(queries, GET_POSTS, { category: 'news' })).toEqual( + posts('N'), + ); + // Without variables, the last result is returned, as before. + expect(readFaustQuery(queries, GET_POSTS)).toEqual(posts('N')); + }); + + test('stores each result once', () => { + const queries = storeFaustQueries([ + { query: GET_POSTS, variables: { category: 'sports' }, data: posts('S') }, + { query: GET_POSTS, variables: { category: 'news' }, data: posts('N') }, + ]); + const stored = Object.values(queries).filter( + (value) => typeof value === 'object', + ); + + expect(stored).toHaveLength(2); + }); + + test('returns undefined for variables that were never queried', () => { + const queries = storeFaustQueries([ + { query: GET_POSTS, variables: { category: 'news' }, data: posts('N') }, + ]); + + expect( + readFaustQuery(queries, GET_POSTS, { category: 'other' }), + ).toBeUndefined(); + expect( + readFaustQuery(null, GET_POSTS, { category: 'news' }), + ).toBeUndefined(); + }); + + test('a query stored without variables has no result for specific variables', () => { + const queries = storeFaustQueries([{ query: GET_POSTS, data: posts('A') }]); + + expect( + readFaustQuery(queries, GET_POSTS, { category: 'news' }), + ).toBeUndefined(); + }); +}); From 18d7aaae473e68fd254cd21e1fac9495e8028fac Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 22 Oct 2026 15:24:54 -0400 Subject: [PATCH 2/7] docs: show how to run the same query with different variables --- .../index.md | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/how-to/use-multiple-queries-in-a-template/index.md b/docs/how-to/use-multiple-queries-in-a-template/index.md index 2be231b1f..40008432b 100644 --- a/docs/how-to/use-multiple-queries-in-a-template/index.md +++ b/docs/how-to/use-multiple-queries-in-a-template/index.md @@ -144,9 +144,33 @@ export default function Component(props) { > [!note] Learn More > The [getting started example](https://github.com/wpengine/faustjs/tree/canary/examples/next/faustwp-getting-started/wp-templates) uses `Component.queries` with a shared layout query in each of its templates. +## 4. Run the same query with different variables + +To list a query more than once, for example one section of posts per category, give each entry its own variables and pass the same variables to `useFaustQuery`: + +```js title="wp-templates/front-page.js" +import { useFaustQuery } from "@faustwp/core"; +import { GET_POSTS_QUERY } from "../queries/GetPosts"; + +export default function Component() { + const { posts: sports } = + useFaustQuery(GET_POSTS_QUERY, { variables: { category: "sports" } }) ?? {}; + const { posts: news } = + useFaustQuery(GET_POSTS_QUERY, { variables: { category: "news" } }) ?? {}; + + // ... +} + +Component.queries = [ + { query: GET_POSTS_QUERY, variables: () => ({ category: "sports" }) }, + { query: GET_POSTS_QUERY, variables: () => ({ category: "news" }) }, +]; +``` + +The variables must match what the entry's `variables` function returns, in any key order. Without the second argument, `useFaustQuery` returns the last entry's result. + ## Notes and limitations - **Use one or the other.** A template can define `Component.query` or `Component.queries`, not both. Faust throws ``Only either `Component.query` or `Component.queries` can be provided, but not both.`` if it finds both. - **Results come from `useFaustQuery`, not `props.data`.** `props.data` is only filled in for `Component.query`. - **Queries run again in the browser.** After the page loads, Faust runs the queries again in the browser. On previews, they only run in the browser. In the browser, the `variables` functions receive the seed node, `asPreview` and `locale`, but not `extra`. -- **Each query can only be listed once.** Faust stores results by the query's text, so listing the same query twice with different variables keeps only the last result. To fetch the same data with different variables, define a second query with a different operation name. See [#1742](https://github.com/wpengine/faustjs/issues/1742). From 99fc5b11e83e18766c4811bad6f9ddcd4e99b51c Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 22 Oct 2026 15:25:37 -0400 Subject: [PATCH 3/7] docs: note that repeated queries' variables shouldn't depend on extra --- docs/how-to/use-multiple-queries-in-a-template/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/use-multiple-queries-in-a-template/index.md b/docs/how-to/use-multiple-queries-in-a-template/index.md index 40008432b..317aa00e0 100644 --- a/docs/how-to/use-multiple-queries-in-a-template/index.md +++ b/docs/how-to/use-multiple-queries-in-a-template/index.md @@ -167,7 +167,7 @@ Component.queries = [ ]; ``` -The variables must match what the entry's `variables` function returns, in any key order. Without the second argument, `useFaustQuery` returns the last entry's result. +The variables must match what the entry's `variables` function returns, in any key order. Since the queries run again in the browser, where the function doesn't receive `extra`, don't base these variables on `extra`. Without the second argument, `useFaustQuery` returns the last entry's result. ## Notes and limitations From 1aabc35768ec0a0dc523a6abf07cf14882691fff Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 22 Oct 2026 15:26:08 -0400 Subject: [PATCH 4/7] feat(core): export the UseFaustQueryOptions type --- packages/faustwp-core/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/faustwp-core/src/index.ts b/packages/faustwp-core/src/index.ts index 90f3d992d..a0e5e973d 100644 --- a/packages/faustwp-core/src/index.ts +++ b/packages/faustwp-core/src/index.ts @@ -33,7 +33,10 @@ import { useLogin } from './hooks/useLogin.js'; import { useBodyClass } from './hooks/useBodyClass.js'; import { getBodyClasses } from './utils/getBodyClasses.js'; import { useLogout } from './hooks/useLogout.js'; -import { useFaustQuery } from './hooks/useFaustContext.js'; +import { + useFaustQuery, + UseFaustQueryOptions, +} from './hooks/useFaustContext.js'; import { FaustContext } from './store/FaustContext.js'; import { @@ -90,5 +93,6 @@ export { FaustPage, hooks, useFaustQuery, + UseFaustQueryOptions, FaustContext, }; From dc64b31194c30e6334c248a9940825976ad74982 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 22 Oct 2026 15:28:07 -0400 Subject: [PATCH 5/7] test(core): cover repeated queries in getWordPressProps --- .../tests/getWordPressProps.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/faustwp-core/tests/getWordPressProps.test.ts diff --git a/packages/faustwp-core/tests/getWordPressProps.test.ts b/packages/faustwp-core/tests/getWordPressProps.test.ts new file mode 100644 index 000000000..bcb1af141 --- /dev/null +++ b/packages/faustwp-core/tests/getWordPressProps.test.ts @@ -0,0 +1,57 @@ +import { gql } from '@apollo/client'; +import * as client from '../src/client'; +import { setConfig } from '../src/config'; +import * as getTemplate from '../src/getTemplate'; +import { getWordPressProps } from '../src/getWordPressProps'; +import { readFaustQuery } from '../src/store/faustQueries'; + +const GET_POSTS = gql` + query GetPosts($category: String) { + posts(where: { categoryName: $category }) { + nodes { + title + } + } + } +`; + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('getWordPressProps()', () => { + test('keeps every result when a template runs the same query with different variables', async () => { + const query = jest.fn(async ({ variables }) => + variables?.uri + ? { data: { nodeByUri: { __typename: 'Page', databaseId: 1 } } } + : { data: { category: variables.category } }, + ); + jest + .spyOn(client, 'getApolloClient') + .mockReturnValue({ query, cache: { extract: () => ({}) } } as any); + + const Template = Object.assign(() => null, { + queries: [ + { query: GET_POSTS, variables: () => ({ category: 'sports' }) }, + { query: GET_POSTS, variables: () => ({ category: 'news' }) }, + ], + }); + setConfig({ templates: { page: Template } } as any); + jest.spyOn(getTemplate, 'getTemplate').mockReturnValue(Template as any); + + const result = (await getWordPressProps({ + ctx: { params: { wordpressNode: ['about'] } } as any, + })) as { props: { __FAUST_QUERIES__: any } }; + const queries = result.props.__FAUST_QUERIES__; + + expect(readFaustQuery(queries, GET_POSTS, { category: 'sports' })).toEqual({ + category: 'sports', + }); + expect(readFaustQuery(queries, GET_POSTS, { category: 'news' })).toEqual({ + category: 'news', + }); + expect(readFaustQuery(queries, GET_POSTS)).toEqual({ category: 'news' }); + // The props are serialized for the page, so they must be plain JSON. + expect(JSON.parse(JSON.stringify(queries))).toEqual(queries); + }); +}); From debaaa85bbf18f763abc0acf28b9ad432583103a Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Fri, 23 Oct 2026 10:52:39 -0400 Subject: [PATCH 6/7] fix(core): key query variables the way Apollo sends them and keep only the last repeated result --- .../faustwp-core/src/store/faustQueries.ts | 46 ++++++++++++------- .../tests/store/faustQueries.test.ts | 36 +++++++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/packages/faustwp-core/src/store/faustQueries.ts b/packages/faustwp-core/src/store/faustQueries.ts index d2b079766..6975d7308 100644 --- a/packages/faustwp-core/src/store/faustQueries.ts +++ b/packages/faustwp-core/src/store/faustQueries.ts @@ -31,7 +31,8 @@ function sortKeys(value: unknown): unknown { /** * Get the key a query's result is stored under. With variables, the key also * covers them, in any key order, so the same query can be stored once per set - * of variables. + * of variables. Variables go through JSON first, as they do when Apollo sends + * them, so values like dates keep what makes them different. */ export function getFaustQueryKey( query: DocumentNode, @@ -41,7 +42,11 @@ export function getFaustQueryKey( return variables === undefined ? sha256(printed) - : sha256(`${printed}\n${JSON.stringify(sortKeys(variables))}`); + : sha256( + `${printed}\n${JSON.stringify( + sortKeys(JSON.parse(JSON.stringify(variables))), + )}`, + ); } /** @@ -60,30 +65,39 @@ export function storeFaustQueries( }[], ): FaustQueries { const queries: FaustQueries = {}; + const entries = results.map(({ query, variables, data }) => ({ + queryKey: getFaustQueryKey(query), + variablesKey: + variables === undefined ? undefined : getFaustQueryKey(query, variables), + data, + })); const lastIndex = new Map(); - results.forEach(({ query }, index) => { - lastIndex.set(getFaustQueryKey(query), index); + // A later entry for the same query and variables replaces an earlier one. + entries.forEach(({ queryKey, variablesKey }, index) => { + lastIndex.set(queryKey, index); + if (variablesKey) { + lastIndex.set(variablesKey, index); + } }); - results.forEach(({ query, variables, data }, index) => { - const queryKey = getFaustQueryKey(query); - + entries.forEach(({ queryKey, variablesKey, data }, index) => { if (lastIndex.get(queryKey) === index) { queries[queryKey] = data; - if (variables !== undefined) { - queries[queryKey + VARIABLES_SUFFIX] = getFaustQueryKey( - query, - variables, - ); + if (variablesKey) { + queries[queryKey + VARIABLES_SUFFIX] = variablesKey; } - - return; } + }); - if (variables !== undefined) { - queries[getFaustQueryKey(query, variables)] = data; + entries.forEach(({ queryKey, variablesKey, data }, index) => { + if ( + variablesKey && + lastIndex.get(variablesKey) === index && + queries[queryKey + VARIABLES_SUFFIX] !== variablesKey + ) { + queries[variablesKey] = data; } }); diff --git a/packages/faustwp-core/tests/store/faustQueries.test.ts b/packages/faustwp-core/tests/store/faustQueries.test.ts index 2be742090..601caae01 100644 --- a/packages/faustwp-core/tests/store/faustQueries.test.ts +++ b/packages/faustwp-core/tests/store/faustQueries.test.ts @@ -38,6 +38,15 @@ describe('getFaustQueryKey()', () => { expect(sports).not.toBe(getFaustQueryKey(GET_POSTS, { category: 'news' })); expect(sports).not.toBe(getFaustQueryKey(GET_POSTS)); }); + + test('tells apart values that JSON serializes, like dates', () => { + expect( + getFaustQueryKey(GET_POSTS, { after: new Date('2026-01-01') }), + ).not.toBe(getFaustQueryKey(GET_POSTS, { after: new Date('2026-02-01') })); + expect(getFaustQueryKey(GET_POSTS, { after: new Date('2026-01-01') })).toBe( + getFaustQueryKey(GET_POSTS, { after: '2026-01-01T00:00:00.000Z' }), + ); + }); }); describe('storeFaustQueries() and readFaustQuery()', () => { @@ -112,4 +121,31 @@ describe('storeFaustQueries() and readFaustQuery()', () => { readFaustQuery(queries, GET_POSTS, { category: 'news' }), ).toBeUndefined(); }); + + test('a repeated query and variables pair keeps only the last result', () => { + const queries = storeFaustQueries([ + { + query: GET_POSTS, + variables: { category: 'sports' }, + data: posts('S1'), + }, + { query: GET_POSTS, variables: { category: 'news' }, data: posts('N') }, + { + query: GET_POSTS, + variables: { category: 'sports' }, + data: posts('S2'), + }, + ]); + + expect(readFaustQuery(queries, GET_POSTS, { category: 'sports' })).toEqual( + posts('S2'), + ); + expect(readFaustQuery(queries, GET_POSTS)).toEqual(posts('S2')); + expect(readFaustQuery(queries, GET_POSTS, { category: 'news' })).toEqual( + posts('N'), + ); + expect( + Object.values(queries).filter((value) => typeof value === 'object'), + ).toHaveLength(2); + }); }); From fa25ff02636b94abdcbff3eaa3b17d117792582f Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Fri, 23 Oct 2026 10:52:39 -0400 Subject: [PATCH 7/7] test(core): resolve client.query mocks with promises like Apollo does --- .../tests/components/WordPressTemplate.test.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx b/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx index 8193eb704..36be3b487 100644 --- a/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx +++ b/packages/faustwp-core/tests/components/WordPressTemplate.test.tsx @@ -308,21 +308,22 @@ xdescribe('', () => { } as any as ApolloClient), ); - clientQueryMock.mockImplementationOnce(() => ({ + // client.query() returns a promise, like Apollo's. + clientQueryMock.mockResolvedValueOnce({ data: { generalSettings: { title: 'testing', }, }, - })); + }); - clientQueryMock.mockImplementationOnce(() => ({ + clientQueryMock.mockResolvedValueOnce({ data: { post: { databaseId: '50', }, }, - })); + }); await act(async () => { render(