Repository: supabase-community/supabase-graphql-example Branch: main Commit: 504560d3124a Files: 71 Total size: 288.9 KB Directory structure: gitextract_uc_3wbbe/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .graphqlrc.yml ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── README.md ├── app/ │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── gql/ │ │ └── .gitignore │ ├── lib/ │ │ ├── active-link.tsx │ │ ├── comment-item.tsx │ │ ├── container.tsx │ │ ├── feed-item.tsx │ │ ├── footer.tsx │ │ ├── icons.tsx │ │ ├── loading.tsx │ │ ├── main-section.tsx │ │ ├── navigation.tsx │ │ ├── noop-uuid.ts │ │ ├── supabase.tsx │ │ ├── time-ago.ts │ │ ├── urql.tsx │ │ └── use-paginated-query.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages/ │ │ ├── _app.tsx │ │ ├── about.tsx │ │ ├── account.tsx │ │ ├── api/ │ │ │ └── graphiql.ts │ │ ├── comments.tsx │ │ ├── index.tsx │ │ ├── item/ │ │ │ └── [postId].tsx │ │ ├── login.tsx │ │ ├── logout.tsx │ │ ├── newest.tsx │ │ ├── profile/ │ │ │ └── [profileId].tsx │ │ └── submit.tsx │ ├── postcss.config.js │ ├── styles/ │ │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json ├── data/ │ ├── db/ │ │ ├── backup.sql │ │ ├── row_level_security_polices.csv │ │ └── schema.sql │ ├── seed/ │ │ ├── blog.xml │ │ ├── blog_posts.csv │ │ └── comments.csv │ └── supabase/ │ ├── 00-initial-schema.sql │ ├── 01-auth-schema.sql │ ├── 02-storage-schema.sql │ ├── 03-post-setup.sql │ ├── 04-public-profiles.sql │ ├── 05-setup-total-counts.sql │ ├── 05-setup-user-profile-trigger.sql │ ├── 06-update-post-vote-counts.sql │ ├── 07-add-post-title-url-constraints.sql │ ├── 08-update-post-cascahe-delete-constraints.sql │ ├── 09-update-all-gravatars.sql │ └── rls-policies.md ├── graphql/ │ ├── queries/ │ │ ├── feed.graphql │ │ ├── hasProfileVotes.graphql │ │ └── rankedFeed.graphql │ └── schema/ │ └── schema.graphql ├── package.json ├── renovate.json └── scripts/ └── fetchGraphQLSchema.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: push: pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: the-guild-org/shared-config/setup@main with: nodeVersion: 16 - run: yarn codegen - run: yarn workspace app run build lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: the-guild-org/shared-config/setup@main with: nodeVersion: 16 - run: yarn workspace app run lint ================================================ FILE: .gitignore ================================================ .idea .DS_Store .env .env.production yarn-error.log node_modules ================================================ FILE: .graphqlrc.yml ================================================ schema: ./graphql/schema/schema.graphql documents: ./app/**/*.{graphql,js,ts,jsx,tsx} extensions: codegen: generates: ./app/gql: preset: gql-tag-operations-preset hooks: afterOneFileWrite: - prettier --write ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "esbenp.prettier-vscode", "GraphQL.vscode-graphql", "bradlc.vscode-tailwindcss" ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" } ================================================ FILE: README.md ================================================ # Supabase GraphQL Example A basic HackerNews-like clone where posts can be submitted with url links and then up and down voted. graphql-hn - Example: [supabase-graphql-example.vercel.app](https://supabase-graphql-example.vercel.app/) - Features: [supabase-graphql-example.vercel.app/about](https://supabase-graphql-example.vercel.app/about) ## Showcase ### Backend - CRUD (Query + Mutation Operations) - Cursor Based Pagination - Authorization / Postgres Row Level Security - [Supabase](https://supabase.com) - Create a backend in less than 2 minutes. Start your project with a Postgres Database, Authentication, instant APIs, Realtime subscriptions and Storage. - [pg_graphql](https://supabase.com/blog/2021/12/03/pg-graphql) - A native [PostgreSQL extension](https://supabase.github.io/pg_graphql/) adding [GraphQL support](https://graphql.org). The extension keeps schema generation, query parsing, and resolvers all neatly contained on your database server requiring no external services. - [Postgres Triggers](https://supabase.com/blog/2021/07/30/supabase-functions-updates) and [Postgres Functions](https://supabase.com/docs/guides/database/functions) - When votes are in, use triggers to invoke a Postgres function that calculates a post score to rank the feed - [Postgres Enumerated Types](https://www.postgresql.org/docs/14/datatype-enum.html) - Enums help defined the direction of a vote: UP or DOWN. ### Frontend - [Next.js](https://nextjs.org) - React Framework - [TypeScript](https://www.typescriptlang.org) - TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. - [graphql-code-generator](https://www.graphql-code-generator.com) - Generate code from your GraphQL schema and operations with a simple CLI - [gql-tag-operations-preset](https://www.graphql-code-generator.com/plugins/gql-tag-operations-preset) - This code gen preset generates typings for your inline gql function usages, without having to manually specify import statements for the documents - [urql](https://formidable.com/open-source/urql/) - A highly customizable and versatile GraphQL client - [Gravatar](https://en.gravatar.com) - Default avatar profile images from Gravatar ### Functionality - Registration - Get a ranked feed of posts - Create Post - Delete Post - Create Comment - Delete Comment - Upvote/Downvote Post - View Profile (Account) - View Profile (Public) - Pagination (Posts, Comments) ## QuickStart ### Setup env vars - `cp app/.env.example app/.env` - Fill in your url and anon key from the Supabase Dashboard: https://app.supabase.io/project/_/settings/api ### Install dependencies, GraphQL codegen, run app ```bash yarn yarn codegen yarn workspace app dev ``` ### Deploy to Vercel Provide the following settings to deploy a production build to Vercel: - BUILD COMMAND: `yarn codegen && yarn workspace app build` - OUTPUT DIRECTORY: `./app/.next` - INSTALL COMMAND: `yarn` - DEVELOPMENT COMMAND: `yarn codegen && yarn workspace app dev --port $PORT` ## Development 1. Fetch latest GraphQL Schema ```bash yarn codegen:fetch ``` 2. Generate Types and Watch for Changes ```bash yarn codegen:watch ``` 3. Run server ```bash yarn workspace app dev ``` ### Synchronize the GraphQL schema Note: You need to call `select graphql.rebuild_schema()` manually to synchronize the GraphQL schema with the SQL schema after altering the SQL schema. #### Manage Schema with dbmate 1. `brew install dbmate` 2. Setup `.env` with `DATABASE_URL` 3. Dump Schema ``` cd data dbmate dump ``` > Note: If `pgdump` fails due to row locks, a workaround is to grant the `postgres` role superuser permissions with `ALTER USER postgres WITH SUPERUSER`. After dumping the schema, you should reset the permissions using `ALTER USER postgres WITH NOSUPERUSER`. You can run these statements in the Superbase Dashboard SQL Editors. ## Schema (Public) - Profile belongs to auth.users - Post - Comment belongs to Post and Profile - Vote belongs to Post (can have a direction of UP/DOWN) - direction enum is "UP" or "DOWN" ### Constraints - Post `url` is unique - Vote is unique per Profile, Post (ie, you cannot vote more than once -- up or down) See: [`./data/db/schema.sql`](./data/db/schema.sql) > Note: The schema includes the entire Supabase schema with auth, storage, functions, etc. ## Seed Data A data file for all Supabase Blog posts from the RSS feed can be found in `./data/seed/blog_posts.csv` and can be loaded. Another file for `comments` is available as well. Note: Assumes a known `profileId` currently. ## GraphQL Schema See: [`./graphql/schema/schema.graphql`](./graphql/schema/schema.graphql) ## Example Query See: [`./graphql/queries/`](./graphql/queries/) Use: `https://mvrfvzcivgabojxddwtk.supabase.co/graphql/v1` > Note: Needs headers ``` Content-Type: application/json apiKey: ``` ## GraphiQL GraphiQL is an in-browser IDE for writing, validating, and testing GraphQL queries. Visit `http://localhost:3000/api/graphiql` for the [Yoga GraphiQL Playground](https://www.graphql-yoga.com/docs/features/graphiql) where you can experiment with queries and mutations. > Note: Needs headers ``` Content-Type: application/json apiKey: ``` > Note: In order for the RLS policies authenticate you, you have to pass an authorization header ([see example](https://github.com/supabase-community/supabase-graphql-example/blob/main/app/lib/urql.tsx#L15)): ``` authorization: Bearer ``` ### Ranked Feed ```gql query { rankedFeed: postCollection(orderBy: [{ voteRank: AscNullsFirst }]) { edges { post: node { id title url upVoteTotal downVoteTotal voteTotal voteDelta score voteRank comments: commentCollection { edges { node { id message profile { id username avatarUrl } } } commentCount: totalCount } } } } } ``` # Row Level Security Matrix (RLS) You can query all policies via: `select * from pg_policies`. See: [Row Level Security Matrix (RLS)](./data/supabase/rls-policies.md) ## Read More - [pg_graphql](https://supabase.github.io/pg_graphql) - [pg_graphql Configuration](https://supabase.github.io/pg_graphql/configuration) ## Troubleshooting 1. `dbmate` can create `schema_migrations` tables in schemas. To make sure they are not included in your GraphQL Schema: ```sql revoke select on table public.schema_migrations from anon, authenticated; ``` 2. To [enable inflection](https://supabase.github.io/pg_graphql/configuration/#inflection) ```sql comment on schema public is e'@graphql({"inflect_names": true})'; ``` 3. Try the heartbeat to see if pg_graphql can access requests ``` select graphql_public.graphql( null, $$ { heartbeat }$$ ) ``` Returns: ```json { "data": { "heartbeat": "2022-07-28T17:07:07.90513" } } ``` 4. Is the `public_graphql` schema not exposed properly? Getting an 406 status or error message like: ``` { "message": "The schema must be one of the following: public, storage" } ``` Then be sure to expose the `graphql_public` in `Settings` > `Project settings` > `API`. > The schema to expose in your API. Tables, views and stored procedures in this schema will get API endpoints. ![image](https://user-images.githubusercontent.com/1051633/181597157-f9a47a5b-bc6a-49d4-b41e-9c1324b5e2a7.png) ================================================ FILE: app/.eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: app/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env.local .env.development.local .env.test.local .env.production.local # vercel .vercel # typescript *.tsbuildinfo ================================================ FILE: app/README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: app/gql/.gitignore ================================================ *.ts *.tsx ================================================ FILE: app/lib/active-link.tsx ================================================ import React from "react"; import { useRouter } from "next/router"; import Link from "next/link"; /** * @source https://nextjs.org/docs/api-reference/next/link * @source https://github.com/vercel/next.js/tree/canary/examples/active-class-name */ export function ActiveLink({ children, activeClassName, ...props }: { children: React.ReactNode; activeClassName: string; href: string; as?: string; }) { const { asPath, isReady } = useRouter(); const child = React.Children.only(children) as React.DetailedReactHTMLElement< any, HTMLElement >; const childClassName = child.props?.className || ""; const [className, setClassName] = React.useState(childClassName); React.useEffect(() => { // Check if the router fields are updated client-side if (isReady) { // Dynamic route will be matched via props.as // Static route will be matched via props.href const linkPathname = new URL(props.as || props.href, location.href) .pathname; // Using URL().pathname to get rid of query and hash const activePathname = new URL(asPath, location.href).pathname; const newClassName = linkPathname === activePathname ? `${childClassName} ${activeClassName}`.trim() : childClassName; if (newClassName !== className) { setClassName(newClassName); } } }, [ asPath, isReady, props.as, props.href, childClassName, activeClassName, setClassName, className, ]); return ( {React.cloneElement(child, { className: className || null, })} ); } ================================================ FILE: app/lib/comment-item.tsx ================================================ import React from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useMutation } from "urql"; import { Auth } from "@supabase/ui"; import { TrashIcon } from "@heroicons/react/outline"; import { CalendarIcon, UserIcon } from "./icons"; import { DocumentType, gql } from "../gql"; import { timeAgo } from "./time-ago"; const CommentItem_CommentFragment = gql(/* GraphQL */ ` fragment CommentItem_CommentFragment on Comment { id message createdAt post { id title } profile { id username avatarUrl } } `); const CommentItem_DeleteCommentFragment = gql(/* GraphQL */ ` mutation CommentItem_DeleteComment($commentId: BigInt!) { deleteFromCommentCollection(atMost: 1, filter: { id: { eq: $commentId } }) { affectedCount } } `); export function CommentItem(props: { comment: DocumentType; }) { const router = useRouter(); const { user } = Auth.useUser(); const [deleteCommentMutation, deleteComment] = useMutation( CommentItem_DeleteCommentFragment ); const createdAt = React.useMemo( () => timeAgo.format(new Date(props.comment.createdAt)), [props.comment.createdAt] ); React.useEffect(() => { if (deleteCommentMutation.data) { router.reload(); } }, [deleteCommentMutation.data]); return (

