diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..0ecd796
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,62 @@
+const path = require("path");
+
+module.exports = {
+ parser: "@typescript-eslint/parser",
+ plugins: ["react", "react-hooks"],
+
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ ],
+
+ parserOptions: {
+ sourceType: "module",
+ project: path.resolve(__dirname + "/tsconfig.json"),
+ },
+
+ env: {
+ browser: true,
+ es2022: true,
+ },
+
+ overrides: [
+ {
+ files: ["**/__{mocks,tests}__/**/*.{ts,tsx}"],
+ rules: {
+ "no-empty": ["error", { allowEmptyCatch: true }],
+ },
+ },
+ {
+ files: ["*.d.ts"],
+ rules: {
+ "@typescript-eslint/consistent-type-definitions": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ },
+ },
+ {
+ files: ["clients/**/src/graphql/**/*.{ts,tsx}"],
+ rules: {
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ },
+ },
+ ],
+
+ rules: {
+ "no-implicit-coercion": "error",
+ "no-param-reassign": "error",
+ "no-var": "error",
+ "object-shorthand": "warn",
+ "prefer-const": "error",
+
+ "no-extra-boolean-cast": "off",
+
+ "react/jsx-boolean-value": ["error", "always"],
+
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn",
+ },
+
+ ignorePatterns: [".eslintrc.js"],
+};
diff --git a/docs/docs/use-query.md b/docs/docs/use-query.md
index c156b64..8e8fb1e 100644
--- a/docs/docs/use-query.md
+++ b/docs/docs/use-query.md
@@ -11,6 +11,7 @@ sidebar_label: useQuery
- `variables`: your query variables
- `config` (optional)
- `suspense`: use React Suspense (default: `false`)
+ - `optimize`: adapt query to only require data that's missing from the cache (default: `false`)
### Returns
diff --git a/example/components/AccountMembership.tsx b/example/components/AccountMembership.tsx
index e10d54d..ef5fdff 100644
--- a/example/components/AccountMembership.tsx
+++ b/example/components/AccountMembership.tsx
@@ -1,6 +1,8 @@
import { FragmentOf, readFragment } from "gql.tada";
+import { useState } from "react";
import { P, match } from "ts-pattern";
import { graphql } from "../graphql";
+import { AccountMembershipDetail } from "./AccountMembershipDetail";
export const accountMembershipFragment = graphql(`
fragment AccountMembership on AccountMembership {
@@ -12,7 +14,6 @@ export const accountMembershipFragment = graphql(`
}
statusInfo {
__typename
- status
... on AccountMembershipBindingUserErrorStatusInfo {
restrictedTo {
firstName
@@ -36,27 +37,37 @@ type Props = {
export const AccountMembership = ({ data }: Props) => {
const accountMembership = readFragment(accountMembershipFragment, data);
+ const [isOpen, setIsOpen] = useState(false);
+
return (
-
-
{accountMembership.id}:
- {match(accountMembership)
- .with(
- { user: { firstName: P.string, lastName: P.string } },
- ({ user: { firstName, lastName } }) => `${firstName} ${lastName}`,
- )
- .with(
- {
- statusInfo: {
- restrictedTo: { firstName: P.string, lastName: P.string },
- },
- },
- ({
- statusInfo: {
- restrictedTo: { firstName, lastName },
+ <>
+
setIsOpen(true)}>
+ {accountMembership.id}:
+ {match(accountMembership)
+ .with(
+ { user: { firstName: P.string, lastName: P.string } },
+ ({ user: { firstName, lastName } }) => `${firstName} ${lastName}`,
+ )
+ .with(
+ {
+ statusInfo: {
+ restrictedTo: { firstName: P.string, lastName: P.string },
+ },
},
- }) => `${firstName} ${lastName} (restricted to)`,
- )
- .otherwise(() => "No user")}
-
+ ({
+ statusInfo: {
+ restrictedTo: { firstName, lastName },
+ },
+ }) => `${firstName} ${lastName} (restricted to)`,
+ )
+ .otherwise(() => "No user")}
+
+ {isOpen ? (
+ setIsOpen(false)}
+ />
+ ) : null}
+ >
);
};
diff --git a/example/components/AccountMembershipDetail.tsx b/example/components/AccountMembershipDetail.tsx
new file mode 100644
index 0000000..cf0c9eb
--- /dev/null
+++ b/example/components/AccountMembershipDetail.tsx
@@ -0,0 +1,70 @@
+import { AsyncData, Result } from "@swan-io/boxed";
+import { useState } from "react";
+import { P, match } from "ts-pattern";
+import { ClientError, useQuery } from "../../src";
+import { graphql } from "../graphql";
+import { UserCard } from "./UserCard";
+
+export const accountMembershipDetailQuery = graphql(`
+ query AccountMembershipDetail($accountMembershipId: ID!) {
+ accountMembership(id: $accountMembershipId) {
+ id
+ user {
+ id
+ firstName
+ lastName
+ }
+ }
+ }
+`);
+
+type Props = {
+ accountMembershipId: string;
+ onPressClose: () => void;
+};
+
+export const AccountMembershipDetail = ({
+ accountMembershipId,
+ onPressClose,
+}: Props) => {
+ const [data] = useQuery(
+ accountMembershipDetailQuery,
+ { accountMembershipId },
+ { optimize: true },
+ );
+
+ const [showDetails, setShowDetails] = useState(false);
+
+ return match(data)
+ .with(AsyncData.P.NotAsked, () => "Nothing")
+ .with(AsyncData.P.Loading, () => "Loading")
+ .with(AsyncData.P.Done(Result.P.Error(P.select())), (error) => {
+ ClientError.forEach(error, (error) => console.error(error));
+ return "Error";
+ })
+ .with(
+ AsyncData.P.Done(Result.P.Ok(P.select())),
+ ({ accountMembership }) => {
+ if (accountMembership == null) {
+ return No membership
;
+ }
+ return (
+
+ );
+ },
+ )
+ .exhaustive();
+};
diff --git a/example/components/App.tsx b/example/components/App.tsx
index 84048f0..67e9342 100644
--- a/example/components/App.tsx
+++ b/example/components/App.tsx
@@ -30,6 +30,7 @@ export const App = () => {
"fa3a2485-43bc-461e-b38c-5a9bc3750c7d",
);
const [suspense, setSuspense] = useState(false);
+ const [optimize, setOptimize] = useState(false);
const [data, { isLoading, reload, refresh }] = useQuery(
transactionListQuery,
@@ -37,7 +38,7 @@ export const App = () => {
accountMembershipId,
after,
},
- { suspense },
+ { suspense, optimize },
);
const toggleAccountMembership = useCallback(() => {
@@ -65,6 +66,14 @@ export const App = () => {
/>
Suspense
+
{match(data)
diff --git a/example/components/UserCard.tsx b/example/components/UserCard.tsx
new file mode 100644
index 0000000..fa7a64f
--- /dev/null
+++ b/example/components/UserCard.tsx
@@ -0,0 +1,80 @@
+import { AsyncData, Result } from "@swan-io/boxed";
+import { P, match } from "ts-pattern";
+import { ClientError, useQuery } from "../../src";
+import { graphql } from "../graphql";
+
+export const accountMembershipUserDetailQuery = graphql(`
+ query AccountMembershipUserDetail($accountMembershipId: ID!) {
+ accountMembership(id: $accountMembershipId) {
+ id
+ user {
+ id
+ firstName
+ lastName
+ birthDate
+ mobilePhoneNumber
+ }
+ }
+ }
+`);
+
+type Props = {
+ accountMembershipId: string;
+};
+
+const formatter = new Intl.DateTimeFormat();
+
+export const UserCard = ({ accountMembershipId }: Props) => {
+ const [data] = useQuery(
+ accountMembershipUserDetailQuery,
+ {
+ accountMembershipId,
+ },
+ { optimize: true },
+ );
+
+ return match(data)
+ .with(AsyncData.P.NotAsked, () => "Nothing")
+ .with(AsyncData.P.Loading, () => "Loading")
+ .with(AsyncData.P.Done(Result.P.Error(P.select())), (error) => {
+ ClientError.forEach(error, (error) => console.error(error));
+ return "Error";
+ })
+ .with(
+ AsyncData.P.Done(Result.P.Ok(P.select())),
+ ({ accountMembership }) => {
+ if (accountMembership == null) {
+ return null;
+ }
+ const user = accountMembership.user;
+ if (user == null) {
+ return No user
;
+ }
+ return (
+
+
+ -
+ ID: {user.id}
+
+ -
+ First name: {user.firstName}
+
+ -
+ Last name: {user.lastName}
+
+ {user.birthDate != null ? (
+ -
+ Birthdate:{" "}
+ {formatter.format(new Date(user.birthDate))}
+
+ ) : null}
+ -
+ Mobile phone number: {user.mobilePhoneNumber}
+
+
+
+ );
+ },
+ )
+ .exhaustive();
+};
diff --git a/example/graphql.ts b/example/graphql.ts
index 6e20c13..3da56b1 100644
--- a/example/graphql.ts
+++ b/example/graphql.ts
@@ -7,5 +7,7 @@ export const graphql = initGraphQLTada<{
ID: string;
Currency: string;
AmountValue: string;
+ Date: string;
+ PhoneNumber: string;
};
}>();
diff --git a/src/cache/cache.ts b/src/cache/cache.ts
index 1229961..5cd766b 100644
--- a/src/cache/cache.ts
+++ b/src/cache/cache.ts
@@ -96,6 +96,12 @@ export class ClientCache {
});
}
+ getFromCacheWithoutKey(cacheKey: symbol) {
+ return this.get(cacheKey).flatMap((entry) => {
+ return Option.Some(entry.value);
+ });
+ }
+
get(cacheKey: symbol): Option {
if (this.cache.has(cacheKey)) {
return Option.Some(this.cache.get(cacheKey) as CacheEntry);
diff --git a/src/cache/read.ts b/src/cache/read.ts
index 5033eba..733cf58 100644
--- a/src/cache/read.ts
+++ b/src/cache/read.ts
@@ -1,7 +1,15 @@
-import { DocumentNode, Kind, SelectionSetNode } from "@0no-co/graphql.web";
+import {
+ DocumentNode,
+ InlineFragmentNode,
+ Kind,
+ OperationDefinitionNode,
+ SelectionNode,
+ SelectionSetNode,
+} from "@0no-co/graphql.web";
import { Array, Option, Result } from "@swan-io/boxed";
import { match } from "ts-pattern";
import {
+ addIdIfPreviousSelected,
getFieldName,
getFieldNameWithArguments,
getSelectedKeys,
@@ -24,6 +32,15 @@ const getFromCacheOrReturnValue = (
: Option.Some(valueOrKey);
};
+const getFromCacheOrReturnValueWithoutKeyFilter = (
+ cache: ClientCache,
+ valueOrKey: unknown,
+): Option => {
+ return typeof valueOrKey === "symbol"
+ ? cache.getFromCacheWithoutKey(valueOrKey).flatMap(Option.fromNullable)
+ : Option.Some(valueOrKey);
+};
+
const STABILITY_CACHE = new WeakMap>();
export const readOperationFromCache = (
@@ -180,3 +197,159 @@ export const readOperationFromCache = (
}
});
};
+
+export const optimizeQuery = (
+ cache: ClientCache,
+ document: DocumentNode,
+ variables: Record,
+): Option => {
+ const traverse = (
+ selections: SelectionSetNode,
+ data: Record,
+ parentSelectedKeys: Set,
+ ): Option => {
+ const nextSelections = Array.filterMap(
+ selections.selections,
+ (selection) => {
+ return match(selection)
+ .with({ kind: Kind.FIELD }, (fieldNode) => {
+ const fieldNameWithArguments = getFieldNameWithArguments(
+ fieldNode,
+ variables,
+ );
+
+ if (data == undefined) {
+ return Option.Some(fieldNode);
+ }
+
+ const cacheHasKey = hasOwnProperty.call(
+ data,
+ fieldNameWithArguments,
+ );
+
+ if (!cacheHasKey) {
+ return Option.Some(fieldNode);
+ }
+
+ if (parentSelectedKeys.has(fieldNameWithArguments)) {
+ const valueOrKeyFromCache = data[fieldNameWithArguments];
+
+ const subFieldSelectedKeys = getSelectedKeys(
+ fieldNode,
+ variables,
+ );
+ if (Array.isArray(valueOrKeyFromCache)) {
+ return valueOrKeyFromCache.reduce((acc, valueOrKey) => {
+ const value = getFromCacheOrReturnValueWithoutKeyFilter(
+ cache,
+ valueOrKey,
+ );
+
+ if (value.isNone()) {
+ return Option.Some(fieldNode);
+ }
+
+ const originalSelectionSet = fieldNode.selectionSet;
+ if (originalSelectionSet != null) {
+ return traverse(
+ originalSelectionSet,
+ value.get() as Record,
+ subFieldSelectedKeys,
+ ).map((selectionSet) => ({
+ ...fieldNode,
+ selectionSet: addIdIfPreviousSelected(
+ originalSelectionSet,
+ selectionSet,
+ ),
+ }));
+ } else {
+ return acc;
+ }
+ }, Option.None());
+ } else {
+ const value = getFromCacheOrReturnValueWithoutKeyFilter(
+ cache,
+ valueOrKeyFromCache,
+ );
+
+ if (value.isNone()) {
+ return Option.Some(fieldNode);
+ }
+
+ const originalSelectionSet = fieldNode.selectionSet;
+ if (originalSelectionSet != null) {
+ return traverse(
+ originalSelectionSet,
+ value.get() as Record,
+ subFieldSelectedKeys,
+ ).map((selectionSet) => ({
+ ...fieldNode,
+ selectionSet: addIdIfPreviousSelected(
+ originalSelectionSet,
+ selectionSet,
+ ),
+ }));
+ } else {
+ return Option.None();
+ }
+ }
+ } else {
+ return Option.Some(fieldNode);
+ }
+ })
+ .with({ kind: Kind.INLINE_FRAGMENT }, (inlineFragmentNode) => {
+ return traverse(
+ inlineFragmentNode.selectionSet,
+ data as Record,
+ parentSelectedKeys,
+ ).map(
+ (selectionSet) =>
+ ({ ...inlineFragmentNode, selectionSet }) as InlineFragmentNode,
+ );
+ })
+ .with({ kind: Kind.FRAGMENT_SPREAD }, () => {
+ return Option.None();
+ })
+ .exhaustive();
+ },
+ );
+ if (nextSelections.length > 0) {
+ return Option.Some({ ...selections, selections: nextSelections });
+ } else {
+ return Option.None();
+ }
+ };
+
+ return Array.findMap(document.definitions, (definition) =>
+ definition.kind === Kind.OPERATION_DEFINITION
+ ? Option.Some(definition)
+ : Option.None(),
+ )
+ .flatMap((operation) =>
+ getCacheKeyFromOperationNode(operation).map((cacheKey) => ({
+ operation,
+ cacheKey,
+ })),
+ )
+ .flatMap(({ operation, cacheKey }) => {
+ const selectedKeys = getSelectedKeys(operation, variables);
+ return cache
+ .getFromCache(cacheKey, selectedKeys)
+ .map((cache) => ({ cache, operation, selectedKeys }));
+ })
+ .flatMap(({ operation, cache, selectedKeys }) => {
+ return traverse(
+ operation.selectionSet,
+ cache as Record,
+ selectedKeys,
+ ).map((selectionSet) => ({
+ ...document,
+ definitions: [
+ {
+ ...operation,
+ selectionSet,
+ } as OperationDefinitionNode,
+ ],
+ }));
+ });
+};
diff --git a/src/client.ts b/src/client.ts
index 9ed7359..c594bca 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -11,7 +11,7 @@ import {
} from "@swan-io/request";
import { P, match } from "ts-pattern";
import { ClientCache } from "./cache/cache";
-import { readOperationFromCache } from "./cache/read";
+import { optimizeQuery, readOperationFromCache } from "./cache/read";
import { writeOperationToCache } from "./cache/write";
import {
ClientError,
@@ -85,6 +85,8 @@ const defaultMakeRequest: MakeRequest = ({
.mapOkToResult(emptyToError);
};
+type RequestOptions = { optimize?: boolean };
+
export class Client {
url: string;
headers: Record;
@@ -136,7 +138,8 @@ export class Client {
request(
document: TypedDocumentNode,
variables: Variables,
- ) {
+ { optimize = false }: RequestOptions = {},
+ ): Future> {
const transformedDocument = this.getTransformedDocument(document);
const transformedDocumentsForRequest =
this.getTransformedDocumentsForRequest(document);
@@ -148,11 +151,30 @@ export class Client {
const variablesAsRecord = variables as Record;
+ const possiblyOptimizedQuery = optimize
+ ? optimizeQuery(this.cache, transformedDocument, variablesAsRecord).map(
+ addTypenames,
+ )
+ : Option.Some(transformedDocumentsForRequest);
+
+ if (possiblyOptimizedQuery.isNone()) {
+ const operationResult = readOperationFromCache(
+ this.cache,
+ transformedDocument,
+ variablesAsRecord,
+ );
+ if (operationResult.isSome()) {
+ return Future.value(operationResult.get() as Result);
+ }
+ }
+
return this.makeRequest({
url: this.url,
headers: this.headers,
operationName,
- document: transformedDocumentsForRequest,
+ document: possiblyOptimizedQuery.getWithDefault(
+ transformedDocumentsForRequest,
+ ),
variables: variablesAsRecord,
})
.mapOkToResult(this.parseResponse)
@@ -200,14 +222,16 @@ export class Client {
query(
document: TypedDocumentNode,
variables: Variables,
+ requestOptions?: RequestOptions,
) {
- return this.request(document, variables);
+ return this.request(document, variables, requestOptions);
}
commitMutation(
document: TypedDocumentNode,
variables: Variables,
+ requestOptions?: RequestOptions,
) {
- return this.request(document, variables);
+ return this.request(document, variables, requestOptions);
}
}
diff --git a/src/graphql/ast.ts b/src/graphql/ast.ts
index 42534df..5c257b2 100644
--- a/src/graphql/ast.ts
+++ b/src/graphql/ast.ts
@@ -251,3 +251,42 @@ export const getExecutableOperationName = (document: DocumentNode) => {
}
});
};
+
+const getIdFieldNode = (selection: SelectionNode): Option => {
+ return match(selection)
+ .with({ kind: Kind.FIELD }, (fieldNode) =>
+ fieldNode.name.value === "id" ? Option.Some(fieldNode) : Option.None(),
+ )
+ .with({ kind: Kind.INLINE_FRAGMENT }, (inlineFragmentNode) => {
+ return Array.findMap(
+ inlineFragmentNode.selectionSet.selections,
+ getIdFieldNode,
+ );
+ })
+ .otherwise(() => Option.None());
+};
+
+export const addIdIfPreviousSelected = (
+ oldSelectionSet: SelectionSetNode,
+ newSelectionSet: SelectionSetNode,
+): SelectionSetNode => {
+ const idSelection = Array.findMap(oldSelectionSet.selections, getIdFieldNode);
+ const idSelectionInNew = Array.findMap(
+ newSelectionSet.selections,
+ getIdFieldNode,
+ );
+
+ if (idSelectionInNew.isSome()) {
+ return newSelectionSet;
+ }
+
+ return idSelection
+ .map((selection) => ({
+ ...newSelectionSet,
+ selections: [
+ selection,
+ ...newSelectionSet.selections,
+ ] as readonly SelectionNode[],
+ }))
+ .getWithDefault(newSelectionSet);
+};
diff --git a/src/react/useQuery.ts b/src/react/useQuery.ts
index 3141878..92abd5d 100644
--- a/src/react/useQuery.ts
+++ b/src/react/useQuery.ts
@@ -15,6 +15,7 @@ import { ClientContext } from "./ClientContext";
export type QueryConfig = {
suspense?: boolean;
+ optimize?: boolean;
};
export type Query = readonly [
@@ -39,7 +40,7 @@ const usePreviousValue = (value: T): T => {
export const useQuery = (
query: TypedDocumentNode,
variables: Variables,
- { suspense = false }: QueryConfig = {},
+ { suspense = false, optimize = false }: QueryConfig = {},
): Query => {
const client = useContext(ClientContext);
@@ -80,9 +81,9 @@ export const useQuery = (
isSuspenseFirstFetch.current = false;
return;
}
- const request = client.query(stableQuery, stableVariables);
+ const request = client.query(stableQuery, stableVariables, { optimize });
return () => request.cancel();
- }, [client, suspense, stableQuery, stableVariables]);
+ }, [client, suspense, optimize, stableQuery, stableVariables]);
const [isRefreshing, setIsRefreshing] = useState(false);
const refresh = useCallback(() => {
@@ -108,7 +109,7 @@ export const useQuery = (
: asyncData;
if (suspense && asyncDataToExpose.isLoading()) {
- throw client.query(stableQuery, stableVariables).toPromise();
+ throw client.query(stableQuery, stableVariables, { optimize }).toPromise();
}
return [asyncDataToExpose, { isLoading, refresh, reload }];
diff --git a/test/__snapshots__/cache.test.ts.snap b/test/__snapshots__/cache.test.ts.snap
index 1d03157..f03b7ee 100644
--- a/test/__snapshots__/cache.test.ts.snap
+++ b/test/__snapshots__/cache.test.ts.snap
@@ -426,3 +426,7 @@ Map {
},
}
`;
+
+exports[`Write & read in cache 4`] = `"query App($id: ID!) {accountMembership(id: $id) {id user {id ... on User {birthDate mobilePhoneNumber}}}}"`;
+
+exports[`Write & read in cache 5`] = `"query App($id: ID!) {accountMembership(id: $id) {id user {id ... on User {birthDate mobilePhoneNumber}}} accountMemberships(first: 2) {edges {node {id createdAt account {bankDetails}}}}}"`;
diff --git a/test/cache.test.ts b/test/cache.test.ts
index 34aae39..56d198c 100644
--- a/test/cache.test.ts
+++ b/test/cache.test.ts
@@ -1,15 +1,18 @@
import { Option, Result } from "@swan-io/boxed";
import { expect, test } from "vitest";
import { ClientCache } from "../src/cache/cache";
-import { readOperationFromCache } from "../src/cache/read";
+import { optimizeQuery, readOperationFromCache } from "../src/cache/read";
import { writeOperationToCache } from "../src/cache/write";
import { addTypenames, inlineFragments } from "../src/graphql/ast";
+import { print } from "../src/graphql/print";
import {
appQuery,
+ appQueryWithExtraArrayInfo,
bindAccountMembershipMutation,
bindMembershipMutationRejectionResponse,
bindMembershipMutationSuccessResponse,
getAppQueryResponse,
+ otherAppQuery,
} from "./data";
test("Write & read in cache", () => {
@@ -50,6 +53,11 @@ test("Write & read in cache", () => {
addTypenames(bindAccountMembershipMutation),
);
+ const preparedOtherAppQuery = inlineFragments(addTypenames(otherAppQuery));
+ const preparedAppQueryWithExtraArrayInfo = inlineFragments(
+ addTypenames(appQueryWithExtraArrayInfo),
+ );
+
writeOperationToCache(
cache,
preparedBindAccountMembershipMutation,
@@ -152,4 +160,16 @@ test("Write & read in cache", () => {
} else {
expect(true).toBe(false);
}
+
+ expect(
+ optimizeQuery(cache, preparedOtherAppQuery, { id: "1" })
+ .map(print)
+ .getWithDefault("no delta"),
+ ).toMatchSnapshot();
+
+ expect(
+ optimizeQuery(cache, preparedAppQueryWithExtraArrayInfo, { id: "1" })
+ .map(print)
+ .getWithDefault("no delta"),
+ ).toMatchSnapshot();
});
diff --git a/test/data.ts b/test/data.ts
index 2074b10..7703cb1 100644
--- a/test/data.ts
+++ b/test/data.ts
@@ -22,6 +22,19 @@ const UserInfo = graphql(
[IdentificationLevels],
);
+const CompleteUserInfo = graphql(
+ `
+ fragment CompleteUserInfo on User {
+ id
+ firstName
+ lastName
+ birthDate
+ mobilePhoneNumber
+ }
+ `,
+ [IdentificationLevels],
+);
+
export const appQuery = graphql(
`
query App($id: ID!) {
@@ -60,6 +73,85 @@ export const appQuery = graphql(
[UserInfo],
);
+export const otherAppQuery = graphql(
+ `
+ query App($id: ID!) {
+ accountMembership(id: $id) {
+ id
+ user {
+ id
+ ...CompleteUserInfo
+ }
+ }
+ accountMemberships(first: 2) {
+ edges {
+ node {
+ id
+ account {
+ name
+ }
+ membershipUser: user {
+ id
+ lastName
+ }
+ }
+ }
+ }
+ supportingDocumentCollection(id: "e8d38e87-9862-47ef-b749-212ed566b955") {
+ __typename
+ supportingDocuments {
+ __typename
+ id
+ createdAt
+ }
+ id
+ }
+ }
+ `,
+ [CompleteUserInfo],
+);
+
+export const appQueryWithExtraArrayInfo = graphql(
+ `
+ query App($id: ID!) {
+ accountMembership(id: $id) {
+ id
+ user {
+ id
+ ...CompleteUserInfo
+ }
+ }
+ accountMemberships(first: 2) {
+ edges {
+ node {
+ id
+ createdAt
+ account {
+ name
+ bankDetails
+ }
+ membershipUser: user {
+ id
+ lastName
+ firstName
+ }
+ }
+ }
+ }
+ supportingDocumentCollection(id: "e8d38e87-9862-47ef-b749-212ed566b955") {
+ __typename
+ supportingDocuments {
+ __typename
+ id
+ createdAt
+ }
+ id
+ }
+ }
+ `,
+ [CompleteUserInfo],
+);
+
export const getAppQueryResponse = ({
user2LastName,
user1IdentificationLevels,