diff --git a/.env.development b/.env.development
index fd6efcd5..836cd55b 100644
--- a/.env.development
+++ b/.env.development
@@ -1,6 +1,8 @@
EAS_PROJECT_ID=""
APP_NAME="Dev"
+API_URL=""
+
SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
diff --git a/.env.production b/.env.production
index a7493af3..7030c1c8 100644
--- a/.env.production
+++ b/.env.production
@@ -1,6 +1,8 @@
EAS_PROJECT_ID=""
APP_NAME="RN Starter"
+API_URL=""
+
SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
diff --git a/.env.staging b/.env.staging
index 2bb5c128..4929f11a 100644
--- a/.env.staging
+++ b/.env.staging
@@ -1,6 +1,8 @@
EAS_PROJECT_ID=""
APP_NAME="Staging"
+API_URL=""
+
SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9be4c85c..7b31714c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -19,5 +19,7 @@
"json"
],
"doppler.autocomplete.enable": true,
- "doppler.hover.enable": true
+ "doppler.hover.enable": true,
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true
}
diff --git a/codegen.ts b/codegen.ts
new file mode 100644
index 00000000..fbaaeabc
--- /dev/null
+++ b/codegen.ts
@@ -0,0 +1,48 @@
+import type { CodegenConfig } from '@graphql-codegen/cli';
+
+const config: CodegenConfig = {
+ overwrite: true,
+ schema: 'https://graphqlzero.almansi.me/api',
+ documents: 'src/**/*.tsx',
+ generates: {
+ // 'src/gql/generated/types.ts': {
+ // plugins: ['typescript'],
+ // config: {
+ // enumsAsTypes: true,
+ // disableDescriptions: true,
+ // strictScalars: true,
+ // defaultScalarType: 'unknown',
+ // scalars: {
+ // Date: 'string',
+ // DateTime: 'string',
+ // },
+ // },
+ // },
+ 'src/gql/generated/': {
+ preset: 'client',
+ },
+ 'src/gql/generated/hooks.ts': {
+ plugins: [
+ 'typescript',
+ 'typescript-operations',
+ 'typescript-react-query',
+ ],
+ config: {
+ reactQueryVersion: 5,
+ exposeQueryKeys: true,
+ exposeFetcher: true,
+ fetcher: '../../core/api/request#request',
+ },
+ },
+ 'src/gql/graphql.schema.json': {
+ plugins: ['introspection'],
+ },
+ },
+ hooks: {
+ afterAllFileWrite: [
+ "eslint ./src/gql --ext .ts,.json --fix && yarn prettier --write './src/gql/**/*.ts'",
+ ],
+ },
+};
+
+export default config;
diff --git a/env.js b/env.js
index dd89d0ba..014e0031 100644
--- a/env.js
+++ b/env.js
@@ -37,6 +37,7 @@ const client = z.object({
VERSION: z.string(),
// ADD CLIENT ENV VARS HERE
+ API_URL: z.string(),
FLAGSMITH_KEY: z.string(),
MIXPANEL_TOKEN: z.string(),
SENTRY_DSN: z.string(),
@@ -62,6 +63,7 @@ const _clientEnv = {
// ADD ENV VARS HERE TOO
APP_NAME: process.env.APP_NAME,
+ API_URL: process.env.API_URL,
FLAGSMITH_KEY: process.env.FLAGSMITH_KEY,
MIXPANEL_TOKEN: process.env.MIXPANEL_TOKEN,
SENTRY_DSN: process.env.SENTRY_DSN,
diff --git a/package.json b/package.json
index 96dc44fa..816a3ae4 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,8 @@
"prepare": "husky",
"postinstall": "husky && patch-package",
"prepack": "pinst --disable",
- "postpack": "pinst --enable"
+ "postpack": "pinst --enable",
+ "codegen": "graphql-codegen --config codegen.ts"
},
"dependencies": {
"@react-native-masked-view/masked-view": "0.3.0",
@@ -75,6 +76,9 @@
"@react-navigation/native-stack": "6.9.26",
"@sentry/react-native": "5.19.1",
"@shopify/restyle": "2.4.2",
+ "@tanstack/query-async-storage-persister": "5.29.1",
+ "@tanstack/react-query": "5.29.2",
+ "@tanstack/react-query-persist-client": "5.29.2",
"dayjs": "1.11.10",
"expo": "~50.0.14",
"expo-application": "~5.8.3",
@@ -91,6 +95,8 @@
"expo-status-bar": "~1.11.1",
"expo-updates": "~0.24.12",
"flagsmith": "3.23.2",
+ "graphql": "16.8.1",
+ "graphql-request": "6.1.0",
"i18next": "23.10.1",
"lodash": "4.17.21",
"mixpanel-react-native": "2.4.0",
@@ -113,12 +119,19 @@
"zod": "3.22.4"
},
"devDependencies": {
+ "@0no-co/graphqlsp": "1.9.1",
"@babel/core": "7.23.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@commitlint/cli": "19.2.1",
"@commitlint/config-conventional": "19.1.0",
"@config-plugins/detox": "3.0.0",
+ "@graphql-codegen/cli": "5.0.2",
+ "@graphql-codegen/client-preset": "4.2.5",
+ "@graphql-codegen/introspection": "4.0.3",
+ "@graphql-codegen/typescript": "4.0.6",
+ "@graphql-codegen/typescript-operations": "4.2.0",
+ "@graphql-codegen/typescript-react-query": "6.1.0",
"@svgr/cli": "8.1.0",
"@testing-library/jest-native": "5.4.3",
"@testing-library/react-native": "12.4.5",
diff --git a/src/App.tsx b/src/App.tsx
index 06241462..433a5fd5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,5 @@
import { ThemeProvider } from '@shopify/restyle';
+import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import type { ErrorInfo } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { StatusBar, StyleSheet } from 'react-native';
@@ -9,6 +10,7 @@ import {
} from 'react-native-safe-area-context';
import Toast from 'react-native-toast-message';
+import { persistOptions, queryClient } from '$core/api/queryClient';
import { bootstrapExternalSdks } from '$core/bootstrapExternalSdks';
import { ErrorMonitoring } from '$core/monitoring';
import { RootStack } from '$core/navigation';
@@ -51,26 +53,31 @@ const App = () => {
-
-
-
-
- <>
-
+
+
+
+
+ <>
+
-
+
-
+
-
- >
-
-
-
-
+
+ >
+
+
+
+
+
);
diff --git a/src/core/api/queryClient.ts b/src/core/api/queryClient.ts
new file mode 100644
index 00000000..60801830
--- /dev/null
+++ b/src/core/api/queryClient.ts
@@ -0,0 +1,35 @@
+import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
+import { QueryClient } from '@tanstack/react-query';
+import type { PersistQueryClientOptions } from '@tanstack/react-query-persist-client';
+
+import { storageKeys } from '$core/constants';
+import { QueryClientStorage } from '$core/storage';
+
+import {
+ GC_TIME,
+ STALE_TIME,
+ THIRTY_DAYS,
+ retryDelay,
+} from './utils/queryClient.utils';
+
+const asyncStoragePersister = createAsyncStoragePersister({
+ key: storageKeys.queryStorage.id,
+ storage: QueryClientStorage,
+});
+
+export const persistOptions: Omit = {
+ persister: asyncStoragePersister,
+ buster: 'v1',
+ maxAge: THIRTY_DAYS,
+};
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retryDelay,
+ staleTime: STALE_TIME, // 5 minutes
+ gcTime: GC_TIME, // 24 hours
+ refetchOnWindowFocus: false,
+ },
+ },
+});
diff --git a/src/core/api/request.ts b/src/core/api/request.ts
new file mode 100644
index 00000000..eadbaa74
--- /dev/null
+++ b/src/core/api/request.ts
@@ -0,0 +1,39 @@
+import { GraphQLClient } from 'graphql-request';
+import type {
+ GraphQLClientRequestHeaders,
+ Variables,
+} from 'graphql-request/build/esm/types';
+import i18next from 'i18next';
+import memoize from 'lodash/memoize';
+
+import { config } from '$core/constants';
+import { getCurrentLocale } from '$core/i18n/utils/getCurrentLocale';
+
+import { getAppIdentifier } from './utils/request.utils';
+
+const getClientEndpoint = (env: string) =>
+ `${env}?lang=${getCurrentLocale(i18next)}`;
+
+const getQueryClient = memoize(
+ (env: string) => new GraphQLClient(getClientEndpoint(env)),
+ (...args) => args.join('_'),
+);
+
+let client: GraphQLClient | undefined;
+
+export const request =
+ (
+ query: string,
+ variables?: TVariables,
+ options?: GraphQLClientRequestHeaders,
+ ): (() => Promise) =>
+ async () => {
+ client = getQueryClient(config.apiURL);
+ client.setEndpoint(getClientEndpoint(config.apiURL));
+
+ if (options) client.setHeaders(options);
+ client.setHeader('app-id', getAppIdentifier());
+ client.setHeader('app-version', config.version);
+
+ return client.request(query, variables);
+ };
diff --git a/src/core/api/utils/queryClient.utils.ts b/src/core/api/utils/queryClient.utils.ts
new file mode 100644
index 00000000..8af65d28
--- /dev/null
+++ b/src/core/api/utils/queryClient.utils.ts
@@ -0,0 +1,14 @@
+/* eslint-disable @typescript-eslint/no-magic-numbers */
+
+const ONE_SECOND = 1_000;
+const MAX_RETRY_DELAY = 30_000;
+const FIVE_MINUTES = 1000 * 60 * 5;
+const TWENTY_FOUR_HOURS = 1000 * 60 * 60 * 24;
+
+export const THIRTY_DAYS = 30 * TWENTY_FOUR_HOURS;
+
+export const retryDelay = (attemptIndex: number) =>
+ Math.min(ONE_SECOND * 2 ** attemptIndex, MAX_RETRY_DELAY);
+
+export const STALE_TIME = FIVE_MINUTES;
+export const GC_TIME = TWENTY_FOUR_HOURS;
diff --git a/src/core/api/utils/request.utils.ts b/src/core/api/utils/request.utils.ts
new file mode 100644
index 00000000..a3f0cb59
--- /dev/null
+++ b/src/core/api/utils/request.utils.ts
@@ -0,0 +1,8 @@
+import { Platform } from 'react-native';
+
+import { config } from '$core/constants';
+
+export function getAppIdentifier() {
+ // com.tsyirvo.rnstarter/2.0.0(777)_ios
+ return `${config.bundleId}/${config.version}${config.buildNumber ? `(${config.buildNumber})` : ''}_${Platform.OS}`;
+}
diff --git a/src/core/constants/config.ts b/src/core/constants/config.ts
index 3982d880..e21e05dc 100644
--- a/src/core/constants/config.ts
+++ b/src/core/constants/config.ts
@@ -1,5 +1,7 @@
import Constants from 'expo-constants';
+import { IS_IOS } from './platform';
+
//@ts-expect-error // We know we're passing the correct environment variables to `extra` in `app.config.ts`
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const Env: typeof import('../../../env.js').ClientEnv =
@@ -7,9 +9,14 @@ const Env: typeof import('../../../env.js').ClientEnv =
const env = Env.APP_ENV;
const version = Env.VERSION;
-const buildNumber = Constants.expoConfig?.ios?.buildNumber;
+const iosbuildNumber = Constants.expoConfig?.ios?.buildNumber ?? '';
+const androidVersionCode = Constants.expoConfig?.android?.versionCode
+ ? Constants.expoConfig.android.versionCode.toString()
+ : '';
const runtimeVersion = Constants.expoConfig?.runtimeVersion;
+const iosBundleIdentifier = Constants.expoConfig?.ios?.bundleIdentifier ?? '';
const androidPackageName = Constants.expoConfig?.android?.package ?? '';
+const apiURL = Env.API_URL;
const sentryDsn = Env.SENTRY_DSN;
const mixpanelToken = Env.MIXPANEL_TOKEN;
const flagsmithKey = Env.FLAGSMITH_KEY;
@@ -17,14 +24,15 @@ const flagsmithKey = Env.FLAGSMITH_KEY;
export const config = {
defaultLocale: 'en',
supportedLocales: ['en', 'fr'],
- // Config
+ // App config
env,
isDebug: env === 'development',
version,
- buildNumber,
+ buildNumber: IS_IOS ? iosbuildNumber : androidVersionCode,
runtimeVersion,
- androidPackageName,
- // SDK
+ bundleId: IS_IOS ? iosBundleIdentifier : androidPackageName,
+ apiURL,
+ // SDKs
sentryDsn,
mixpanelToken,
flagsmithKey,
diff --git a/src/core/constants/storage.ts b/src/core/constants/storage.ts
index 4006e057..f367a975 100644
--- a/src/core/constants/storage.ts
+++ b/src/core/constants/storage.ts
@@ -1,5 +1,9 @@
export const storageKeys = {
appStorage: {
+ id: 'app-storage',
locale: 'app.locale',
},
+ queryStorage: {
+ id: 'query-storage',
+ },
};
diff --git a/src/core/i18n/utils/getCurrentLocale.ts b/src/core/i18n/utils/getCurrentLocale.ts
new file mode 100644
index 00000000..898708de
--- /dev/null
+++ b/src/core/i18n/utils/getCurrentLocale.ts
@@ -0,0 +1,16 @@
+import type { i18n } from 'i18next';
+
+import { config } from '$core/constants';
+
+export const getCurrentLocale = (i18n: i18n) => {
+ const languageCode = i18n.language;
+ const [primaryCode] = languageCode.split('-');
+
+ if (i18n.hasResourceBundle(languageCode, 'common')) {
+ return languageCode;
+ } else if (primaryCode && i18n.hasResourceBundle(primaryCode, 'common')) {
+ return primaryCode;
+ }
+
+ return config.defaultLocale;
+};
diff --git a/src/core/storage/appStorage.ts b/src/core/storage/appStorage.ts
index 8c9831f4..7d46d323 100644
--- a/src/core/storage/appStorage.ts
+++ b/src/core/storage/appStorage.ts
@@ -1,5 +1,7 @@
import { MMKV } from 'react-native-mmkv';
+import { storageKeys } from '$core/constants';
+
export const AppStorage = new MMKV({
- id: 'app-storage',
+ id: storageKeys.appStorage.id,
});
diff --git a/src/core/storage/index.ts b/src/core/storage/index.ts
index ccb820bd..758189d1 100644
--- a/src/core/storage/index.ts
+++ b/src/core/storage/index.ts
@@ -1 +1,2 @@
export { AppStorage } from './appStorage';
+export { QueryClientStorage } from './queryClientStorage';
diff --git a/src/core/storage/queryClientStorage.ts b/src/core/storage/queryClientStorage.ts
new file mode 100644
index 00000000..c9cfd274
--- /dev/null
+++ b/src/core/storage/queryClientStorage.ts
@@ -0,0 +1,36 @@
+import type { MMKVConfiguration } from 'react-native-mmkv';
+import { MMKV } from 'react-native-mmkv';
+
+class StorageClass {
+ private _storage: MMKV;
+
+ constructor(config?: MMKVConfiguration) {
+ this._storage = new MMKV(config);
+ }
+
+ setItem = async (key: string, value: string) => {
+ this._storage.set(key, value);
+
+ return Promise.resolve();
+ };
+
+ getItem = async (key: string) => {
+ const value = this._storage.getString(key);
+
+ if (!value) return Promise.resolve(null);
+
+ return Promise.resolve(value);
+ };
+
+ removeItem = async (key: string) => {
+ this._storage.delete(key);
+
+ return Promise.resolve();
+ };
+}
+
+export const REACT_QUERY_CACHE_KEY = 'REACT_QUERY_CACHE_KEY';
+
+export const QueryClientStorage = new StorageClass({
+ id: REACT_QUERY_CACHE_KEY,
+});
diff --git a/src/features/blogPost/BlogPost.tsx b/src/features/blogPost/BlogPost.tsx
new file mode 100644
index 00000000..9e3f740f
--- /dev/null
+++ b/src/features/blogPost/BlogPost.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import { graphql } from '$gql/generated';
+import { useGetPostQuery } from '$gql/generated/hooks';
+import { Loader } from '$shared/uiKit/Loader';
+import { Text } from '$shared/uiKit/primitives';
+
+import { BlogPostUser } from './components/BlogPostUser';
+
+export const BlogPost = () => {
+ const { data, isLoading } = useGetPostQuery();
+
+ if (isLoading) return ;
+
+ return (
+ <>
+ Blog post fetch with GraphQL
+
+ {data?.post?.title}
+
+
+ >
+ );
+};
+
+BlogPost.query = graphql(`
+ query getPost {
+ post(id: 1) {
+ id
+ title
+ user {
+ ...UserItem
+ }
+ }
+ }
+`);
diff --git a/src/features/blogPost/components/BlogPostUser.tsx b/src/features/blogPost/components/BlogPostUser.tsx
new file mode 100644
index 00000000..108d4edb
--- /dev/null
+++ b/src/features/blogPost/components/BlogPostUser.tsx
@@ -0,0 +1,20 @@
+import { graphql } from '$gql/generated';
+import type { UserItemFragment } from '$gql/generated/hooks';
+import { Box, Text } from '$shared/uiKit/primitives';
+
+export const BlogPostUser = ({ user }: { user?: UserItemFragment | null }) => {
+ return (
+
+ {user?.username}
+
+ );
+};
+
+BlogPostUser.fragments = {
+ user: graphql(`
+ fragment UserItem on User {
+ id
+ username
+ }
+ `),
+};
diff --git a/src/features/blogPost/index.ts b/src/features/blogPost/index.ts
new file mode 100644
index 00000000..ef0c7f38
--- /dev/null
+++ b/src/features/blogPost/index.ts
@@ -0,0 +1 @@
+export { BlogPost } from './BlogPost';
diff --git a/src/gql/generated/fragment-masking.ts b/src/gql/generated/fragment-masking.ts
new file mode 100644
index 00000000..c9fb944f
--- /dev/null
+++ b/src/gql/generated/fragment-masking.ts
@@ -0,0 +1,87 @@
+/* eslint-disable */
+import {
+ ResultOf,
+ DocumentTypeDecoration,
+ TypedDocumentNode,
+} from '@graphql-typed-document-node/core';
+import { FragmentDefinitionNode } from 'graphql';
+import { Incremental } from './graphql';
+
+export type FragmentType<
+ TDocumentType extends DocumentTypeDecoration,
+> =
+ TDocumentType extends DocumentTypeDecoration
+ ? [TType] extends [{ ' $fragmentName'?: infer TKey }]
+ ? TKey extends string
+ ? { ' $fragmentRefs'?: { [key in TKey]: TType } }
+ : never
+ : never
+ : never;
+
+// return non-nullable if `fragmentType` is non-nullable
+export function useFragment(
+ _documentNode: DocumentTypeDecoration,
+ fragmentType: FragmentType>,
+): TType;
+// return nullable if `fragmentType` is nullable
+export function useFragment(
+ _documentNode: DocumentTypeDecoration,
+ fragmentType:
+ | FragmentType>
+ | null
+ | undefined,
+): TType | null | undefined;
+// return array of non-nullable if `fragmentType` is array of non-nullable
+export function useFragment(
+ _documentNode: DocumentTypeDecoration,
+ fragmentType: ReadonlyArray>>,
+): ReadonlyArray;
+// return array of nullable if `fragmentType` is array of nullable
+export function useFragment(
+ _documentNode: DocumentTypeDecoration,
+ fragmentType:
+ | ReadonlyArray>>
+ | null
+ | undefined,
+): ReadonlyArray | null | undefined;
+export function useFragment(
+ _documentNode: DocumentTypeDecoration,
+ fragmentType:
+ | FragmentType>
+ | ReadonlyArray>>
+ | null
+ | undefined,
+): TType | ReadonlyArray | null | undefined {
+ return fragmentType as any;
+}
+
+export function makeFragmentData<
+ F extends DocumentTypeDecoration,
+ FT extends ResultOf,
+>(data: FT, _fragment: F): FragmentType {
+ return data as FragmentType;
+}
+export function isFragmentReady(
+ queryNode: DocumentTypeDecoration,
+ fragmentNode: TypedDocumentNode,
+ data:
+ | FragmentType, any>>
+ | null
+ | undefined,
+): data is FragmentType {
+ const deferredFields = (
+ queryNode as {
+ __meta__?: { deferredFields: Record };
+ }
+ ).__meta__?.deferredFields;
+
+ if (!deferredFields) return true;
+
+ const fragDef = fragmentNode.definitions[0] as
+ | FragmentDefinitionNode
+ | undefined;
+ const fragName = fragDef?.name?.value;
+
+ const fields = (fragName && deferredFields[fragName]) || [];
+ return fields.length > 0 && fields.every((field) => data && field in data);
+}
diff --git a/src/gql/generated/gql.ts b/src/gql/generated/gql.ts
new file mode 100644
index 00000000..52f4a681
--- /dev/null
+++ b/src/gql/generated/gql.ts
@@ -0,0 +1,54 @@
+/* eslint-disable */
+import * as types from './graphql';
+import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
+/**
+ * Map of all GraphQL operations in the project.
+ *
+ * This map has several performance disadvantages:
+ * 1. It is not tree-shakeable, so it will include all operations in the project.
+ * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
+ * 3. It does not support dead code elimination, so it will add unused operations.
+ *
+ * Therefore it is highly recommended to use the babel or swc plugin for production.
+ */
+const documents = {
+ '\n query getPost {\n post(id: 1) {\n id\n title\n user {\n ...UserItem\n }\n }\n }\n':
+ types.GetPostDocument,
+ '\n fragment UserItem on User {\n id\n username\n }\n ':
+ types.UserItemFragmentDoc,
+};
+
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ *
+ *
+ * @example
+ * ```ts
+ * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
+ * ```
+ *
+ * The query argument is unknown!
+ * Please regenerate the types.
+ */
+export function graphql(source: string): unknown;
+
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(
+ source: '\n query getPost {\n post(id: 1) {\n id\n title\n user {\n ...UserItem\n }\n }\n }\n',
+): (typeof documents)['\n query getPost {\n post(id: 1) {\n id\n title\n user {\n ...UserItem\n }\n }\n }\n'];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(
+ source: '\n fragment UserItem on User {\n id\n username\n }\n ',
+): (typeof documents)['\n fragment UserItem on User {\n id\n username\n }\n '];
+
+export function graphql(source: string) {
+ return (documents as any)[source] ?? {};
+}
+
+export type DocumentType> =
+ TDocumentNode extends DocumentNode ? TType : never;
diff --git a/src/gql/generated/graphql.ts b/src/gql/generated/graphql.ts
new file mode 100644
index 00000000..bca5949a
--- /dev/null
+++ b/src/gql/generated/graphql.ts
@@ -0,0 +1,600 @@
+/* eslint-disable */
+import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+export type Maybe = T | null;
+export type InputMaybe = Maybe;
+export type Exact = {
+ [K in keyof T]: T[K];
+};
+export type MakeOptional = Omit & {
+ [SubKey in K]?: Maybe;
+};
+export type MakeMaybe = Omit & {
+ [SubKey in K]: Maybe;
+};
+export type MakeEmpty<
+ T extends { [key: string]: unknown },
+ K extends keyof T,
+> = { [_ in K]?: never };
+export type Incremental =
+ | T
+ | {
+ [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never;
+ };
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+ ID: { input: string; output: string };
+ String: { input: string; output: string };
+ Boolean: { input: boolean; output: boolean };
+ Int: { input: number; output: number };
+ Float: { input: number; output: number };
+};
+
+export type Address = {
+ __typename?: 'Address';
+ city?: Maybe;
+ geo?: Maybe;
+ street?: Maybe;
+ suite?: Maybe;
+ zipcode?: Maybe;
+};
+
+export type AddressInput = {
+ city?: InputMaybe;
+ geo?: InputMaybe;
+ street?: InputMaybe;
+ suite?: InputMaybe;
+ zipcode?: InputMaybe;
+};
+
+export type Album = {
+ __typename?: 'Album';
+ id?: Maybe;
+ photos?: Maybe;
+ title?: Maybe;
+ user?: Maybe;
+};
+
+export type AlbumPhotosArgs = {
+ options?: InputMaybe;
+};
+
+export type AlbumsPage = {
+ __typename?: 'AlbumsPage';
+ data?: Maybe>>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type Comment = {
+ __typename?: 'Comment';
+ body?: Maybe;
+ email?: Maybe;
+ id?: Maybe;
+ name?: Maybe;
+ post?: Maybe;
+};
+
+export type CommentsPage = {
+ __typename?: 'CommentsPage';
+ data?: Maybe>>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type Company = {
+ __typename?: 'Company';
+ bs?: Maybe;
+ catchPhrase?: Maybe;
+ name?: Maybe;
+};
+
+export type CompanyInput = {
+ bs?: InputMaybe;
+ catchPhrase?: InputMaybe;
+ name?: InputMaybe;
+};
+
+export type CreateAlbumInput = {
+ title: Scalars['String']['input'];
+ userId: Scalars['ID']['input'];
+};
+
+export type CreateCommentInput = {
+ body: Scalars['String']['input'];
+ email: Scalars['String']['input'];
+ name: Scalars['String']['input'];
+};
+
+export type CreatePhotoInput = {
+ thumbnailUrl: Scalars['String']['input'];
+ title: Scalars['String']['input'];
+ url: Scalars['String']['input'];
+};
+
+export type CreatePostInput = {
+ body: Scalars['String']['input'];
+ title: Scalars['String']['input'];
+};
+
+export type CreateTodoInput = {
+ completed: Scalars['Boolean']['input'];
+ title: Scalars['String']['input'];
+};
+
+export type CreateUserInput = {
+ address?: InputMaybe;
+ company?: InputMaybe;
+ email: Scalars['String']['input'];
+ name: Scalars['String']['input'];
+ phone?: InputMaybe;
+ username: Scalars['String']['input'];
+ website?: InputMaybe;
+};
+
+export type Geo = {
+ __typename?: 'Geo';
+ lat?: Maybe;
+ lng?: Maybe;
+};
+
+export type GeoInput = {
+ lat?: InputMaybe;
+ lng?: InputMaybe;
+};
+
+export type Mutation = {
+ __typename?: 'Mutation';
+ _?: Maybe;
+ createAlbum?: Maybe;
+ createComment?: Maybe;
+ createPhoto?: Maybe;
+ createPost?: Maybe;
+ createTodo?: Maybe;
+ createUser?: Maybe;
+ deleteAlbum?: Maybe;
+ deleteComment?: Maybe;
+ deletePhoto?: Maybe;
+ deletePost?: Maybe;
+ deleteTodo?: Maybe;
+ deleteUser?: Maybe;
+ updateAlbum?: Maybe;
+ updateComment?: Maybe;
+ updatePhoto?: Maybe;
+ updatePost?: Maybe;
+ updateTodo?: Maybe;
+ updateUser?: Maybe;
+};
+
+export type MutationCreateAlbumArgs = {
+ input: CreateAlbumInput;
+};
+
+export type MutationCreateCommentArgs = {
+ input: CreateCommentInput;
+};
+
+export type MutationCreatePhotoArgs = {
+ input: CreatePhotoInput;
+};
+
+export type MutationCreatePostArgs = {
+ input: CreatePostInput;
+};
+
+export type MutationCreateTodoArgs = {
+ input: CreateTodoInput;
+};
+
+export type MutationCreateUserArgs = {
+ input: CreateUserInput;
+};
+
+export type MutationDeleteAlbumArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type MutationDeleteCommentArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type MutationDeletePhotoArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type MutationDeletePostArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type MutationDeleteTodoArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type MutationDeleteUserArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type MutationUpdateAlbumArgs = {
+ id: Scalars['ID']['input'];
+ input: UpdateAlbumInput;
+};
+
+export type MutationUpdateCommentArgs = {
+ id: Scalars['ID']['input'];
+ input: UpdateCommentInput;
+};
+
+export type MutationUpdatePhotoArgs = {
+ id: Scalars['ID']['input'];
+ input: UpdatePhotoInput;
+};
+
+export type MutationUpdatePostArgs = {
+ id: Scalars['ID']['input'];
+ input: UpdatePostInput;
+};
+
+export type MutationUpdateTodoArgs = {
+ id: Scalars['ID']['input'];
+ input: UpdateTodoInput;
+};
+
+export type MutationUpdateUserArgs = {
+ id: Scalars['ID']['input'];
+ input: UpdateUserInput;
+};
+
+export enum OperatorKindEnum {
+ Gte = 'GTE',
+ Like = 'LIKE',
+ Lte = 'LTE',
+ Ne = 'NE',
+}
+
+export type OperatorOptions = {
+ field?: InputMaybe;
+ kind?: InputMaybe;
+ value?: InputMaybe;
+};
+
+export type PageLimitPair = {
+ __typename?: 'PageLimitPair';
+ limit?: Maybe;
+ page?: Maybe;
+};
+
+export type PageMetadata = {
+ __typename?: 'PageMetadata';
+ totalCount?: Maybe;
+};
+
+export type PageQueryOptions = {
+ operators?: InputMaybe>>;
+ paginate?: InputMaybe;
+ search?: InputMaybe;
+ slice?: InputMaybe;
+ sort?: InputMaybe>>;
+};
+
+export type PaginateOptions = {
+ limit?: InputMaybe;
+ page?: InputMaybe;
+};
+
+export type PaginationLinks = {
+ __typename?: 'PaginationLinks';
+ first?: Maybe;
+ last?: Maybe;
+ next?: Maybe;
+ prev?: Maybe;
+};
+
+export type Photo = {
+ __typename?: 'Photo';
+ album?: Maybe;
+ id?: Maybe;
+ thumbnailUrl?: Maybe;
+ title?: Maybe;
+ url?: Maybe;
+};
+
+export type PhotosPage = {
+ __typename?: 'PhotosPage';
+ data?: Maybe>>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type Post = {
+ __typename?: 'Post';
+ body?: Maybe;
+ comments?: Maybe;
+ id?: Maybe;
+ title?: Maybe;
+ user?: Maybe;
+};
+
+export type PostCommentsArgs = {
+ options?: InputMaybe;
+};
+
+export type PostsPage = {
+ __typename?: 'PostsPage';
+ data?: Maybe>>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type Query = {
+ __typename?: 'Query';
+ _?: Maybe;
+ album?: Maybe;
+ albums?: Maybe;
+ comment?: Maybe;
+ comments?: Maybe;
+ photo?: Maybe;
+ photos?: Maybe;
+ post?: Maybe;
+ posts?: Maybe;
+ todo?: Maybe;
+ todos?: Maybe;
+ user?: Maybe;
+ users?: Maybe;
+};
+
+export type QueryAlbumArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type QueryAlbumsArgs = {
+ options?: InputMaybe;
+};
+
+export type QueryCommentArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type QueryCommentsArgs = {
+ options?: InputMaybe;
+};
+
+export type QueryPhotoArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type QueryPhotosArgs = {
+ options?: InputMaybe;
+};
+
+export type QueryPostArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type QueryPostsArgs = {
+ options?: InputMaybe;
+};
+
+export type QueryTodoArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type QueryTodosArgs = {
+ options?: InputMaybe;
+};
+
+export type QueryUserArgs = {
+ id: Scalars['ID']['input'];
+};
+
+export type QueryUsersArgs = {
+ options?: InputMaybe;
+};
+
+export type SearchOptions = {
+ q?: InputMaybe;
+};
+
+export type SliceOptions = {
+ end?: InputMaybe;
+ limit?: InputMaybe;
+ start?: InputMaybe;
+};
+
+export type SortOptions = {
+ field?: InputMaybe;
+ order?: InputMaybe;
+};
+
+export enum SortOrderEnum {
+ Asc = 'ASC',
+ Desc = 'DESC',
+}
+
+export type Todo = {
+ __typename?: 'Todo';
+ completed?: Maybe;
+ id?: Maybe;
+ title?: Maybe;
+ user?: Maybe;
+};
+
+export type TodosPage = {
+ __typename?: 'TodosPage';
+ data?: Maybe>>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type UpdateAlbumInput = {
+ title?: InputMaybe;
+ userId?: InputMaybe;
+};
+
+export type UpdateCommentInput = {
+ body?: InputMaybe;
+ email?: InputMaybe;
+ name?: InputMaybe;
+};
+
+export type UpdatePhotoInput = {
+ thumbnailUrl?: InputMaybe;
+ title?: InputMaybe;
+ url?: InputMaybe;
+};
+
+export type UpdatePostInput = {
+ body?: InputMaybe;
+ title?: InputMaybe;
+};
+
+export type UpdateTodoInput = {
+ completed?: InputMaybe;
+ title?: InputMaybe;
+};
+
+export type UpdateUserInput = {
+ address?: InputMaybe;
+ company?: InputMaybe;
+ email?: InputMaybe;
+ name?: InputMaybe;
+ phone?: InputMaybe;
+ username?: InputMaybe;
+ website?: InputMaybe;
+};
+
+export type User = {
+ __typename?: 'User';
+ address?: Maybe;
+ albums?: Maybe;
+ company?: Maybe;
+ email?: Maybe;
+ id?: Maybe;
+ name?: Maybe;
+ phone?: Maybe;
+ posts?: Maybe;
+ todos?: Maybe;
+ username?: Maybe;
+ website?: Maybe;
+};
+
+export type UserAlbumsArgs = {
+ options?: InputMaybe;
+};
+
+export type UserPostsArgs = {
+ options?: InputMaybe;
+};
+
+export type UserTodosArgs = {
+ options?: InputMaybe;
+};
+
+export type UsersPage = {
+ __typename?: 'UsersPage';
+ data?: Maybe>>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type GetPostQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GetPostQuery = {
+ __typename?: 'Query';
+ post?: {
+ __typename?: 'Post';
+ id?: string | null;
+ title?: string | null;
+ user?:
+ | ({ __typename?: 'User' } & {
+ ' $fragmentRefs'?: { UserItemFragment: UserItemFragment };
+ })
+ | null;
+ } | null;
+};
+
+export type UserItemFragment = {
+ __typename?: 'User';
+ id?: string | null;
+ username?: string | null;
+} & { ' $fragmentName'?: 'UserItemFragment' };
+
+export const UserItemFragmentDoc = {
+ kind: 'Document',
+ definitions: [
+ {
+ kind: 'FragmentDefinition',
+ name: { kind: 'Name', value: 'UserItem' },
+ typeCondition: {
+ kind: 'NamedType',
+ name: { kind: 'Name', value: 'User' },
+ },
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+ { kind: 'Field', name: { kind: 'Name', value: 'username' } },
+ ],
+ },
+ },
+ ],
+} as unknown as DocumentNode;
+export const GetPostDocument = {
+ kind: 'Document',
+ definitions: [
+ {
+ kind: 'OperationDefinition',
+ operation: 'query',
+ name: { kind: 'Name', value: 'getPost' },
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [
+ {
+ kind: 'Field',
+ name: { kind: 'Name', value: 'post' },
+ arguments: [
+ {
+ kind: 'Argument',
+ name: { kind: 'Name', value: 'id' },
+ value: { kind: 'IntValue', value: '1' },
+ },
+ ],
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+ { kind: 'Field', name: { kind: 'Name', value: 'title' } },
+ {
+ kind: 'Field',
+ name: { kind: 'Name', value: 'user' },
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [
+ {
+ kind: 'FragmentSpread',
+ name: { kind: 'Name', value: 'UserItem' },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ kind: 'FragmentDefinition',
+ name: { kind: 'Name', value: 'UserItem' },
+ typeCondition: {
+ kind: 'NamedType',
+ name: { kind: 'Name', value: 'User' },
+ },
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+ { kind: 'Field', name: { kind: 'Name', value: 'username' } },
+ ],
+ },
+ },
+ ],
+} as unknown as DocumentNode;
diff --git a/src/gql/generated/hooks.ts b/src/gql/generated/hooks.ts
new file mode 100644
index 00000000..77ff0431
--- /dev/null
+++ b/src/gql/generated/hooks.ts
@@ -0,0 +1,565 @@
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
+
+import { request } from '../../core/api/request';
+
+export type Maybe = T | null;
+export type InputMaybe = Maybe;
+export type Exact> = { [K in keyof T]: T[K] };
+export type MakeOptional = Omit & {
+ [SubKey in K]?: Maybe;
+};
+export type MakeMaybe = Omit & {
+ [SubKey in K]: Maybe;
+};
+export type MakeEmpty, K extends keyof T> = {
+ [_ in K]?: never;
+};
+export type Incremental =
+ | T
+ | {
+ [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never;
+ };
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+ ID: { input: string; output: string };
+ String: { input: string; output: string };
+ Boolean: { input: boolean; output: boolean };
+ Int: { input: number; output: number };
+ Float: { input: number; output: number };
+};
+
+export type Address = {
+ __typename?: 'Address';
+ city?: Maybe;
+ geo?: Maybe;
+ street?: Maybe;
+ suite?: Maybe;
+ zipcode?: Maybe;
+};
+
+export type AddressInput = {
+ city?: InputMaybe;
+ geo?: InputMaybe;
+ street?: InputMaybe;
+ suite?: InputMaybe;
+ zipcode?: InputMaybe;
+};
+
+export type Album = {
+ __typename?: 'Album';
+ id?: Maybe;
+ photos?: Maybe;
+ title?: Maybe;
+ user?: Maybe;
+};
+
+export type AlbumPhotosArgs = {
+ options?: InputMaybe;
+};
+
+export type AlbumsPage = {
+ __typename?: 'AlbumsPage';
+ data?: Maybe[]>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type Comment = {
+ __typename?: 'Comment';
+ body?: Maybe;
+ email?: Maybe;
+ id?: Maybe;
+ name?: Maybe;
+ post?: Maybe;
+};
+
+export type CommentsPage = {
+ __typename?: 'CommentsPage';
+ data?: Maybe[]>;
+ links?: Maybe;
+ meta?: Maybe;
+};
+
+export type Company = {
+ __typename?: 'Company';
+ bs?: Maybe;
+ catchPhrase?: Maybe;
+ name?: Maybe;
+};
+
+export type CompanyInput = {
+ bs?: InputMaybe;
+ catchPhrase?: InputMaybe;
+ name?: InputMaybe;
+};
+
+export type CreateAlbumInput = {
+ title: Scalars['String']['input'];
+ userId: Scalars['ID']['input'];
+};
+
+export type CreateCommentInput = {
+ body: Scalars['String']['input'];
+ email: Scalars['String']['input'];
+ name: Scalars['String']['input'];
+};
+
+export type CreatePhotoInput = {
+ thumbnailUrl: Scalars['String']['input'];
+ title: Scalars['String']['input'];
+ url: Scalars['String']['input'];
+};
+
+export type CreatePostInput = {
+ body: Scalars['String']['input'];
+ title: Scalars['String']['input'];
+};
+
+export type CreateTodoInput = {
+ completed: Scalars['Boolean']['input'];
+ title: Scalars['String']['input'];
+};
+
+export type CreateUserInput = {
+ address?: InputMaybe