{props.comment.message} {user?.id && user.id === props.comment.profile?.id ? ( ) : null}

); } ================================================ FILE: app/lib/container.tsx ================================================ import React from "react"; export function Container(props: { children: React.ReactNode }) { return
{props.children}
; } ================================================ FILE: app/lib/feed-item.tsx ================================================ import { Auth } from "@supabase/ui"; import Link from "next/link"; import { useRouter } from "next/router"; import React from "react"; import { useMutation } from "urql"; import { Modal } from "@supabase/ui"; import { DocumentType, gql } from "../gql"; import { CalendarIcon, ChevronDownIcon, ChevronUpIcon, CommentIcon, PointIcon, TrashIcon, UserIcon, } from "./icons"; import { timeAgo } from "./time-ago"; const VoteButtons_PostFragment = gql(/* GraphQL */ ` fragment VoteButtons_PostFragment on Post { id upVoteByViewer: voteCollection( filter: { profileId: { eq: $profileId }, direction: { eq: "UP" } } ) { totalCount } downVoteByViewer: voteCollection( filter: { profileId: { eq: $profileId }, direction: { eq: "DOWN" } } ) { totalCount } } `); const VoteButtons_DeleteVoteMutation = gql(/* GraphQL */ ` mutation VoteButtons_DeleteVoteMutation($postId: BigInt!, $profileId: UUID!) { deleteFromVoteCollection( filter: { postId: { eq: $postId }, profileId: { eq: $profileId } } ) { __typename } } `); const VoteButtons_VoteMutation = gql(/* GraphQL */ ` mutation VoteButtons_VoteMutation( $postId: BigInt! $profileId: UUID! $voteDirection: String! ) { insertIntoVoteCollection( objects: [ { postId: $postId, profileId: $profileId, direction: $voteDirection } ] ) { __typename affectedCount records { id direction } } } `); function VoteButtons(props: { post: DocumentType; }) { const router = useRouter(); const { user } = Auth.useUser(); const [, deleteVote] = useMutation(VoteButtons_DeleteVoteMutation); const [voteMutation, vote] = useMutation(VoteButtons_VoteMutation); React.useEffect(() => { if (voteMutation.data) { router.reload(); } }, [voteMutation.data]); return (
); } const FeedItem_PostFragment = gql(/* GraphQL */ ` fragment FeedItem_PostFragment on Post { id title url voteTotal createdAt commentCollection { totalCount } profile { id username avatarUrl } ...VoteButtons_PostFragment ...DeleteButton_PostFragment } `); export function FeedItem(props: { post: DocumentType; }) { const { user } = Auth.useUser(); const createdAt = React.useMemo( () => timeAgo.format(new Date(props.post.createdAt)), [props.post.createdAt] ); return (

{props.post.title}

{props.post.voteTotal}{" "} {props.post.voteTotal === 1 ? "point" : "points"} {props.post.commentCollection?.totalCount}{" "} {props.post.commentCollection?.totalCount === 1 ? "comment" : "comments"} {props.post.profile?.username} {createdAt} {user?.id && props.post.profile?.id === user?.id ? ( ) : null}
); } const DeleteButton_DeletePostMutation = gql(/* GraphQL */ ` mutation DeleteButton_DeletePostMutation($postId: BigInt!) { deleteFromPostCollection(atMost: 1, filter: { id: { eq: $postId } }) { affectedCount } } `); const DeleteButton_PostFragment = gql(/* GraphQL */ ` fragment DeleteButton_PostFragment on Post { id } `); const DeleteButton = (props: { post: DocumentType; }) => { const router = useRouter(); const [show, setShow] = React.useState(false); const [deletePostMutation, deletePost] = useMutation( DeleteButton_DeletePostMutation ); React.useEffect(() => { if (deletePostMutation.data) { router.push("/"); } }, [deletePostMutation.data, deletePostMutation.error]); return ( <> setShow(false)} onConfirm={() => { deletePost({ postId: props.post.id, }); }} > Deleting your post can not be reverted ); }; ================================================ FILE: app/lib/footer.tsx ================================================ import Image from "next/image"; import Link from "next/link"; export function Footer() { const navigation = { main: [{ name: "🤔 How did we build this app?", href: "/about" }], social: [ { name: "Twitter", href: "https://twitter.com/supabase", icon: (props: any) => ( ), }, { name: "GitHub", href: "https://github.com/supabase-community/supabase-graphql-example", icon: (props: any) => ( ), }, ], }; return ( ); } ================================================ FILE: app/lib/icons.tsx ================================================ import React from "react"; export function CalendarIcon(props: { className?: string }) { return ( ); } export function TrashIcon(props: { className?: string }) { return ( ); } export function CommentIcon(props: { className?: string }) { return ( ); } export function PointIcon(props: { className?: string }) { return ( ); } export function UserIcon(props: { className?: string }) { return ( ); } export function SupabaseIcon(props: { className?: string; height: number }) { return ( ); } export function ChevronUpIcon(props: { className?: string; strokeWidth?: string; }) { return ( ); } export function ChevronDownIcon(props: { className?: string; strokeWidth?: string; }) { return ( ); } ================================================ FILE: app/lib/loading.tsx ================================================ import React from "react"; import { LightningBoltIcon } from "@heroicons/react/solid"; export function Loading() { return (
); } ================================================ FILE: app/lib/main-section.tsx ================================================ import React from "react"; export function MainSection(props: { children: React.ReactNode }) { return (
{props.children}
); } ================================================ FILE: app/lib/navigation.tsx ================================================ import { Auth } from "@supabase/ui"; import Link from "next/link"; import React from "react"; import { ActiveLink } from "./active-link"; import { SupabaseIcon } from "./icons"; export function Navigation() { const user = Auth.useUser(); return (
); } ================================================ FILE: app/lib/noop-uuid.ts ================================================ /** * Noop UUID for GraphQL operations that require an UUID */ export const noopUUID = "00000000-0000-0000-0000-000000000000"; ================================================ FILE: app/lib/supabase.tsx ================================================ import React from "react"; import { createClient, SupabaseClient } from "@supabase/supabase-js"; import { Auth } from "@supabase/ui"; const SupabaseClientContext = React.createContext(null); export function SupabaseProvider(props: { children: React.ReactNode }) { const [client] = React.useState(() => createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? "http://127.0.0.1:6969", process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "noop" ) ); return ( {props.children} ); } export function useSupabaseClient(): SupabaseClient { const client = React.useContext(SupabaseClientContext); if (client === null) { throw new Error( "Supabase client not provided via context.\n" + "Did you forget to wrap your component tree with SupabaseProvider?" ); } return client; } ================================================ FILE: app/lib/time-ago.ts ================================================ import TimeAgo from "javascript-time-ago"; import en from "javascript-time-ago/locale/en.json"; TimeAgo.addDefaultLocale(en); export const timeAgo = new TimeAgo("en-US"); ================================================ FILE: app/lib/urql.tsx ================================================ import React from "react"; import { createClient, Provider } from "urql"; import { useSupabaseClient } from "./supabase"; export function UrqlProvider(props: { children: React.ReactNode }) { const supabaseClient = useSupabaseClient(); function getHeaders(): Record { const headers: Record = { apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, }; const authorization = supabaseClient.auth.session()?.access_token; if (authorization) { headers["authorization"] = `Bearer ${authorization}`; } return headers; } const [client] = React.useState(function createUrqlClient() { return createClient({ url: `${process.env.NEXT_PUBLIC_SUPABASE_URL!}/graphql/v1`, fetchOptions: function createFetchOptions() { return { headers: getHeaders() }; }, }); }); return {props.children}; } ================================================ FILE: app/lib/use-paginated-query.ts ================================================ import React from "react"; import { useQuery, UseQueryArgs, UseQueryResponse } from "urql"; /** * Urql only supports "merge/infinite" pagination by adoptinh the GraphCache (a global normalized cache), * which certainly is an overkill for this demo. * * This hook wraps `useQuery` from urql and adds a light-weight merge previous and current result API. */ export function usePaginatedQuery( args: UseQueryArgs & { /** * Merge the old result with the new result. */ mergeResult: (oldData: Data, newData: Data) => Data; } ): UseQueryResponse { const [query, queryFn] = useQuery(args); const { data, ...rest } = query; const mergeRef = React.useRef({ current: data, last: data }); if ( data && mergeRef.current.current && query.data !== mergeRef.current.last ) { mergeRef.current.current = args.mergeResult(mergeRef.current.current, data); } if (data != null && mergeRef.current.current == null) { mergeRef.current.current = data; } mergeRef.current.last = query.data; return [ { ...rest, data: mergeRef.current.current, }, queryFn, ]; } ================================================ FILE: app/next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: app/next.config.js ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, }; module.exports = nextConfig; ================================================ FILE: app/package.json ================================================ { "name": "app", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@graphql-yoga/render-graphiql": "2.13.12", "@heroicons/react": "1.0.6", "@supabase/supabase-js": "1.35.7", "@supabase/ui": "0.36.5", "autoprefixer": "10.4.12", "graphql": "16.6.0", "javascript-time-ago": "2.5.7", "next": "12.3.1", "postcss": "8.4.17", "react": "17.0.2", "react-dom": "17.0.2", "tailwindcss": "3.1.8", "urql": "2.2.3" }, "devDependencies": { "@types/javascript-time-ago": "2.0.3", "@types/node": "17.0.45", "@types/react": "17.0.50", "eslint": "8.24.0", "eslint-config-next": "12.3.1", "typescript": "4.6.2" } } ================================================ FILE: app/pages/_app.tsx ================================================ import "../styles/globals.css"; import type { AppProps } from "next/app"; import { UrqlProvider } from "../lib/urql"; import { SupabaseProvider } from "../lib/supabase"; import { Navigation } from "../lib/navigation"; import { Footer } from "../lib/footer"; function MyApp({ Component, pageProps }: AppProps) { return (