├── .DS_Store
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── .babelrc
├── .env.sample
├── .gitignore
├── Dockerfile
├── README.md
├── __mocks__
│ └── NextApi.ts
├── __pages__
│ ├── __snapshots__
│ │ └── index.test.tsx.snap
│ ├── api
│ │ ├── graphql.test.ts
│ │ └── ping.test.ts
│ └── index.test.tsx
├── __screenshots__
│ ├── analyze-client.png
│ ├── analyze-server.png
│ ├── jest-coverage-report-cli.png
│ ├── jest-coverage-report-html.png
│ ├── neo4j-browser-00-paste-cypher.png
│ ├── neo4j-browser-01-paste-cypher.png
│ ├── neo4j-browser-02-example-database-from-seed.png
│ ├── neo4j-browser-03-example-database-from-seed-fullscreen-view.png
│ └── nextjs-frontend-default-page.png
├── analytics
│ └── google
│ │ ├── googleAnalytics.test.ts
│ │ └── googleAnalytics.ts
├── apollo
│ ├── client.test.ts
│ ├── client.ts
│ ├── queries
│ │ └── hello.ts
│ ├── resolvers.test.ts
│ ├── resolvers.ts
│ ├── schema.test.ts
│ ├── schema.ts
│ ├── type-defs.test.ts
│ └── type-defs.ts
├── grandstack-demo
│ ├── components
│ │ ├── Dashboard
│ │ │ ├── Dashboard.styles.tsx
│ │ │ └── Dashboard.tsx
│ │ ├── RatingsChart
│ │ │ ├── RatingsChart.test.tsx
│ │ │ └── RatingsChart.tsx
│ │ ├── RecentReviews
│ │ │ ├── RecentReviews.test.tsx
│ │ │ └── RecentReviews.tsx
│ │ ├── Title
│ │ │ ├── Title.test.tsx
│ │ │ ├── Title.tsx
│ │ │ └── __snapshots__
│ │ │ │ └── Title.test.tsx.snap
│ │ └── UserCount
│ │ │ ├── UserCount.styles.tsx
│ │ │ ├── UserCount.test.tsx
│ │ │ ├── UserCount.tsx
│ │ │ └── __snapshots__
│ │ │ └── UserCount.test.tsx.snap
│ └── layout
│ │ ├── Footer
│ │ └── Footer.tsx
│ │ ├── Header
│ │ ├── Header.styles.tsx
│ │ └── Header.tsx
│ │ ├── Layout.styles.tsx
│ │ └── Layout.tsx
├── jest.config.js
├── jest.setup.js
├── neo4j
│ ├── __seed__
│ │ └── db.cypher
│ ├── db.test.ts
│ └── db.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ ├── graphql.ts
│ │ └── ping.ts
│ └── index.tsx
├── public
│ └── img
│ │ └── grandstack.png
├── tsconfig.json
└── types
│ └── Window.d.ts
├── docker-compose-neo4j-v4.x.yml
├── docker-compose.yml
├── neo4j
├── README.md
├── cypher
│ └── cheatsheet.cypher
├── v3.5.x
│ └── Dockerfile
└── v4.x.x
│ └── Dockerfile
└── package.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "autogenerate",
4 | "clsx",
5 | "dockerized",
6 | "dstack",
7 | "fullscreen",
8 | "gran",
9 | "grandstack",
10 | "imdb",
11 | "jwts",
12 | "lcov",
13 | "letmein",
14 | "mkdir",
15 | "rating",
16 | "therobbrennan",
17 | "vercel"
18 | ]
19 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GRANDstack Starter for Next.js with TypeScript
2 |
3 | **What's new in 2021?**
4 |
5 | A lot. 🎉
6 |
7 | On Thursday, January 14th, 2021, Neo4j had a big announcement in their GraphQL Community Call.
8 |
9 | As part of the initial [GRANDstack](https://grandstack.io) ([GraphQL](https://graphql.org), [React](https://reactjs.org), [Apollo](https://www.apollographql.com), [Neo4j Database](https://neo4j.com)) offering, Neo4j labs had created [https://github.com/neo4j-graphql/neo4j-graphql-js](https://github.com/neo4j-graphql/neo4j-graphql-js) - enabling developers to build a GraphQL API layer on top of an already existing Neo4j database and the Cypher query language.
10 |
11 | Neo4j has announced this effort is graduating from a labs project into a fully supported Neo4j product for the GraphQL layer - `@neo4j/graphql`
12 |
13 | This was a huge announcement; and I would encourage you to review the community call and materials below.
14 |
15 | `@neo4j/graphql` is currently in early alpha release and is available NOW.
16 |
17 | Useful resources
18 |
19 | - [Neo4j GraphQL Community Call - January 2021](https://www.youtube.com/watch?v=Og2I2K21MdI)
20 | - [Slide deck for the Community Call](https://docs.google.com/presentation/d/1GfdFUpYguMBUtWrrjSynGrJBkqfzHZYvffYScPltnDE/edit#slide=id.gb270d1e6e4_1_119)
21 | - [@neo4j/graphql on npm](https://www.npmjs.com/package/@neo4j/graphql)
22 | - [https://github.com/neo4j/graphql-tracker-temp](https://github.com/neo4j/graphql-tracker-temp) - This is the temporary repo where you can submit issues, etc.
23 | - [Neo4j on Slack](http://neo4j-users-slack-invite.herokuapp.com)
24 |
25 | ---
26 |
27 | This project is a starter for building a [GRANDstack](https://grandstack.io) ([GraphQL](https://graphql.org), [React](https://reactjs.org), [Apollo](https://www.apollographql.com), [Neo4j Database](https://neo4j.com)) application using [Next.js](https://nextjs.org) and [TypeScript](https://www.typescriptlang.org) instead of the original [create-react-app](https://reactjs.org/docs/create-a-new-react-app.html) example available at [https://github.com/grand-stack/grand-stack-starter](https://github.com/grand-stack/grand-stack-starter).
28 |
29 | ## Getting started
30 |
31 | To run this application as intended, you will need to:
32 |
33 | - Build and run the Dockerized project
34 | - Seed your Neo4j database with sample data
35 |
36 | ### DEMO
37 |
38 | 
39 |
40 | A [demo](https://nextjs-grandstack-starter-typescript.vercel.app/) application has been deployed using a free [Neo4j Sandbox](https://sandbox.neo4j.com) as the backend database at [https://nextjs-grandstack-starter-typescript.vercel.app/]
41 |
42 | Please note that these sandboxes do not last forever - ten (10) days at most with active renewal and upkeep.
43 |
44 | TL:DR If the demo app is broken, that's why. You can run this locally to your heart's content. 🤣
45 |
46 | ### Build and run the Dockerized project
47 |
48 | If you would like to have your [Next.js](https://nextjs.org) application and [Neo4j Database](https://neo4j.com) running in a [Docker](https://www.docker.com) environment, you can quickly build, start, and stop versions of [Neo4j Database](https://neo4j.com) to your heart's content!
49 |
50 | To run this example, all you need to have installed on your system is [Docker](https://www.docker.com) and `npm` installed on your development system.
51 |
52 | If you do not have [Docker](https://www.docker.com) installed on your development system, go to freely available [Docker Desktop](https://www.docker.com/products/docker-desktop) and get that installed and configured on your development machine.
53 |
54 | If you already have `npm` and [Docker](https://www.docker.com) installed on your development system, run:
55 |
56 | ```sh
57 | # Run the project using Neo4j v3.5.x
58 | $ npm run dev
59 |
60 | # ALTERNATIVE: Run the project using Neo4j v4.x.x
61 | $ npm run dev:v4
62 | ```
63 |
64 | You should be able to access the following URLs:
65 |
66 | - [http://localhost:7474/browser/](http://localhost:7474/browser/) - This is the Neo4j Browser application that you can use to explore your Neo4j database - as well as run Cypher commands to seed your database with example data
67 | - [http://localhost:3000](http://localhost:3000) - The frontend for our Next.js application
68 | - [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql) - The GraphIQL explorer for our backend Next.js API which will be a serverless GraphQL function on Vercel
69 | - [http://localhost:3000/api/ping](http://localhost:3000/api/ping) - A sample API route that will be a serverless function on Vercel
70 |
71 | Additionally, the following scripts have been created for managing your Docker environment based on the version of Neo4j you are using.
72 |
73 | Neo4j v3.5.x:
74 |
75 | - `dev` - This starts the Dockerized project with services defined in `docker-compose.yml`
76 | - `dev:clean` - This builds fresh images and containers for the Dockerized project with services defined in `docker-compose.yml`
77 | - `dev:stop` - This stops the Dockerized project with services defined in `docker-compose.yml`
78 |
79 | Neo4j v4.x.x:
80 |
81 | - `dev:v4` - This starts the Dockerized project with services defined in `docker-compose-neo4j-v4.x.yml`
82 | - `dev:v4:clean` - This builds fresh images and containers for the Dockerized project with services defined in `docker-compose-neo4j-v4.x.yml`
83 | - `dev:v4:stop` - This stops the Dockerized project with services defined in `docker-compose-neo4j-v4.x.yml`
84 |
85 | Docker
86 |
87 | - `docker:destroy:global` - **WARNING: This command will delete all Docker images, containers, networks, and volumes for ALL Dockerized applications on your system**
88 |
89 | ### Seed your Neo4j database with sample data
90 |
91 | Once your Dockerized project is running, you can navigate to the [Neo4j Browser](https://neo4j.com/developer/neo4j-browser/) at [http://0.0.0.0:7474/browser/](http://0.0.0.0:7474/browser/).
92 |
93 | Open up `./app/neo4j/__seed__/db.cypher` so you can copy and paste the example Cypher statements into the Cypher window and press `play` to seed your database with example data.
94 |
95 | ## Tests
96 |
97 | This project uses [Jest](https://jestjs.io/) and [Enzyme](https://enzymejs.github.io/enzyme/) for unit and integration tests.
98 |
99 | The following scripts are available at both the top-level project directory as well as the `app` directory:
100 |
101 | - `npm run test` or `npm test` - This runs a single run of the Jest tests for our project.
102 | - `npm run test:ci` - Useful for running Jest tests in a continuous integration (CI) environment
103 | - See [https://jestjs.io/docs/en/cli#--ci](https://jestjs.io/docs/en/cli#--ci) for more details
104 | - `npm run test:coverage` - Generates a code coverage report of the Jest tests
105 |
106 | 
107 |
108 | - `npm run test:coverage:view` - Generates a code coverage report of the Jest tests and automatically launches a web browser on macOS/Linux to interactively see what code is and is not being covered in tests
109 |
110 | 
111 |
112 | - `npm run test:verbose` - This displays verbose output during the execution of the Jest tests
113 |
114 | - `npm run test:watch` - **This is the only test script that runs locally.** It runs Jest in `--watch` mode; running tests related to current code changes and not the entire suite
115 |
116 | ## Bundle analysis
117 |
118 | This project uses the [Next.js + Webpack Bundle Analyzer](https://github.com/vercel/next.js/tree/canary/packages/next-bundle-analyzer) to generate bundle analysis of both client and server bundles by running the `npm run analyze` script:
119 |
120 | 
121 |
122 | 
123 |
124 | ## Static files
125 |
126 | The `public` folder in your [Next.js](https://nextjs.org) app can be used to serve whatever assets you desire (e.g. `app/public/logo.png` would be available at [http://localhost:3000/logo.png](http://localhost:3000/logo.png))
127 |
128 | ## Back-end API
129 |
130 | One benefit of developing with [Next.js](https://nextjs.org) and deploying to [Vercel](https://vercel.com/) is that any files contained within your `pages/api` folder will be separate serverless functions.
131 |
132 | In our case, we have two serverless functions that we are exposing:
133 |
134 | - `graphql` - This is the GraphQL endpoint our application and our GraphIQL IDE will use
135 | - `ping` - Displays a simple message to verify our serverless functions are online
136 |
137 | For more details, please refer to [Vercel Serverless Functions](https://vercel.com/docs/serverless-functions/introduction)
138 |
139 | ### GraphQL
140 |
141 | You can explore your GraphQL schema using the GraphIQL IDE - available at [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql)
142 |
143 | ### Ping
144 |
145 | To verify your back-end API is running, you should be able to visit [http://localhost:3000/api/ping](http://localhost:3000/api/ping) and see a response.
146 |
147 | ## Third-party services
148 |
149 | ### Apollo GraphQL
150 |
151 | This project has an example [Apollo GraphQL](https://www.apollographql.com) server up and running at `/api/graphql`
152 |
153 | If you are running this project locally, you can view the [GraphIQL IDE](http://localhost:3000/api/graphql) at [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql)
154 |
155 | ### Google Analytics
156 |
157 | This project uses [Google Analytics](http://analytics.google.com) to track user interactions and evaluate the application's usage.
158 |
159 | ### Neo4j
160 |
161 | The [Neo4j Database](https://neo4j.com) is the cornerstone of our GRANDstack application.
162 |
163 | Once you have defined the environment variables - either in `app/.env` for local development or the appropriate places for your environment - you can have a [Neo4j Database](https://neo4j.com) hosted anywhere you'd like.
164 |
165 | [Neo4j](https://neo4j.com) has three options to get you up and running quickly:
166 |
167 | - [Neo4j Desktop](https://neo4j.com/download/) - FREE - Perfect for exploring [Neo4j](https://neo4j.com) on your local machine
168 | - [Neo4j Sandbox](https://sandbox.neo4j.com) - FREE - This allows you to run short-lived Neo4j projects in the cloud for free. By default, Neo4j sandboxes will terminate after three (3) days; however you do have the ability to optionally extend for another seven (7) days if desired.
169 | - [Neo4j Aura](https://neo4j.com/cloud/aura/) - \$ - This is [Neo4j](https://neo4j.com)'s flagship offering with a developer-friendly graph database as a service.
170 |
171 | All you will need is the bolt URL and the credentials to access your database.
172 |
173 | #### Sample data
174 |
175 | The original [https://github.com/grand-stack/grand-stack-starter](https://github.com/grand-stack/grand-stack-starter) had a helper script to seed your [Neo4j Database](https://neo4j.com) with example data. That same example database is available for you in a series of Cypher commands.
176 |
177 | Once you have connected to your [Neo4j Database](https://neo4j.com) with [Neo4j Browser](https://neo4j.com/developer/neo4j-browser/), simply copy the text as is from `app/neo4j/__seed__/db.cypher` and execute the Cypher commands in a single action:
178 |
179 | 
180 |
181 | 
182 |
183 | You can verify that your [Neo4j Database](https://neo4j.com) has been successfully created:
184 |
185 | 
186 |
187 | 
188 |
189 | ### Vercel
190 |
191 | This project is ready to be configured for deployment to [Vercel](https://vercel.com/), as well as optionally using the [Vercel for GitHub](https://vercel.com/github) integration for automatic deployment.
192 |
193 | #### Environment variables
194 |
195 | We can define the environment variables we would like to use for our `Production,` `Preview,` and `Development` environments. Navigate to your [Vercel](https://vercel.com/) project settings and define the following environment variables:
196 |
197 | ```sh
198 | GOOGLE_ANALYTICS_TRACKING_ID=UA-156456153-7
199 | NEO4J_URI=bolt://54.167.150.120:32844
200 | NEO4J_USER=neo4j
201 | NEO4J_PASSWORD=
202 |
203 | # Enable encrypted driver connection for Neo4j
204 | #NEO4J_ENCRYPTED=true
205 |
206 | # Specify a specific Neo4j database (v4.x+ only)
207 | #NEO4J_DATABASE=neo4j
208 | ```
209 |
210 | Please refer to `app/.env.sample` to see the latest environment variables that you will need to declare if the above list is incomplete.
211 |
212 | Please see [https://vercel.com/docs/v2/build-step#environment-variables](https://vercel.com/docs/v2/build-step#environment-variables) and/or [https://nextjs.org/docs/basic-features/environment-variables](https://nextjs.org/docs/basic-features/environment-variables) for details on defining environment variables for your application on [Vercel](https://vercel.com/).
213 |
--------------------------------------------------------------------------------
/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["next/babel"]]
3 | }
4 |
--------------------------------------------------------------------------------
/app/.env.sample:
--------------------------------------------------------------------------------
1 | # Use this file to set environment variables with credentials and configuration options
2 | # This file is provided as an example and should be replaced with your own values
3 |
4 | # Analytics
5 | GOOGLE_ANALYTICS_TRACKING_ID=UA-156456153-7
6 |
7 | # Neo4j v3.5.x / v4.x.x [DOCKER]
8 | # NEO4J_URI=bolt://neo4j:7687
9 | # NEO4J_USER=neo4j
10 | # NEO4J_PASSWORD=letmein
11 |
12 | # Neo4j Desktop [LOCAL]
13 | # NEO4J_URI=bolt://localhost:7687
14 | # NEO4J_USER=neo4j
15 | # NEO4J_PASSWORD=password
16 |
17 | # Neo4j Sandbox [2021.01.16 ~3:03pm - Neo4j v3.5.17 - Enterprise]
18 | # Each sandbox is available for three (3) days but can optionally be extended another seven (7) days before it is destroyed.
19 | NEO4J_URI=bolt://54.167.150.120:32844
20 | NEO4J_USER=neo4j
21 | NEO4J_PASSWORD=the-assigned-password
22 |
23 | # Uncomment this line to enable encrypted driver connection for Neo4j
24 | #NEO4J_ENCRYPTED=true
25 |
26 | # Uncomment this line to specify a specific Neo4j database (v4.x+ only)
27 | #NEO4J_DATABASE=neo4j
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .next
3 | .env
4 | coverage/
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.15.1
2 |
3 | ENV PORT 3000
4 |
5 | # Create app directory
6 | RUN mkdir -p /usr/src/app
7 |
8 | # Define the working directory of our Docker container
9 | WORKDIR /usr/src/app
10 |
11 | # Install dependencies
12 | COPY package*.json /usr/src/app/
13 | RUN npm install
14 |
15 | # Copy source files
16 | COPY . /usr/src/app
17 |
18 | # Build app
19 | RUN npm run build
20 |
21 | # Expose our Next.js web application port
22 | EXPOSE 3000
23 |
24 | # Expose our Node.js debug port
25 | EXPOSE 9229
26 |
27 | # Start the app
28 | CMD ["npm", "run", "dev"]
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # REFERENCE: Create a Next.js app from scratch
2 |
3 | ```sh
4 | # Navigate to your desired directory (such as app)
5 | $ cd app
6 |
7 | # Use npm init to create a package.json with typical values for your app
8 | $ npm init
9 |
10 | # Install required Next.js and React dependencies
11 | $ npm install next react react-dom
12 |
13 | # Create a pages directory
14 | $ mkdir pages
15 |
16 | # Create a default page
17 | $ cd pages
18 | $ touch index.js
19 | ```
20 |
21 | Create a simple default page:
22 |
23 | ```jsx
24 | // app/pages/index.js
25 | const DefaultPage = () => {
26 | return
Welcome to Next.js!
27 | }
28 |
29 | export default DefaultPage
30 | ```
31 |
32 | Once you have created the default page, you can now run your app with `$ npm run dev`
33 |
34 | You should be able to view your application at [http://localhost:3000](http://localhost:3000). 🤓
35 |
36 | ## Add TypeScript
37 |
38 | To add [TypeScript](https://www.typescriptlang.org) to your [Next.js](https://nextjs.org) app:
39 |
40 | ```sh
41 | # Navigate to your app directory
42 | $ cd app
43 |
44 | # Install TypeScript dev dependencies
45 | $ npm i -D typescript @types/react @types/node
46 |
47 | # Create an empty tsconfig.json file
48 | $ touch tsconfig.json
49 |
50 | # Run your app, and Next.js will automatically discover and configure TypeScript for you
51 | $ npm run dev
52 | ```
53 |
--------------------------------------------------------------------------------
/app/__mocks__/NextApi.ts:
--------------------------------------------------------------------------------
1 | import httpMocks from "node-mocks-http"
2 |
3 | export const generateMockNextApiRequest = () => {
4 | const req = httpMocks.createRequest()
5 |
6 | // We need to add an env property to our request for it to be a valid NextApiRequest type
7 | req.env = { test: "test" }
8 |
9 | return req
10 | }
11 |
12 | export const generateMockNextApiResponse = () => {
13 | const res = httpMocks.createResponse()
14 |
15 | // Mock our response status function so we can verify what HTTP status code it has been called with
16 | res.status = jest.fn(function () {
17 | return this
18 | })
19 |
20 | // Mock our JSON response function so we can see what data has been returned from our API
21 | res.json = jest.fn()
22 |
23 | return res
24 | }
25 |
--------------------------------------------------------------------------------
/app/__pages__/__snapshots__/index.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`The default page should render 1`] = `
4 |
7 |
10 |
13 |
21 |
24 | Next.js GRANDstack Starter with @neo4j/graphql ^1.0.0-alpha.2 and TypeScript
25 |
26 |
27 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |
47 | Loading
48 |
49 |
50 |
51 |
54 |
57 |
60 | Total Users
61 |
62 |
65 | Loading...
66 |
67 |
70 | users found
71 |
72 |
73 |
74 |
77 |
80 |
81 | Loading
82 |
83 |
84 |
85 |
86 |
106 |
107 |
108 |
109 | `;
110 |
--------------------------------------------------------------------------------
/app/__pages__/api/graphql.test.ts:
--------------------------------------------------------------------------------
1 | // Special thanks to https://medium.com/@jdeflaux/graphql-integration-tests-with-apollo-server-testing-jest-mongodb-and-nock-af5a82e95954 for inspiration
2 | import { apolloServer } from "../../pages/api/graphql"
3 | import { createTestClient } from "apollo-server-testing"
4 |
5 | // Types
6 | import { HelloQuery, HelloResponse } from "../../apollo/queries/hello"
7 |
8 | describe("Apollo Server", () => {
9 | describe("Query", () => {
10 | describe("hello", () => {
11 | it("should return an expected greeting", async () => {
12 | // Setup a test client against our Apollo Server
13 | // @ts-ignore - Argument of type 'ApolloServer' is not assignable to parameter of type 'ApolloServerBase'. Types have separate declarations of a private property 'logger'
14 | const { query } = createTestClient(apolloServer)
15 |
16 | // Execute our query
17 | const { data } = await query({
18 | query: HelloQuery,
19 | })
20 | const { hello } = data
21 | const expectedGreeting = "Hello. The current timestamp is"
22 |
23 | // Validate
24 | expect(hello).toContain(expectedGreeting)
25 | })
26 | })
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/app/__pages__/api/ping.test.ts:
--------------------------------------------------------------------------------
1 | import { handler, PingResponse as HandlerResponse } from "../../pages/api/ping"
2 | import {
3 | generateMockNextApiRequest,
4 | generateMockNextApiResponse,
5 | } from "../../__mocks__/NextApi"
6 |
7 | describe("/api/ping", () => {
8 | describe("when invoked should return", () => {
9 | let req, res
10 |
11 | beforeEach(() => {
12 | req = generateMockNextApiRequest()
13 | res = generateMockNextApiResponse()
14 | })
15 |
16 | // We need to reset mocks after every test so that we can reuse them
17 | afterEach(() => {
18 | jest.resetAllMocks()
19 | })
20 |
21 | it("an HTTP status code of 200", async () => {
22 | const result = await handler(req, res)
23 |
24 | expect(res.status).toHaveBeenCalledWith(200)
25 | })
26 | it("an expected JSON response from our handler", async () => {
27 | const result = await handler(req, res)
28 | const expectedResponse: HandlerResponse = {
29 | message: expect.any(String),
30 | }
31 | const expectedResponseShape = expect.objectContaining(expectedResponse)
32 |
33 | expect(res.json).toHaveBeenCalledWith(expectedResponseShape)
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/app/__pages__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import TestRenderer from "react-test-renderer"
3 |
4 | import DefaultPage from "../pages/index"
5 | import { MockedProvider } from "@apollo/client/testing"
6 |
7 | describe("The default page", () => {
8 | it(`should render`, () => {
9 | // Verify success state
10 | const component = TestRenderer.create(
11 |
12 |
13 |
14 | )
15 | expect(component.toJSON()).toMatchSnapshot()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/app/__screenshots__/analyze-client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/analyze-client.png
--------------------------------------------------------------------------------
/app/__screenshots__/analyze-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/analyze-server.png
--------------------------------------------------------------------------------
/app/__screenshots__/jest-coverage-report-cli.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/jest-coverage-report-cli.png
--------------------------------------------------------------------------------
/app/__screenshots__/jest-coverage-report-html.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/jest-coverage-report-html.png
--------------------------------------------------------------------------------
/app/__screenshots__/neo4j-browser-00-paste-cypher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-00-paste-cypher.png
--------------------------------------------------------------------------------
/app/__screenshots__/neo4j-browser-01-paste-cypher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-01-paste-cypher.png
--------------------------------------------------------------------------------
/app/__screenshots__/neo4j-browser-02-example-database-from-seed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-02-example-database-from-seed.png
--------------------------------------------------------------------------------
/app/__screenshots__/neo4j-browser-03-example-database-from-seed-fullscreen-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/neo4j-browser-03-example-database-from-seed-fullscreen-view.png
--------------------------------------------------------------------------------
/app/__screenshots__/nextjs-frontend-default-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/__screenshots__/nextjs-frontend-default-page.png
--------------------------------------------------------------------------------
/app/analytics/google/googleAnalytics.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GOOGLE_ANALYTICS_TRACKING_ID,
3 | pageview,
4 | event,
5 | } from "./googleAnalytics"
6 |
7 | describe("Our Google Analytics library", () => {
8 | describe("should contain a GOOGLE_ANALYTICS_TRACKING_ID that", () => {
9 | it("has been defined", () => {
10 | expect(GOOGLE_ANALYTICS_TRACKING_ID).toBeDefined()
11 | })
12 | it("is a string", () => {
13 | expect(typeof GOOGLE_ANALYTICS_TRACKING_ID).toEqual("string")
14 | })
15 | })
16 |
17 | describe("should contain a pageview function that", () => {
18 | // Create a mock for the gtag script on our window object
19 | const originalWindow = window
20 |
21 | beforeEach(() => {
22 | window.gtag = jest.fn()
23 | })
24 | afterEach(() => {
25 | window.gtag = originalWindow.gtag
26 | })
27 |
28 | it("has been defined", () => {
29 | expect(pageview).toBeDefined()
30 | })
31 | it("sends a config event to window.gtag with parameters containing a URL", () => {
32 | // Create a mock for the gtag script on our window object
33 | const mockEvent = "config"
34 | const mockUrl = "/"
35 | const expectedArgs = [
36 | mockEvent,
37 | GOOGLE_ANALYTICS_TRACKING_ID,
38 | { page_path: `${mockUrl}` },
39 | ]
40 |
41 | // Call our function
42 | pageview(mockUrl)
43 |
44 | // Validate
45 | expect(window.gtag).toBeCalledTimes(1)
46 | expect(window.gtag).toBeCalledWith(
47 | expectedArgs[0],
48 | expectedArgs[1],
49 | expectedArgs[2]
50 | )
51 | })
52 | })
53 | describe("should contain an event function that", () => {
54 | // Create a mock for the gtag script on our window object
55 | const originalWindow = window
56 |
57 | beforeEach(() => {
58 | window.gtag = jest.fn()
59 | })
60 | afterEach(() => {
61 | window.gtag = originalWindow.gtag
62 | })
63 |
64 | it("has been defined", () => {
65 | expect(event).toBeDefined()
66 | })
67 | it("sends an action to window.gtag with event details", () => {
68 | // Define our mocks
69 | const mockEvent = "event"
70 | const mockEventCategory = "ecommerce"
71 | const mockEventLabel = "test-label"
72 | const mockEventValue = "test-value"
73 | const mockEventDetails = {
74 | action: "login",
75 | category: mockEventCategory,
76 | label: mockEventLabel,
77 | value: mockEventValue,
78 | }
79 | const expectedArgs = [
80 | mockEvent,
81 | mockEventDetails.action,
82 | {
83 | event_category: mockEventDetails.category,
84 | event_label: mockEventDetails.label,
85 | value: mockEventDetails.value,
86 | },
87 | ]
88 |
89 | // Call our function
90 | event(mockEventDetails)
91 |
92 | // Validate
93 | expect(window.gtag).toBeCalledTimes(1)
94 | expect(window.gtag).toBeCalledWith(
95 | expectedArgs[0],
96 | expectedArgs[1],
97 | expectedArgs[2]
98 | )
99 | })
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/app/analytics/google/googleAnalytics.ts:
--------------------------------------------------------------------------------
1 | export const GOOGLE_ANALYTICS_TRACKING_ID =
2 | process.env.GOOGLE_ANALYTICS_TRACKING_ID
3 |
4 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
5 | export const pageview = (url) => {
6 | window.gtag("config", GOOGLE_ANALYTICS_TRACKING_ID, {
7 | page_path: url,
8 | })
9 | }
10 |
11 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
12 | export const event = ({ action, category, label, value }) => {
13 | window.gtag("event", action, {
14 | event_category: category,
15 | event_label: label,
16 | value: value,
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/app/apollo/client.test.ts:
--------------------------------------------------------------------------------
1 | import { initializeApollo, useApollo } from "./client"
2 |
3 | describe("Apollo client", () => {
4 | describe("initializeApollo", () => {
5 | it("is defined", () => {
6 | expect(initializeApollo).toBeDefined()
7 | })
8 | describe("when executed without any parameters", () => {
9 | it("should return an initialized Apollo client with an empty initial state", () => {
10 | const result = initializeApollo()
11 | const { version, localState } = result
12 | const expectedVersion = "local"
13 | const expectedState = {}
14 |
15 | expect(version).toEqual(expectedVersion)
16 | expect(localState.cache.data.data).toEqual(expectedState)
17 | })
18 | })
19 | describe("when executed with a null initialState", () => {
20 | it("should return an initialized Apollo client with an empty initial state", () => {
21 | const result = initializeApollo(null)
22 | const { version, localState } = result
23 | const expectedVersion = "local"
24 | const expectedState = {}
25 |
26 | expect(version).toEqual(expectedVersion)
27 | expect(localState.cache.data.data).toEqual(expectedState)
28 | })
29 | })
30 | describe("when executed with a defined object as initialState", () => {
31 | it("should return an Apollo client with the supplied state", () => {
32 | const mockInitialState = { aKey: "aValue" }
33 |
34 | const result = initializeApollo(mockInitialState)
35 | const { version, localState } = result
36 | const expectedVersion = "local"
37 | const expectedState = mockInitialState
38 |
39 | expect(version).toEqual(expectedVersion)
40 | expect(localState.cache.data.data).not.toEqual({})
41 | expect(localState.cache.data.data).toEqual(expectedState)
42 | })
43 | })
44 | })
45 | describe("useApollo", () => {
46 | it("is defined", () => {
47 | expect(useApollo).toBeDefined()
48 | })
49 | // REMEMBER: Hooks need to be tested within functional components directly; not as standalone code
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/app/apollo/client.ts:
--------------------------------------------------------------------------------
1 | import fetch from "cross-fetch"
2 | import { useMemo } from "react"
3 | import { ApolloClient, InMemoryCache } from "@apollo/client"
4 |
5 | let apolloClient
6 |
7 | function createIsomorphLink() {
8 | /* istanbul ignore next */
9 | if (typeof window === "undefined") {
10 | const { SchemaLink } = require("@apollo/client/link/schema")
11 | const { schema } = require("./schema")
12 | return new SchemaLink({ schema })
13 | } else {
14 | const { HttpLink } = require("@apollo/client/link/http")
15 | return new HttpLink({
16 | uri: "api/graphql",
17 | credentials: "same-origin",
18 | fetch,
19 | })
20 | }
21 | }
22 |
23 | function createApolloClient() {
24 | return new ApolloClient({
25 | ssrMode: typeof window === "undefined",
26 | link: createIsomorphLink(),
27 | cache: new InMemoryCache(),
28 | })
29 | }
30 |
31 | export function initializeApollo(initialState = null) {
32 | const _apolloClient = apolloClient ?? createApolloClient()
33 |
34 | // If your page has Next.js data fetching methods that use Apollo Client, the initial state
35 | // get hydrated here
36 | if (initialState) {
37 | _apolloClient.cache.restore(initialState)
38 | }
39 | // For SSG and SSR always create a new Apollo Client
40 | /* istanbul ignore next */
41 | if (typeof window === "undefined") return _apolloClient
42 | // Create the Apollo Client once in the client
43 | if (!apolloClient) apolloClient = _apolloClient
44 |
45 | return _apolloClient
46 | }
47 |
48 | /* istanbul ignore next */
49 | export function useApollo(initialState) {
50 | const store = useMemo(() => initializeApollo(initialState), [initialState])
51 | return store
52 | }
53 |
--------------------------------------------------------------------------------
/app/apollo/queries/hello.ts:
--------------------------------------------------------------------------------
1 | // Define custom types
2 | export type HelloResponse = {
3 | hello: string
4 | }
5 |
6 | // GraphQL Query
7 | export const HelloQuery = `
8 | {
9 | hello
10 | }
11 | `
12 |
--------------------------------------------------------------------------------
/app/apollo/resolvers.test.ts:
--------------------------------------------------------------------------------
1 | import { resolvers } from "./resolvers"
2 |
3 | describe("Our GraphQL resolvers should contain", () => {
4 | describe("A Query object", () => {
5 | const { Query } = resolvers
6 |
7 | it("that has been defined", () => {
8 | expect(Query).toBeDefined()
9 | })
10 |
11 | describe("with a custom 'hello' function", () => {
12 | const { hello } = Query
13 | it("that has been defined", () => {
14 | expect(hello).toBeDefined()
15 | })
16 | it("that returns a string when executed", async () => {
17 | const result = await hello(null, null, null)
18 | expect(typeof result).toEqual("string")
19 | })
20 | })
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/app/apollo/resolvers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Write resolvers to respond to our queries and mutations
3 | *
4 | * Be sure to view the link below to see great examples of:
5 | * - Creating custom resolvers (e.g. mutation to handle creating a User node and send an automated email)
6 | * - Translate to override auto-generated resolvers by using the same name (e.g. createUsers with custom functionality)
7 | * - Use of the @cypher directive for any query or mutation (including in type definitions for properties such as relatedPosts)
8 | * - Use of the @auth directive for accepting JWTs in the request
9 | * - Use of the @exclude directive to exclude automatically generating queries or resolvers for types
10 | * - Use of the @autogenerate directive to automatically generate unique values for ID fields
11 | *
12 | * https://www.notion.so/neo4j-graphql-v1-0-0-alpha-2-d47908030d4e4a0c86babbaef63887d0
13 | */
14 | export const resolvers = {
15 | Query: {
16 | async hello(_parent, _args, _context) {
17 | return `Hello. The current timestamp is ${Date.now()}`
18 | },
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/app/apollo/schema.test.ts:
--------------------------------------------------------------------------------
1 | import { augmentedSchema } from "./schema"
2 |
3 | describe("GraphQL schema", () => {
4 | describe("augmentedSchema", () => {
5 | it("is defined", () => {
6 | expect(augmentedSchema).toBeDefined()
7 | })
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/app/apollo/schema.ts:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from "@neo4j/graphql"
2 | import { typeDefs } from "./type-defs"
3 | import { resolvers } from "./resolvers"
4 |
5 | export const augmentedSchema = makeAugmentedSchema({
6 | typeDefs,
7 | resolvers,
8 | })
9 |
--------------------------------------------------------------------------------
/app/apollo/type-defs.test.ts:
--------------------------------------------------------------------------------
1 | import { typeDefs } from "./type-defs"
2 |
3 | describe("GraphQL type definitions", () => {
4 | it("are defined", () => {
5 | expect(typeDefs).toBeDefined()
6 | })
7 | })
8 |
--------------------------------------------------------------------------------
/app/apollo/type-defs.ts:
--------------------------------------------------------------------------------
1 | // Describe our GraphQL schema with type definitions
2 | export const typeDefs = `
3 | type Business {
4 | businessId: ID!
5 | name: String!
6 | address: String
7 | city: String
8 | state: String
9 | reviews: [Review] @relationship(type: "REVIEWS", direction: "IN")
10 | categories: [Category] @relationship(type: "IN_CATEGORY", direction: "OUT")
11 | }
12 |
13 | type Category {
14 | name: ID!
15 | businesses: [Business] @relationship(type: "IN_CATEGORY", direction: "IN")
16 | }
17 |
18 | type RatingCount {
19 | stars: Float!
20 | count: Int!
21 | }
22 |
23 | type User {
24 | userId: ID!
25 | name: String
26 | reviews: [Review] @relationship(type: "WROTE", direction: "OUT")
27 | }
28 |
29 | type Review {
30 | reviewId: ID!
31 | stars: Float
32 | text: String
33 | date: Date
34 | business: Business @relationship(type: "REVIEWS", direction: "OUT")
35 | user: User @relationship(type: "WROTE", direction: "IN")
36 | }
37 |
38 | type Query {
39 | """
40 | A sample query to verify that our GraphQL server is online.
41 |
42 | It returns a friendly greeting with the current timestamp.
43 | """
44 | hello: String!,
45 |
46 | """
47 | A sample query to return the total number of users in our system
48 | """
49 | userCount: Int! @cypher(statement: "MATCH (u:User) RETURN COUNT(u)")
50 |
51 | """
52 | A sample query to return the total number of stars awarded in reviews for our system
53 | """
54 | ratingsCount: [RatingCount]
55 | @cypher(
56 | statement: "MATCH (r:Review) WITH r.stars AS stars, COUNT(*) AS count ORDER BY stars RETURN {stars: stars, count: count}"
57 | )
58 | }
59 | `
60 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/Dashboard/Dashboard.styles.tsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { makeStyles } from "@material-ui/core/styles"
3 |
4 | export const useStyles = makeStyles((theme) => ({
5 | root: {
6 | display: "flex",
7 | },
8 | paper: {
9 | padding: theme.spacing(2),
10 | display: "flex",
11 | overflow: "auto",
12 | flexDirection: "column",
13 | },
14 | fixedHeight: {
15 | height: 240,
16 | },
17 | }))
18 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/Dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import clsx from "clsx"
3 |
4 | // Components
5 | import RatingsChart from "../RatingsChart/RatingsChart"
6 | import UserCount from "../UserCount/UserCount"
7 | import RecentReviews from "../RecentReviews/RecentReviews"
8 |
9 | // Material UI
10 | import { useStyles } from "./Dashboard.styles"
11 | import { useTheme } from "@material-ui/core/styles"
12 | import { Grid, Paper } from "@material-ui/core"
13 |
14 | const Dashboard = () => {
15 | const theme = useTheme()
16 | const classes = useStyles(theme)
17 | const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight)
18 |
19 | return (
20 | <>
21 |
22 | {/* Ratings Chart */}
23 |
24 |
25 |
26 |
27 |
28 | {/* User Count */}
29 |
30 |
31 |
32 |
33 |
34 | {/* Recent Reviews */}
35 |
36 |
37 |
38 |
39 |
40 |
41 | >
42 | )
43 | }
44 | export default Dashboard
45 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/RatingsChart/RatingsChart.test.tsx:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | import React from "react"
3 | import TestRenderer, { act } from "react-test-renderer"
4 | import { MockedProvider } from "@apollo/client/testing"
5 |
6 | import RatingsChart, { GET_DATA_QUERY } from "./RatingsChart"
7 |
8 | describe("GRANDstack RatingsChart component", () => {
9 | describe("when invoked WITHOUT a specific height or size", () => {
10 | it("should render the Ratings Distribution chart after receiving data", async () => {
11 | // Define our Apollo request
12 | const renderRequest = {
13 | request: {
14 | query: GET_DATA_QUERY,
15 | variables: {},
16 | },
17 | result: {
18 | data: {
19 | ratingsCount: [
20 | { stars: 3, count: 2, __typename: "RatingCount" },
21 | { stars: 4, count: 4, __typename: "RatingCount" },
22 | { stars: 5, count: 6, __typename: "RatingCount" },
23 | ],
24 | },
25 | },
26 | }
27 |
28 | // Define our mock response(s)
29 | const gqlMocks = [renderRequest]
30 |
31 | // REVISIT: Known untestable design using Recharts 🥺
32 | console.warn(
33 | `This is untestable due to a bug with testing the ResizeDetector component within the Recharts third-party library 🥺`
34 | )
35 |
36 | // Test
37 | // const component = TestRenderer.create(
38 | //
39 | //
40 | //
41 | // )
42 |
43 | // Advance to the next tick in the event loop so our chart can render
44 | // await act(() => {
45 | // return new Promise((resolve) => {
46 | // setTimeout(resolve, 0)
47 | // })
48 | // })
49 |
50 | // Verify
51 | })
52 | })
53 |
54 | describe("when invoked WITH a specific height and size", () => {
55 | it("should render the Ratings Distribution chart after receiving data", async () => {
56 | // Define our Apollo request
57 | const renderRequest = {
58 | request: {
59 | query: GET_DATA_QUERY,
60 | variables: {},
61 | },
62 | result: {
63 | data: {
64 | ratingsCount: [
65 | { stars: 3, count: 2, __typename: "RatingCount" },
66 | { stars: 4, count: 4, __typename: "RatingCount" },
67 | { stars: 5, count: 6, __typename: "RatingCount" },
68 | ],
69 | },
70 | },
71 | }
72 |
73 | // Define our mock response(s)
74 | const gqlMocks = [renderRequest]
75 |
76 | // REVISIT: Known untestable design using Recharts 🥺
77 | console.warn(
78 | `This is untestable due to a bug with testing the ResizeDetector component within the Recharts third-party library 🥺`
79 | )
80 |
81 | // Test
82 | // const component = TestRenderer.create(
83 | //
84 | //
85 | //
86 | // )
87 |
88 | // Advance to the next tick in the event loop so our chart can render
89 | // await act(() => {
90 | // return new Promise((resolve) => {
91 | // setTimeout(resolve, 0)
92 | // })
93 | // })
94 |
95 | // Verify
96 | })
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/RatingsChart/RatingsChart.tsx:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | import { FC } from "react"
3 | import { useQuery } from "@apollo/react-hooks"
4 | import gql from "graphql-tag"
5 |
6 | // Components
7 | import Title from "../Title/Title"
8 |
9 | // Material UI
10 | import { useTheme } from "@material-ui/core/styles"
11 |
12 | // Chart
13 | import {
14 | Bar,
15 | XAxis,
16 | YAxis,
17 | Label,
18 | ResponsiveContainer,
19 | BarChart,
20 | } from "recharts"
21 |
22 | // GraphQL
23 | export const GET_DATA_QUERY = gql`
24 | {
25 | ratingsCount {
26 | stars
27 | count
28 | }
29 | }
30 | `
31 |
32 | interface IRatingsChart {
33 | width?: number | string
34 | height?: number | string
35 | }
36 |
37 | const RatingsChart: FC = ({ width, height }) => {
38 | const theme = useTheme()
39 | const { loading, error, data } = useQuery(GET_DATA_QUERY)
40 |
41 | if (error) return Error
42 | if (loading) return Loading
43 |
44 | return (
45 | <>
46 | Ratings Distribution
47 |
51 |
60 |
61 |
62 |
67 | Count
68 |
69 |
70 |
71 |
72 |
73 | >
74 | )
75 | }
76 | export default RatingsChart
77 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/RecentReviews/RecentReviews.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import TestRenderer, { act } from "react-test-renderer"
3 | import { MockedProvider } from "@apollo/client/testing"
4 |
5 | import RecentReviews, { GET_RECENT_REVIEWS_QUERY } from "./RecentReviews"
6 |
7 | describe("GRANDstack RecentReviews component", () => {
8 | it("should render the latest reviews after receiving data", async () => {
9 | // Define our Apollo request
10 | const renderRequest = {
11 | request: {
12 | query: GET_RECENT_REVIEWS_QUERY,
13 | variables: {},
14 | },
15 | result: {
16 | data: {
17 | Reviews: [
18 | {
19 | date: {
20 | year: {
21 | low: 2018,
22 | high: 0,
23 | },
24 | month: {
25 | low: 9,
26 | high: 0,
27 | },
28 | day: {
29 | low: 10,
30 | high: 0,
31 | },
32 | },
33 | user: {
34 | userId: "u4",
35 | name: "Angie",
36 | __typename: "User",
37 | },
38 | business: {
39 | name: "Imagine Nation Brewing",
40 | __typename: "Business",
41 | },
42 | text: "",
43 | stars: {
44 | low: 3,
45 | high: 0,
46 | },
47 | __typename: "Review",
48 | },
49 | {
50 | date: {
51 | year: {
52 | low: 2018,
53 | high: 0,
54 | },
55 | month: {
56 | low: 8,
57 | high: 0,
58 | },
59 | day: {
60 | low: 11,
61 | high: 0,
62 | },
63 | },
64 | user: {
65 | userId: "u4",
66 | name: "Angie",
67 | __typename: "User",
68 | },
69 | business: {
70 | name: "Zootown Brew",
71 | __typename: "Business",
72 | },
73 | text: "",
74 | stars: {
75 | low: 5,
76 | high: 0,
77 | },
78 | __typename: "Review",
79 | },
80 | {
81 | date: {
82 | year: {
83 | low: 2018,
84 | high: 0,
85 | },
86 | month: {
87 | low: 3,
88 | high: 0,
89 | },
90 | day: {
91 | low: 24,
92 | high: 0,
93 | },
94 | },
95 | user: {
96 | userId: "u2",
97 | name: "Bob",
98 | __typename: "User",
99 | },
100 | business: {
101 | name: "Market on Front",
102 | __typename: "Business",
103 | },
104 | text: "",
105 | stars: {
106 | low: 4,
107 | high: 0,
108 | },
109 | __typename: "Review",
110 | },
111 | {
112 | date: {
113 | year: {
114 | low: 2018,
115 | high: 0,
116 | },
117 | month: {
118 | low: 1,
119 | high: 0,
120 | },
121 | day: {
122 | low: 3,
123 | high: 0,
124 | },
125 | },
126 | user: {
127 | userId: "u1",
128 | name: "Will",
129 | __typename: "User",
130 | },
131 | business: {
132 | name: "Ninja Mike's",
133 | __typename: "Business",
134 | },
135 | text:
136 | "Best breakfast sandwich at the Farmer's Market. Always get the works.",
137 | stars: {
138 | low: 4,
139 | high: 0,
140 | },
141 | __typename: "Review",
142 | },
143 | {
144 | date: {
145 | year: {
146 | low: 2017,
147 | high: 0,
148 | },
149 | month: {
150 | low: 11,
151 | high: 0,
152 | },
153 | day: {
154 | low: 13,
155 | high: 0,
156 | },
157 | },
158 | user: {
159 | userId: "u3",
160 | name: "Jenny",
161 | __typename: "User",
162 | },
163 | business: {
164 | name: "Ninja Mike's",
165 | __typename: "Business",
166 | },
167 | text: "",
168 | stars: {
169 | low: 5,
170 | high: 0,
171 | },
172 | __typename: "Review",
173 | },
174 | {
175 | date: {
176 | year: {
177 | low: 2016,
178 | high: 0,
179 | },
180 | month: {
181 | low: 11,
182 | high: 0,
183 | },
184 | day: {
185 | low: 21,
186 | high: 0,
187 | },
188 | },
189 | user: {
190 | userId: "u3",
191 | name: "Jenny",
192 | __typename: "User",
193 | },
194 | business: {
195 | name: "Hanabi",
196 | __typename: "Business",
197 | },
198 | text: "",
199 | stars: {
200 | low: 5,
201 | high: 0,
202 | },
203 | __typename: "Review",
204 | },
205 | {
206 | date: {
207 | year: {
208 | low: 2016,
209 | high: 0,
210 | },
211 | month: {
212 | low: 7,
213 | high: 0,
214 | },
215 | day: {
216 | low: 14,
217 | high: 0,
218 | },
219 | },
220 | user: {
221 | userId: "u3",
222 | name: "Jenny",
223 | __typename: "User",
224 | },
225 | business: {
226 | name: "KettleHouse Brewing Co.",
227 | __typename: "Business",
228 | },
229 | text: "",
230 | stars: {
231 | low: 5,
232 | high: 0,
233 | },
234 | __typename: "Review",
235 | },
236 | {
237 | date: {
238 | year: {
239 | low: 2016,
240 | high: 0,
241 | },
242 | month: {
243 | low: 3,
244 | high: 0,
245 | },
246 | day: {
247 | low: 4,
248 | high: 0,
249 | },
250 | },
251 | user: {
252 | userId: "u1",
253 | name: "Will",
254 | __typename: "User",
255 | },
256 | business: {
257 | name: "Ducky's Car Wash",
258 | __typename: "Business",
259 | },
260 | text: "Awesome full-service car wash. Love Ducky's!",
261 | stars: {
262 | low: 5,
263 | high: 0,
264 | },
265 | __typename: "Review",
266 | },
267 | {
268 | date: {
269 | year: {
270 | low: 2016,
271 | high: 0,
272 | },
273 | month: {
274 | low: 1,
275 | high: 0,
276 | },
277 | day: {
278 | low: 3,
279 | high: 0,
280 | },
281 | },
282 | user: {
283 | userId: "u1",
284 | name: "Will",
285 | __typename: "User",
286 | },
287 | business: {
288 | name: "KettleHouse Brewing Co.",
289 | __typename: "Business",
290 | },
291 | text: "Great IPA selection!",
292 | stars: {
293 | low: 4,
294 | high: 0,
295 | },
296 | __typename: "Review",
297 | },
298 | {
299 | date: {
300 | year: {
301 | low: 2015,
302 | high: 0,
303 | },
304 | month: {
305 | low: 12,
306 | high: 0,
307 | },
308 | day: {
309 | low: 15,
310 | high: 0,
311 | },
312 | },
313 | user: {
314 | userId: "u2",
315 | name: "Bob",
316 | __typename: "User",
317 | },
318 | business: {
319 | name: "Imagine Nation Brewing",
320 | __typename: "Business",
321 | },
322 | text: "",
323 | stars: {
324 | low: 4,
325 | high: 0,
326 | },
327 | __typename: "Review",
328 | },
329 | {
330 | date: {
331 | year: {
332 | low: 2015,
333 | high: 0,
334 | },
335 | month: {
336 | low: 9,
337 | high: 0,
338 | },
339 | day: {
340 | low: 1,
341 | high: 0,
342 | },
343 | },
344 | user: {
345 | userId: "u1",
346 | name: "Will",
347 | __typename: "User",
348 | },
349 | business: {
350 | name: "Neo4j",
351 | __typename: "Business",
352 | },
353 | text: "The world's leading graph database HQ!",
354 | stars: {
355 | low: 5,
356 | high: 0,
357 | },
358 | __typename: "Review",
359 | },
360 | {
361 | date: {
362 | year: {
363 | low: 2015,
364 | high: 0,
365 | },
366 | month: {
367 | low: 8,
368 | high: 0,
369 | },
370 | day: {
371 | low: 29,
372 | high: 0,
373 | },
374 | },
375 | user: {
376 | userId: "u1",
377 | name: "Will",
378 | __typename: "User",
379 | },
380 | business: {
381 | name: "Missoula Public Library",
382 | __typename: "Business",
383 | },
384 | text:
385 | "Not a great selection of books, but fortunately the inter-library loan system is good. Wifi is quite slow. Not many comfortable places to site and read. Looking forward to the new building across the street in 2020!",
386 | stars: {
387 | low: 3,
388 | high: 0,
389 | },
390 | __typename: "Review",
391 | },
392 | ],
393 | },
394 | },
395 | }
396 |
397 | // Define our mock response(s)
398 | const gqlMocks = [renderRequest]
399 |
400 | // Verify success state
401 | const component = TestRenderer.create(
402 |
403 |
404 |
405 | )
406 |
407 | // Advance to the next tick in the event loop so our chart can render
408 | await act(() => {
409 | return new Promise((resolve) => {
410 | setTimeout(resolve, 0)
411 | })
412 | })
413 |
414 | // Verify
415 | const p = component.root.findByType("h2")
416 | expect(p.children).toContain("Recent Reviews")
417 | })
418 |
419 | describe("should display an error message if", () => {
420 | it("there is a network request failure", async () => {
421 | // Define our Apollo request
422 | const renderRequest = {
423 | request: {
424 | query: GET_RECENT_REVIEWS_QUERY,
425 | variables: {},
426 | },
427 | error: new Error("Simulated network or HTTP error"),
428 | }
429 |
430 | // Define our mock response(s)
431 | const gqlMocks = [renderRequest]
432 |
433 | // Verify success state
434 | const component = TestRenderer.create(
435 |
436 |
437 |
438 | )
439 |
440 | // Advance to the next tick in the event loop so our chart can render
441 | await act(() => {
442 | return new Promise((resolve) => {
443 | setTimeout(resolve, 0)
444 | })
445 | })
446 |
447 | // Verify
448 | const tree = component.toJSON()
449 | expect(tree.children).toContain("Simulated network or HTTP error")
450 | })
451 | it("our GraphQL server successfully processed our request and has returned with one or more error(s) in the result", async () => {
452 | // Define our Apollo request
453 | const renderRequest = {
454 | request: {
455 | query: GET_RECENT_REVIEWS_QUERY,
456 | variables: {},
457 | },
458 | result: {
459 | // REVISIT: Create a pull request - errors should be able to be defined on the GraphQL result object because it can contain data and errors
460 | // https://www.apollographql.com/docs/react/development-testing/testing/#graphql-errors
461 | errors: [
462 | new Error("Simulated GraphQL server response with an error"),
463 | ],
464 | },
465 | }
466 |
467 | // Define our mock response(s)
468 | const gqlMocks = [renderRequest]
469 |
470 | // Verify success state
471 | const component = TestRenderer.create(
472 |
473 |
474 |
475 | )
476 |
477 | // Advance to the next tick in the event loop so our chart can render
478 | await act(() => {
479 | return new Promise((resolve) => {
480 | setTimeout(resolve, 0)
481 | })
482 | })
483 |
484 | // Verify
485 | const tree = component.toJSON()
486 | expect(tree.children).toContain(
487 | "Simulated GraphQL server response with an error"
488 | )
489 | })
490 | })
491 | })
492 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/RecentReviews/RecentReviews.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 | import { useQuery } from "@apollo/react-hooks"
3 | import gql from "graphql-tag"
4 |
5 | // Components
6 | import Title from "../Title/Title"
7 |
8 | // Material UI
9 | import {
10 | Table,
11 | TableBody,
12 | TableCell,
13 | TableHead,
14 | TableRow,
15 | } from "@material-ui/core"
16 |
17 | export const GET_RECENT_REVIEWS_QUERY = gql`
18 | {
19 | Reviews(options: { sort: [date_DESC] }) {
20 | date
21 | user {
22 | userId
23 | name
24 | }
25 | business {
26 | name
27 | }
28 | text
29 | stars
30 | }
31 | }
32 | `
33 |
34 | const RecentReviews: FC = () => {
35 | const { loading, error, data } = useQuery(GET_RECENT_REVIEWS_QUERY)
36 | if (loading) return Loading
37 | if (error) return {error.message}
38 |
39 | return (
40 | <>
41 | Recent Reviews
42 |
43 |
44 |
45 | Date
46 | Business
47 | User
48 | Review
49 | Stars
50 |
51 |
52 |
53 | {data?.Reviews.map((review) => (
54 |
57 |
58 | {`${review?.date?.year?.low}.${review?.date?.month?.low}.${review?.date?.day?.low}`}
59 |
60 | {review?.business?.name}
61 | {review?.user?.name}
62 | {review?.text}
63 | {review?.stars?.low}
64 |
65 | ))}
66 |
67 |
68 | >
69 | )
70 | }
71 | export default RecentReviews
72 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/Title/Title.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import TestRenderer from "react-test-renderer"
3 | import { MockedProvider } from "@apollo/client/testing"
4 |
5 | import Title from "./Title"
6 |
7 | describe("GRANDstack Title component", () => {
8 | describe("should render", () => {
9 | it("without any children", () => {
10 | const component = TestRenderer.create(
11 |
12 |
13 |
14 | )
15 | expect(component.toJSON()).toMatchSnapshot()
16 | })
17 | it("with a supplied title", () => {
18 | const title = "A GRANDstack Title"
19 | const component = TestRenderer.create(
20 |
21 | {title}
22 |
23 | )
24 | expect(component.toJSON()).toMatchSnapshot()
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/Title/Title.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | // Material UI
4 | import { Typography } from "@material-ui/core"
5 |
6 | interface ITitle {
7 | children?: string
8 | }
9 |
10 | const Title: FC = ({ children }) => {
11 | return (
12 |
13 | {children}
14 |
15 | )
16 | }
17 | export default Title
18 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/Title/__snapshots__/Title.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`GRANDstack Title component should render with a supplied title 1`] = `
4 |
7 | A GRANDstack Title
8 |
9 | `;
10 |
11 | exports[`GRANDstack Title component should render without any children 1`] = `
12 |
15 | `;
16 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/UserCount/UserCount.styles.tsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { makeStyles } from "@material-ui/core/styles"
3 |
4 | export const useStyles = makeStyles({
5 | depositContext: {
6 | flex: 1,
7 | },
8 | navLink: {
9 | textDecoration: "none",
10 | },
11 | })
12 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/UserCount/UserCount.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import TestRenderer, { act } from "react-test-renderer"
3 | import { MockedProvider } from "@apollo/client/testing"
4 |
5 | import UserCount, { GET_USER_COUNT_QUERY } from "./UserCount"
6 |
7 | describe("GRANDstack UserCount component", () => {
8 | it("should render without an error when loading data", async () => {
9 | const expectedTotalUsers = 4
10 |
11 | // Define our Apollo request
12 | const renderRequest = {
13 | request: {
14 | query: GET_USER_COUNT_QUERY,
15 | variables: {},
16 | },
17 | result: {
18 | data: { userCount: expectedTotalUsers },
19 | },
20 | }
21 |
22 | // Define our mock response(s)
23 | const gqlMocks = [renderRequest]
24 |
25 | // Render component
26 | const component = TestRenderer.create(
27 |
28 |
29 |
30 | )
31 |
32 | // NOTE: We are NOT advancing to the next tick in the event loop; we expect to see a loading state on the first render
33 |
34 | // Verify loading state
35 | expect(component.toJSON()).toMatchSnapshot()
36 | })
37 |
38 | it("should render the Total Users chart after receiving data", async () => {
39 | const expectedTotalUsers = 4
40 |
41 | // Define our Apollo request
42 | const renderRequest = {
43 | request: {
44 | query: GET_USER_COUNT_QUERY,
45 | variables: {},
46 | },
47 | result: {
48 | data: { userCount: expectedTotalUsers },
49 | },
50 | }
51 |
52 | // Define our mock response(s)
53 | const gqlMocks = [renderRequest]
54 |
55 | // Render component
56 | const component = TestRenderer.create(
57 |
58 |
59 |
60 | )
61 |
62 | // Advance to the next tick in the event loop so our chart can render
63 | await act(() => {
64 | return new Promise((resolve) => {
65 | setTimeout(resolve, 0)
66 | })
67 | })
68 |
69 | // Generate our result string
70 | const p = component.root.findAllByType("p")
71 | const result = p
72 | .map((obj) => {
73 | return obj.children
74 | })
75 | .join(" ")
76 |
77 | // Verify success state
78 | expect(result).toContain(`${expectedTotalUsers} users found`)
79 | })
80 |
81 | describe("should display an error message if", () => {
82 | it("there is a network request failure", async () => {
83 | // Define our Apollo request
84 | const renderRequest = {
85 | request: {
86 | query: GET_USER_COUNT_QUERY,
87 | variables: {},
88 | },
89 | error: new Error("Simulated network or HTTP error"),
90 | }
91 |
92 | // Define our mock response(s)
93 | const gqlMocks = [renderRequest]
94 |
95 | // Verify success state
96 | const component = TestRenderer.create(
97 |
98 |
99 |
100 | )
101 |
102 | // Advance to the next tick in the event loop so our chart can render
103 | await act(() => {
104 | return new Promise((resolve) => {
105 | setTimeout(resolve, 0)
106 | })
107 | })
108 |
109 | // Verify
110 | const tree = component.toJSON()
111 | expect(tree.children).toContain("Simulated network or HTTP error")
112 | })
113 | it("our GraphQL server successfully processed our request and has returned with one or more error(s) in the result", async () => {
114 | // Define our Apollo request
115 | const renderRequest = {
116 | request: {
117 | query: GET_USER_COUNT_QUERY,
118 | variables: {},
119 | },
120 | result: {
121 | // REVISIT: Create a pull request - errors should be able to be defined on the GraphQL result object because it can contain data and errors
122 | // https://www.apollographql.com/docs/react/development-testing/testing/#graphql-errors
123 | errors: [
124 | new Error("Simulated GraphQL server response with an error"),
125 | ],
126 | },
127 | }
128 |
129 | // Define our mock response(s)
130 | const gqlMocks = [renderRequest]
131 |
132 | // Verify success state
133 | const component = TestRenderer.create(
134 |
135 |
136 |
137 | )
138 |
139 | // Advance to the next tick in the event loop so our chart can render
140 | await act(() => {
141 | return new Promise((resolve) => {
142 | setTimeout(resolve, 0)
143 | })
144 | })
145 |
146 | // Verify
147 | const tree = component.toJSON()
148 | expect(tree.children).toContain(
149 | "Simulated GraphQL server response with an error"
150 | )
151 | })
152 | })
153 | })
154 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/UserCount/UserCount.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 | import { useQuery } from "@apollo/react-hooks"
3 | import gql from "graphql-tag"
4 |
5 | // Components
6 | import Title from "../Title/Title"
7 |
8 | // Material UI
9 | import { useStyles } from "./UserCount.styles"
10 | import { Typography } from "@material-ui/core"
11 |
12 | export const GET_USER_COUNT_QUERY = gql`
13 | {
14 | userCount
15 | }
16 | `
17 |
18 | const UserCount: FC = () => {
19 | const classes = useStyles()
20 |
21 | const { loading, error, data } = useQuery(GET_USER_COUNT_QUERY)
22 | if (error) return {error.message}
23 |
24 | return (
25 | <>
26 | Total Users
27 |
28 | {loading ? "Loading..." : data.userCount}
29 |
30 |
31 | users found
32 |
33 | >
34 | )
35 | }
36 | export default UserCount
37 |
--------------------------------------------------------------------------------
/app/grandstack-demo/components/UserCount/__snapshots__/UserCount.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`GRANDstack UserCount component should render without an error when loading data 1`] = `
4 | Array [
5 |
8 | Total Users
9 | ,
10 |
13 | Loading...
14 |
,
15 |
18 | users found
19 |
,
20 | ]
21 | `;
22 |
--------------------------------------------------------------------------------
/app/grandstack-demo/layout/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | // Material UI
4 | import { Box, Link as MUILink, Typography } from "@material-ui/core"
5 |
6 | const Footer: FC = () => {
7 | return (
8 |
9 |
10 | {"Copyright © "}
11 | {new Date().getFullYear()}{" "}
12 |
13 | Robert J Brennan
14 |
15 | {". All rights reserved."}
16 |
17 |
18 | )
19 | }
20 | export default Footer
21 |
--------------------------------------------------------------------------------
/app/grandstack-demo/layout/Header/Header.styles.tsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { makeStyles } from "@material-ui/core/styles"
3 |
4 | // Styling
5 | export const useStyles = makeStyles((theme) => ({
6 | toolbar: {
7 | paddingRight: 24,
8 | },
9 | appBar: {
10 | transition: theme.transitions.create(["width", "margin"], {
11 | easing: theme.transitions.easing.sharp,
12 | duration: theme.transitions.duration.leavingScreen,
13 | }),
14 | },
15 | title: {
16 | flexGrow: 1,
17 | },
18 | appBarImage: {
19 | maxHeight: "75px",
20 | paddingRight: "20px",
21 | },
22 | }))
23 |
--------------------------------------------------------------------------------
/app/grandstack-demo/layout/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 | import Head from "next/head"
3 | import Link from "next/link"
4 | import clsx from "clsx"
5 |
6 | import { dependencies } from "../../../package.json"
7 |
8 | // Material UI
9 | import { useStyles } from "./Header.styles"
10 | import { AppBar, CssBaseline, Toolbar, Typography } from "@material-ui/core"
11 |
12 | const Header: FC = () => {
13 | const classes = useStyles()
14 |
15 | const APP_TITLE = `Next.js GRANDstack Starter with @neo4j/graphql ${dependencies["@neo4j/graphql"]} and TypeScript`
16 | const APP_DESCRIPTION = `A sample ${APP_TITLE}`
17 | const APP_URL = "https://nextjs-grandstack-starter-typescript.vercel.app"
18 | const APP_TYPE = "website"
19 | const APP_LOGO = "img/grandstack.png"
20 | const APP_LOGO_URL = `${APP_URL}/${APP_LOGO}`
21 | const APP_TWITTER_ACCOUNT = "therobbrennan"
22 | const KEYWORDS = "nextjs, react, grandstack, neo4j, typescript, apollo"
23 |
24 | return (
25 | <>
26 |
27 | {APP_TITLE}
28 |
33 |
34 |
35 |
36 | {/* Open Graph */}
37 |
42 |
43 |
44 |
45 |
46 | {/* Twitter */}
47 |
52 |
57 |
62 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
85 | {APP_TITLE}
86 |
87 |
88 |
89 | >
90 | )
91 | }
92 | export default Header
93 |
--------------------------------------------------------------------------------
/app/grandstack-demo/layout/Layout.styles.tsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { makeStyles } from "@material-ui/core/styles"
3 |
4 | // Styling
5 | export const useStyles = makeStyles((theme) => ({
6 | root: {
7 | display: "flex",
8 | },
9 | appBarSpacer: theme.mixins.toolbar,
10 | content: {
11 | flexGrow: 1,
12 | height: "100vh",
13 | overflow: "auto",
14 | },
15 | container: {
16 | paddingTop: theme.spacing(4),
17 | paddingBottom: theme.spacing(4),
18 | },
19 | }))
20 |
--------------------------------------------------------------------------------
/app/grandstack-demo/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | // Material UI
4 | import { useStyles } from "./Layout.styles"
5 | import { Container } from "@material-ui/core"
6 | import Header from "./Header/Header"
7 | import Footer from "./Footer/Footer"
8 |
9 | interface ILayout {
10 | children?: JSX.Element[] | JSX.Element | string | null
11 | }
12 |
13 | const Layout: FC = ({ children }) => {
14 | const classes = useStyles()
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 |
26 |
27 | )
28 | }
29 | export default Layout
30 |
--------------------------------------------------------------------------------
/app/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: [""],
3 | moduleFileExtensions: ["js", "ts", "tsx", "json"],
4 | setupFiles: ["/jest.setup.js", "dotenv/config"],
5 | testPathIgnorePatterns: [
6 | "[/\\\\](build|docs|node_modules|.next|.vercel|coverage)[/\\\\]",
7 | ],
8 | transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$"],
9 | transform: {
10 | "^.+\\.(ts|tsx)$": "babel-jest",
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/app/jest.setup.js:
--------------------------------------------------------------------------------
1 | const enzyme = require("enzyme")
2 | const Adapter = require("enzyme-adapter-react-16")
3 |
4 | enzyme.configure({ adapter: new Adapter() })
5 |
--------------------------------------------------------------------------------
/app/neo4j/__seed__/db.cypher:
--------------------------------------------------------------------------------
1 | // OPTIONAL: Clear previously defined constraints
2 | // DROP CONSTRAINT ON (node:User) ASSERT (node.userId) IS UNIQUE;
3 | // DROP CONSTRAINT ON (node:Category) ASSERT (node.name) IS UNIQUE;
4 | // DROP CONSTRAINT ON (node:Review) ASSERT (node.reviewId) IS UNIQUE;
5 | // DROP CONSTRAINT ON (node:Business) ASSERT (node.businessId) IS UNIQUE;
6 |
7 | // Generated using "CALL apoc.export.cypher.all(null);" in Cypher to export all Cypher statements in a single row in the cypherStatements column based on the original GRANDstack Database Seed creation
8 | CREATE CONSTRAINT ON (node:User) ASSERT (node.userId) IS UNIQUE;
9 | CREATE CONSTRAINT ON (node:Category) ASSERT (node.name) IS UNIQUE;
10 | CREATE CONSTRAINT ON (node:Review) ASSERT (node.reviewId) IS UNIQUE;
11 | CREATE CONSTRAINT ON (node:Business) ASSERT (node.businessId) IS UNIQUE;
12 |
13 | CALL db.awaitIndexes(300);
14 |
15 | UNWIND [{reviewId:"r4", properties:{date:date('2017-11-13'), text:"", stars:5}},
16 | {reviewId:"r8", properties:{date:date('2018-08-11'), text:"", stars:5}},
17 | {reviewId:"r11", properties:{date:date('2016-03-04'), text:"Awesome full-service car wash. Love Ducky's!", stars:5}},
18 | {reviewId:"r7", properties:{date:date('2015-08-29'), text:"Not a great selection of books, but fortunately the inter-library loan system is good. Wifi is quite slow. Not many comfortable places to site and read. Looking forward to the new building across the street in 2020!", stars:3}},
19 | {reviewId:"r3", properties:{date:date('2018-09-10'), text:"", stars:3}},
20 | {reviewId:"r12", properties:{date:date('2015-09-01'), text:"The world's leading graph database HQ!", stars:5}},
21 | {reviewId:"r9", properties:{date:date('2016-11-21'), text:"", stars:5}},
22 | {reviewId:"r2", properties:{date:date('2016-07-14'), text:"", stars:5}},
23 | {reviewId:"r5", properties:{date:date('2018-01-03'), text:"Best breakfast sandwich at the Farmer's Market. Always get the works.", stars:4}},
24 | {reviewId:"r1", properties:{date:date('2016-01-03'), text:"Great IPA selection!", stars:4}},
25 | {reviewId:"r10", properties:{date:date('2015-12-15'), text:"", stars:4}},
26 | {reviewId:"r6", properties:{date:date('2018-03-24'), text:"", stars:4}}]
27 | AS row
28 | CREATE (n:Review{reviewId: row.reviewId}) SET n += row.properties;
29 |
30 | UNWIND [{businessId:"b2", properties:{address:"1151 W Broadway St", city:"Missoula", name:"Imagine Nation Brewing", location:point({x: -114.009628, y: 46.876672, crs: 'wgs-84'}), state:"MT"}},
31 | {businessId:"b1", properties:{address:"313 N 1st St W", city:"Missoula", name:"KettleHouse Brewing Co.", location:point({x: -113.995297, y: 46.877981, crs: 'wgs-84'}), state:"MT"}},
32 | {businessId:"b6", properties:{address:"121 W Broadway St", city:"Missoula", name:"Zootown Brew", location:point({x: -113.995018, y: 46.873985, crs: 'wgs-84'}), state:"MT"}},
33 | {businessId:"b3", properties:{address:"200 W Pine St", city:"Missoula", name:"Ninja Mike's", location:point({x: -113.995057, y: 46.874029, crs: 'wgs-84'}), state:"MT"}},
34 | {businessId:"b7", properties:{address:"723 California Dr", city:"Burlingame", name:"Hanabi", location:point({x: -122.351519, y: 37.582598, crs: 'wgs-84'}), state:"CA"}},
35 | {businessId:"b4", properties:{address:"201 E Front St", city:"Missoula", name:"Market on Front", location:point({x: -113.993633, y: 46.869824, crs: 'wgs-84'}), state:"MT"}},
36 | {businessId:"b8", properties:{address:"716 N San Mateo Dr", city:"San Mateo", name:"Ducky's Car Wash", location:point({x: -122.336041, y: 37.575968, crs: 'wgs-84'}), state:"CA"}},
37 | {businessId:"b5", properties:{address:"301 E Main St", city:"Missoula", name:"Missoula Public Library", location:point({x: -113.990976, y: 46.870035, crs: 'wgs-84'}), state:"MT"}},
38 | {businessId:"b9", properties:{address:"111 E 5th Ave", city:"San Mateo", name:"Neo4j", location:point({x: -122.322269, y: 37.563534, crs: 'wgs-84'}), state:"CA"}}]
39 | AS row
40 | CREATE (n:Business{businessId: row.businessId}) SET n += row.properties;
41 |
42 | UNWIND [{name:"Car Wash", properties:{}}, {name:"Brewery", properties:{}},
43 | {name:"Beer", properties:{}}, {name:"Breakfast", properties:{}}, {name:"Deli", properties:{}},
44 | {name:"Cafe", properties:{}}, {name:"Coffee", properties:{}}, {name:"Graph Database", properties:{}},
45 | {name:"Library", properties:{}}, {name:"Ramen", properties:{}}, {name:"Restaurant", properties:{}}]
46 | AS row
47 | CREATE (n:Category{name: row.name}) SET n += row.properties;
48 |
49 | UNWIND [{userId:"u4", properties:{name:"Angie"}},
50 | {userId:"u2", properties:{name:"Bob"}},
51 | {userId:"u3", properties:{name:"Jenny"}},
52 | {userId:"u1", properties:{name:"Will"}}]
53 | AS row
54 | CREATE (n:User{userId: row.userId}) SET n += row.properties;
55 |
56 | UNWIND [{start: {businessId:"b9"}, end: {name:"Graph Database"}, properties:{}},
57 | {start: {businessId:"b2"}, end: {name:"Brewery"}, properties:{}},
58 | {start: {businessId:"b2"}, end: {name:"Beer"}, properties:{}},
59 | {start: {businessId:"b1"}, end: {name:"Brewery"}, properties:{}},
60 | {start: {businessId:"b1"}, end: {name:"Beer"}, properties:{}},
61 | {start: {businessId:"b7"}, end: {name:"Ramen"}, properties:{}},
62 | {start: {businessId:"b7"}, end: {name:"Restaurant"}, properties:{}},
63 | {start: {businessId:"b8"}, end: {name:"Car Wash"}, properties:{}},
64 | {start: {businessId:"b6"}, end: {name:"Coffee"}, properties:{}},
65 | {start: {businessId:"b4"}, end: {name:"Breakfast"}, properties:{}},
66 | {start: {businessId:"b4"}, end: {name:"Deli"}, properties:{}},
67 | {start: {businessId:"b4"}, end: {name:"Cafe"}, properties:{}},
68 | {start: {businessId:"b4"}, end: {name:"Restaurant"}, properties:{}},
69 | {start: {businessId:"b4"}, end: {name:"Coffee"}, properties:{}},
70 | {start: {businessId:"b5"}, end: {name:"Library"}, properties:{}},
71 | {start: {businessId:"b3"}, end: {name:"Breakfast"}, properties:{}},
72 | {start: {businessId:"b3"}, end: {name:"Restaurant"}, properties:{}}]
73 | AS row
74 | MATCH (start:Business{businessId: row.start.businessId})
75 | MATCH (end:Category{name: row.end.name})
76 | CREATE (start)-[r:IN_CATEGORY]->(end) SET r += row.properties;
77 |
78 | UNWIND [{start: {userId:"u2"}, end: {reviewId:"r10"}, properties:{}},
79 | {start: {userId:"u4"}, end: {reviewId:"r8"}, properties:{}},
80 | {start: {userId:"u1"}, end: {reviewId:"r7"}, properties:{}},
81 | {start: {userId:"u3"}, end: {reviewId:"r2"}, properties:{}},
82 | {start: {userId:"u1"}, end: {reviewId:"r5"}, properties:{}},
83 | {start: {userId:"u2"}, end: {reviewId:"r6"}, properties:{}},
84 | {start: {userId:"u3"}, end: {reviewId:"r9"}, properties:{}},
85 | {start: {userId:"u4"}, end: {reviewId:"r3"}, properties:{}},
86 | {start: {userId:"u1"}, end: {reviewId:"r12"}, properties:{}},
87 | {start: {userId:"u3"}, end: {reviewId:"r4"}, properties:{}},
88 | {start: {userId:"u1"}, end: {reviewId:"r1"}, properties:{}},
89 | {start: {userId:"u1"}, end: {reviewId:"r11"}, properties:{}}]
90 | AS row
91 | MATCH (start:User{userId: row.start.userId})
92 | MATCH (end:Review{reviewId: row.end.reviewId})
93 | CREATE (start)-[r:WROTE]->(end) SET r += row.properties;
94 |
95 | UNWIND [{start: {reviewId:"r1"}, end: {businessId:"b1"}, properties:{}},
96 | {start: {reviewId:"r7"}, end: {businessId:"b5"}, properties:{}},
97 | {start: {reviewId:"r10"}, end: {businessId:"b2"}, properties:{}},
98 | {start: {reviewId:"r6"}, end: {businessId:"b4"}, properties:{}},
99 | {start: {reviewId:"r3"}, end: {businessId:"b2"}, properties:{}},
100 | {start: {reviewId:"r5"}, end: {businessId:"b3"}, properties:{}},
101 | {start: {reviewId:"r8"}, end: {businessId:"b6"}, properties:{}},
102 | {start: {reviewId:"r11"}, end: {businessId:"b8"}, properties:{}},
103 | {start: {reviewId:"r4"}, end: {businessId:"b3"}, properties:{}},
104 | {start: {reviewId:"r9"}, end: {businessId:"b7"}, properties:{}},
105 | {start: {reviewId:"r12"}, end: {businessId:"b9"}, properties:{}},
106 | {start: {reviewId:"r2"}, end: {businessId:"b1"}, properties:{}}]
107 | AS row
108 | MATCH (start:Review{reviewId: row.start.reviewId})
109 | MATCH (end:Business{businessId: row.end.businessId})
110 | CREATE (start)-[r:REVIEWS]->(end) SET r += row.properties;
111 |
--------------------------------------------------------------------------------
/app/neo4j/db.test.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_NEO4J_DESKTOP, driver } from "./db"
2 |
3 | describe("Neo4j database driver", () => {
4 | describe("should have Neo4j Desktop settings", () => {
5 | it("exported as DEFAULT_NEO4J_DESKTOP", () => {
6 | expect(DEFAULT_NEO4J_DESKTOP).toBeDefined()
7 | })
8 | describe("with settings defined for", () => {
9 | const { URI, USER, PASSWORD } = DEFAULT_NEO4J_DESKTOP
10 | it("the URI of the Neo4j database", () => {
11 | expect(URI).toBeDefined()
12 | })
13 | it("the user account to access the Neo4j database", () => {
14 | expect(USER).toBeDefined()
15 | })
16 | it("the password associated with the user account", () => {
17 | expect(PASSWORD).toBeDefined()
18 | })
19 | })
20 | })
21 | describe("should export a Neo4j driver that", () => {
22 | it("has been defined", () => {
23 | expect(driver).toBeDefined()
24 | })
25 | describe("has been configured with the intended settings for", () => {
26 | const expectedURI = process.env.NEO4J_URI.replace("bolt://", "")
27 | const expectedUser = process.env.NEO4J_USER
28 | const expectedPassword = process.env.NEO4J_PASSWORD
29 |
30 | const { _address, _authToken, _config } = driver()
31 |
32 | it("the URI of the Neo4j database", () => {
33 | expect(_address._stringValue).toEqual(expectedURI)
34 | })
35 | it("the user account to access the Neo4j database", () => {
36 | expect(_authToken.principal).toEqual(expectedUser)
37 | })
38 | it("the password associated with the user account", () => {
39 | expect(_authToken.credentials).toEqual(expectedPassword)
40 | })
41 | it("encryption has been defined", () => {
42 | expect(_config.encrypted).toBeDefined()
43 | })
44 | })
45 | describe("has been configured with Neo4j Desktop settings", () => {
46 | const { URI, USER, PASSWORD, isEncrypted } = DEFAULT_NEO4J_DESKTOP
47 | const expectedURI = URI.replace("bolt://", "")
48 | const expectedUser = USER
49 | const expectedPassword = PASSWORD
50 |
51 | const { _address, _authToken, _config } = driver(
52 | URI,
53 | USER,
54 | PASSWORD,
55 | isEncrypted
56 | )
57 |
58 | it("the URI of the Neo4j database", () => {
59 | expect(_address._stringValue).toEqual(expectedURI)
60 | })
61 | it("the user account to access the Neo4j database", () => {
62 | expect(_authToken.principal).toEqual(expectedUser)
63 | })
64 | it("the password associated with the user account", () => {
65 | expect(_authToken.credentials).toEqual(expectedPassword)
66 | })
67 | describe("when the neo4jEncryptedConnection parameter", () => {
68 | describe("is undefined", () => {
69 | const { _config } = driver(URI, USER, PASSWORD)
70 | it("encryption has been set to ENCRYPTION_OFF", () => {
71 | expect(_config.encrypted).toEqual("ENCRYPTION_OFF")
72 | })
73 | })
74 | describe("is set to the string value 'false'", () => {
75 | const { _config } = driver(URI, USER, PASSWORD, "false")
76 | it("encryption has been set to ENCRYPTION_OFF", () => {
77 | expect(_config.encrypted).toEqual("ENCRYPTION_OFF")
78 | })
79 | })
80 | describe("is set to the boolean value 'false'", () => {
81 | const { _config } = driver(URI, USER, PASSWORD, false)
82 | it("encryption has been set to ENCRYPTION_OFF", () => {
83 | expect(_config.encrypted).toEqual("ENCRYPTION_OFF")
84 | })
85 | })
86 | describe("is set to the string value 'true'", () => {
87 | const { _config } = driver(URI, USER, PASSWORD, "true")
88 | it("encryption has been set to ENCRYPTION_ON", () => {
89 | expect(_config.encrypted).toEqual("ENCRYPTION_ON")
90 | })
91 | })
92 | describe("is set to the boolean value 'true'", () => {
93 | const { _config } = driver(URI, USER, PASSWORD, true)
94 | it("encryption has been set to ENCRYPTION_ON", () => {
95 | expect(_config.encrypted).toEqual("ENCRYPTION_ON")
96 | })
97 | })
98 | })
99 | })
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/app/neo4j/db.ts:
--------------------------------------------------------------------------------
1 | import * as neo4j from "neo4j-driver"
2 |
3 | // Neo4j Desktop settings
4 | export const DEFAULT_NEO4J_DESKTOP = {
5 | URI: "bolt://localhost:7687",
6 | USER: "neo4j",
7 | PASSWORD: "neo4j",
8 | isEncrypted: false, // v3.5 does not use encrypted connections, but 4.0 does
9 | }
10 |
11 | // Create a configured neo4j driver instance (this doesn't start a session)
12 | export const driver = (
13 | neo4jURI = process.env.NEO4J_URI,
14 | neo4jUser = process.env.NEO4J_USER,
15 | neo4jPassword = process.env.NEO4J_PASSWORD,
16 | neo4jEncryptedConnection = process.env.NEO4J_ENCRYPTED
17 | ) => {
18 | // REMEMBER: !!('false') IS true; we need to explicitly check for a false string value
19 | const isEncrypted =
20 | !!neo4jEncryptedConnection && neo4jEncryptedConnection != "false"
21 |
22 | return neo4j.driver(neo4jURI, neo4j.auth.basic(neo4jUser, neo4jPassword), {
23 | encrypted: isEncrypted ? "ENCRYPTION_ON" : "ENCRYPTION_OFF",
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/app/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 |
3 | // Plug-ins
4 | const withPlugins = require("next-compose-plugins")
5 | const withBundleAnalyzer = require("@next/bundle-analyzer")({
6 | enabled: process.env.ANALYZE === "true",
7 | })
8 | const withImages = require("next-images")
9 |
10 | // Combine an array of plug-ins with a Next.js configuration object
11 | module.exports = withPlugins([[withImages], [withBundleAnalyzer]], {
12 | // Custom webpack configuration goes here 🤓
13 | webpack: (config, { isServer }) => {
14 | // Fixes npm packages that depend on `fs` module
15 | if (!isServer) {
16 | config.node = {
17 | fs: "empty",
18 | }
19 | }
20 |
21 | // Necessary for resolving our styles, etc
22 | config.resolve.modules.push(path.resolve("./"))
23 |
24 | // Return our custom webpack configuration
25 | return config
26 | },
27 | })
28 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-grandstack-starter",
3 | "version": "0.0.0",
4 | "description": "GRANDstack starter powered by Next.js",
5 | "keywords": [],
6 | "author": "Rob Brennan (therobbrennan.com)",
7 | "license": "ISC",
8 | "scripts": {
9 | "analyze": "ANALYZE=true npm run build",
10 | "dev": "next dev",
11 | "build": "next build",
12 | "start": "next start",
13 | "test": "jest",
14 | "test:ci": "jest --ci",
15 | "test:coverage": "jest --coverage",
16 | "test:coverage:view": "jest --coverage && open coverage/lcov-report/index.html",
17 | "test:verbose": "jest --verbose",
18 | "test:watch": "jest --watch",
19 | "test:watchAll": "jest --watchAll"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/TheRobBrennan/nextjs-grandstack-starter.git"
24 | },
25 | "bugs": {
26 | "url": "https://github.com/TheRobBrennan/nextjs-grandstack-starter/issues"
27 | },
28 | "homepage": "https://github.com/TheRobBrennan/nextjs-grandstack-starter#readme",
29 | "dependencies": {
30 | "@apollo/client": "^3.1.3",
31 | "@apollo/react-hooks": "^4.0.0",
32 | "@material-ui/core": "^4.11.0",
33 | "@material-ui/icons": "^4.9.1",
34 | "@neo4j/graphql": "^1.0.0-alpha.2",
35 | "@next/bundle-analyzer": "^9.5.2",
36 | "apollo-server-micro": "^2.16.1",
37 | "clsx": "^1.1.1",
38 | "cross-fetch": "^3.0.5",
39 | "graphql-tag": "^2.11.0",
40 | "graphql-tools": "^6.0.18",
41 | "neo4j-driver": "^4.1.1",
42 | "next": "^10.0.5",
43 | "next-compose-plugins": "^2.2.0",
44 | "next-images": "^1.4.1",
45 | "react": "^17.0.1",
46 | "react-dom": "^17.0.1",
47 | "recharts": "^1.8.5"
48 | },
49 | "devDependencies": {
50 | "@types/enzyme": "^3.10.5",
51 | "@types/jest": "^26.0.10",
52 | "@types/node": "^14.6.0",
53 | "@types/react": "^16.9.47",
54 | "apollo-server-testing": "^2.17.0",
55 | "dotenv": "^8.2.0",
56 | "enzyme": "^3.11.0",
57 | "enzyme-adapter-react-16": "^1.15.3",
58 | "jest": "^26.4.2",
59 | "node-mocks-http": "^1.9.0",
60 | "react-test-renderer": "^17.0.1",
61 | "typescript": "^4.1.3"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { ApolloProvider } from "@apollo/client"
2 | import { useApollo } from "../apollo/client"
3 |
4 | export default function App({ Component, pageProps }) {
5 | const apolloClient = useApollo(pageProps.initialApolloState)
6 |
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/app/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Document, { Html, Head, Main, NextScript } from "next/document"
3 | import { ServerStyleSheets } from "@material-ui/styles"
4 |
5 | import { GOOGLE_ANALYTICS_TRACKING_ID } from "../analytics/google/googleAnalytics"
6 |
7 | class MyDocument extends Document {
8 | render() {
9 | // Fixed missing document components as advised in https://github.com/vercel/next.js/blob/master/errors/missing-document-component.md
10 | return (
11 |
12 |
13 |
14 |
15 | {/* Global Site Tag (gtag.js) - Google Analytics */}
16 |
20 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | MyDocument.getInitialProps = async (ctx) => {
43 | // Resolution order
44 | // Source https://itnext.io/next-js-with-material-ui-7a7f6485f671
45 | //
46 | // On the server:
47 | // 1. app.getInitialProps
48 | // 2. page.getInitialProps
49 | // 3. document.getInitialProps
50 | // 4. app.render
51 | // 5. page.render
52 | // 6. document.render
53 | //
54 | // On the server with error:
55 | // 1. document.getInitialProps
56 | // 2. app.render
57 | // 3. page.render
58 | // 4. document.render
59 | //
60 | // On the client
61 | // 1. app.getInitialProps
62 | // 2. page.getInitialProps
63 | // 3. app.render
64 | // 4. page.render
65 |
66 | // Render app and page and get the context of the page with collected side effects.
67 | const sheets = new ServerStyleSheets()
68 | const originalRenderPage = ctx.renderPage
69 |
70 | ctx.renderPage = () =>
71 | originalRenderPage({
72 | enhanceApp: (App) => (props) => sheets.collect( ),
73 | })
74 |
75 | const initialProps = await Document.getInitialProps(ctx)
76 |
77 | return {
78 | ...initialProps,
79 | // Styles fragment is rendered after the app and page rendering finish.
80 | styles: [
81 |
82 | {initialProps.styles}
83 | {sheets.getStyleElement()}
84 | ,
85 | ],
86 | }
87 | }
88 |
89 | export default MyDocument
90 |
--------------------------------------------------------------------------------
/app/pages/api/graphql.ts:
--------------------------------------------------------------------------------
1 | import { ApolloServer } from "apollo-server-micro"
2 | import { driver } from "../../neo4j/db"
3 | import { augmentedSchema } from "../../apollo/schema"
4 |
5 | export const neo4jDriverInstance = driver()
6 |
7 | export const apolloServer = new ApolloServer({
8 | schema: augmentedSchema.schema,
9 | context: ({ req }) => ({ req, driver: neo4jDriverInstance }),
10 |
11 | // Disable GraphIQL in production by setting these to false
12 | introspection: true,
13 | playground: true,
14 | })
15 |
16 | // We need to disable the bodyParser so we can consume our API endpoint as a stream
17 | // https://nextjs.org/docs/api-routes/api-middlewares#custom-config
18 | export const config = {
19 | api: {
20 | bodyParser: false,
21 | },
22 | }
23 |
24 | export default apolloServer.createHandler({ path: "/api/graphql" })
25 |
--------------------------------------------------------------------------------
/app/pages/api/ping.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next"
2 |
3 | export type PingResponse = {
4 | message: string
5 | }
6 |
7 | export const handler = async (
8 | req: NextApiRequest,
9 | res: NextApiResponse
10 | ) => {
11 | res.status(200).json({
12 | message: `Back-end API is online at ${Date.now()}`,
13 | })
14 | }
15 |
16 | export default handler
17 |
--------------------------------------------------------------------------------
/app/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from "next"
2 |
3 | import Layout from "../grandstack-demo/layout/Layout"
4 | import Dashboard from "../grandstack-demo/components/Dashboard/Dashboard"
5 |
6 | const DefaultPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 | export default DefaultPage
14 |
--------------------------------------------------------------------------------
/app/public/img/grandstack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRobBrennan/nextjs-grandstack-starter-typescript/c5a2f5677e9a7e9390e9ba50432838cf5c3c9bdd/app/public/img/grandstack.png
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve"
16 | },
17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/app/types/Window.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | gtag?: any
3 | }
4 |
--------------------------------------------------------------------------------
/docker-compose-neo4j-v4.x.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | nextjs:
5 | container_name: nextjs
6 | depends_on:
7 | - neo4j
8 | ports:
9 | - 3000:3000 # Next.js application
10 | - 9229:9229 # Node.js debug port
11 | build:
12 | context: ./app
13 | dockerfile: Dockerfile
14 | volumes:
15 | - ./app:/usr/src/app
16 | # Prevent the node_modules and .next folders in the Docker container from being accidentally overwritten with our initial mapping of our local directory to /usr/src/app
17 | - /usr/src/app/node_modules
18 | - /usr/src/app/.next
19 | env_file:
20 | - ./app/.env
21 |
22 | neo4j: # Official image available at https://hub.docker.com/_/neo4j
23 | build: ./neo4j/v4.x.x
24 | container_name: "neo4j-4.x.x"
25 | ports:
26 | - 7474:7474 # HTTP
27 | - 7473:7473 # HTTPS
28 | - 7687:7687 # Bolt
29 | environment:
30 | # See https://neo4j.com/docs/operations-manual/current/docker/configuration/ for the naming convention for NEO4J settings
31 | # As an example, dbms.tx_log.rotation.size would be NEO4J_dbms_tx__log_rotation_size
32 | - NEO4J_dbms_security_procedures_unrestricted=apoc.*
33 | - NEO4J_apoc_import_file_enabled=true
34 | - NEO4J_apoc_export_file_enabled=true
35 | - NEO4J_dbms_shell_enabled=true
36 |
37 | # Please set 'dbms.allow_upgrade' to 'true' in your configuration file and try again. Detailed description: Upgrade is required to migrate store to new major version.
38 | - NEO4J_dbms_allow__upgrade=true
39 |
40 | volumes:
41 | - neo4j_v4.x.x_data:/var/lib/neo4j
42 |
43 | # Uncomment the following section if you want to persist Neo4j settings and data
44 | # volumes:
45 | # # Stores the authentication and roles for each database, as well as the actual data contents of each database instance (in graph.db folder)
46 | # - ./neo4j/tmp/data:/data
47 |
48 | # # Outputting the Neo4j logs to a place outside the container ensures we can troubleshoot any errors in Neo4j, even if the container crashes.
49 | # - ./neo4j/tmp/logs:/logs
50 |
51 | # # Binds the import directory, so we can copy CSV or other flat files into that directory for importing into Neo4j.
52 | # # Load scripts for importing that data can also be placed in this folder for us to execute.
53 | # - ./neo4j/tmp/import:/var/lib/neo4j/import
54 |
55 | # # Sets up our plugins directory. If we want to include any custom extensions or add the Neo4j APOC or graph algorithms library,
56 | # # exposing this directory simplifies the process of copying the jars for Neo4j to access.
57 | # - ./neo4j/tmp/plugins:/plugins
58 | # # See https://neo4j.com/developer/docker-run-neo4j/ for additional configuration ideas
59 |
60 | # For more information on how volumes work, please see https://docs.docker.com/storage/volumes/
61 | volumes:
62 | neo4j_v4.x.x_data:
63 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | nextjs:
5 | container_name: nextjs
6 | depends_on:
7 | - neo4j
8 | ports:
9 | - 3000:3000 # Next.js application
10 | - 9229:9229 # Node.js debug port
11 | build:
12 | context: ./app
13 | dockerfile: Dockerfile
14 | volumes:
15 | - ./app:/usr/src/app
16 | # Prevent the node_modules and .next folders in the Docker container from being accidentally overwritten with our initial mapping of our local directory to /usr/src/app
17 | - /usr/src/app/node_modules
18 | - /usr/src/app/.next
19 | env_file:
20 | - ./app/.env
21 |
22 | neo4j: # Official image available at https://hub.docker.com/_/neo4j
23 | build: ./neo4j/v3.5.x
24 | container_name: "neo4j-3.5.x"
25 | ports:
26 | - 7474:7474 # HTTP
27 | - 7473:7473 # HTTPS
28 | - 7687:7687 # Bolt
29 | environment:
30 | - NEO4J_dbms_security_procedures_unrestricted=apoc.*
31 | - NEO4J_apoc_import_file_enabled=true
32 | - NEO4J_apoc_export_file_enabled=true
33 | - NEO4J_dbms_shell_enabled=true
34 | volumes:
35 | - neo4j_v3.5.x_data:/var/lib/neo4j
36 |
37 | # Uncomment the following section if you want to persist Neo4j settings and data
38 | # volumes:
39 | # # Stores the authentication and roles for each database, as well as the actual data contents of each database instance (in graph.db folder)
40 | # - ./neo4j/tmp/data:/data
41 |
42 | # # Outputting the Neo4j logs to a place outside the container ensures we can troubleshoot any errors in Neo4j, even if the container crashes.
43 | # - ./neo4j/tmp/logs:/logs
44 |
45 | # # Binds the import directory, so we can copy CSV or other flat files into that directory for importing into Neo4j.
46 | # # Load scripts for importing that data can also be placed in this folder for us to execute.
47 | # - ./neo4j/tmp/import:/var/lib/neo4j/import
48 |
49 | # # Sets up our plugins directory. If we want to include any custom extensions or add the Neo4j APOC or graph algorithms library,
50 | # # exposing this directory simplifies the process of copying the jars for Neo4j to access.
51 | # - ./neo4j/tmp/plugins:/plugins
52 | # # See https://neo4j.com/developer/docker-run-neo4j/ for additional configuration ideas
53 |
54 | # For more information on how volumes work, please see https://docs.docker.com/storage/volumes/
55 | volumes:
56 | neo4j_v3.5.x_data:
57 |
--------------------------------------------------------------------------------
/neo4j/README.md:
--------------------------------------------------------------------------------
1 | The original [GRANDstack Starter](https://github.com/grand-stack/grand-stack-starter) contained the following notes and a great Dockerfile to use as a starting point.
2 |
3 | Both files have been included for reference as part of this project.
4 |
5 | ### Neo4j
6 |
7 | You need a Neo4j instance, e.g. a [Neo4j Sandbox](http://neo4j.com/sandbox), a local instance via [Neo4j Desktop](https://neo4j.com/download), [Docker](http://hub.docker.com/_/neo4j) or a [Neo4j instance on AWS, Azure or GCP](http://neo4j.com/developer/guide-cloud-deployment) or [Neo4j Cloud](http://neo4j.com/cloud)
8 |
9 | For schemas using the `@cypher` directive (as in this repo) via [`neo4j-graphql-js`](https://github.com/neo4j-graphql/neo4j-graphql-js), you need to have the [APOC library](https://github.com/neo4j-contrib/neo4j-apoc-procedures) installed, which should be automatic in Sandbox, Cloud and is a single click install in Neo4j Desktop. If when using the Sandbox / cloud you encounter an issue where an error similar to `Can not be converted to long: org.neo4j.kernel.impl.core.NodeProxy, Location: [object Object], Path: users` appears in the console when running the React app, try installing and using Neo4j locally instead.
10 |
11 | #### Sandbox setup
12 |
13 | A good tutorial can be found here: https://www.youtube.com/watch?v=rPC71lUhK_I
14 |
15 | #### Local setup
16 |
17 | 1. [Download Neo4j Desktop](https://neo4j.com/download/)
18 | 2. Install and open Neo4j Desktop.
19 | 3. Create a new DB by clicking "New Graph", and clicking "create local graph".
20 | 4. Set password to "letmein" (as suggested by `app/.env`), and click "Create".
21 | 5. Make sure that the default credentials in `app/.env` are used. Leave them as follows: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=letmein`
22 | 6. Click "Manage".
23 | 7. Click "Plugins".
24 | 8. Find "APOC" and click "Install".
25 | 9. Click the "play" button at the top of left the screen, which should start the server. _(screenshot 2)_
26 | 10. Wait until it says "RUNNING".
27 | 11. Proceed forward with the rest of the tutorial.
28 |
--------------------------------------------------------------------------------
/neo4j/cypher/cheatsheet.cypher:
--------------------------------------------------------------------------------
1 | // Delete everything
2 | MATCH (n) DETACH DELETE n
3 |
4 | // Create constraints to prevent User nodes from having duplicate email or username values
5 | CREATE CONSTRAINT ON ( user:User ) ASSERT user.email IS UNIQUE;
6 | CREATE CONSTRAINT ON ( user:User ) ASSERT user.username IS UNIQUE;
7 |
8 | // List constraints defined for the database
9 | CALL db.constraints();
10 |
11 | // Create a User node with the specified properties
12 | CREATE (u:User { username: 'test', email: 'test@nomail.com', password: 'testtest' })
13 |
14 | // Find User nodes
15 | MATCH (u:User) WITH { username: u.username, id: u.id, email: u.email} as User RETURN User
16 |
17 | // Turn timestamp in milliseconds into local time
18 | WITH datetime({ epochMillis: 1584796319600 }) AS dd
19 | RETURN datetime({datetime:dd, timezone:'America/Vancouver'}) AS PacificTime
20 | // 2020-03-21T06:11:59.600000000[America/Vancouver]
21 |
22 | // Find users that registered on or after 2020.03.22
23 | WITH datetime({year: 2020, month: 3, day: 22}).epochMillis AS testDate
24 | MATCH (u:User) WHERE u.registeredOn >= testDate
25 | RETURN u.id, u.email, u.username, u.firstName, u.lastName, u.registeredOn,
26 | datetime({epochmillis:u.registeredOn, timezone:'America/Vancouver'}) AS PacificTime,
27 | apoc.temporal.format(datetime({epochmillis:u.registeredOn, timezone:'America/Vancouver'}), "yyyy.MM.dd HH:mm:ss") AS registered
28 | ORDER BY u.registeredOn DESC
29 | ╒══════════════════════════════════════╤═════════════════╤════════════╤════════════════╤══════════════════════════════════════════════════╤═════════════════════╕
30 | │"u.id" │"u.email" │"u.username"│"u.registeredOn"│"PacificTime" │"registered" │
31 | ╞══════════════════════════════════════╪═════════════════╪════════════╪════════════════╪══════════════════════════════════════════════════╪═════════════════════╡
32 | │"ab776c26-85d4-462e-b674-a027d17b03d0"│"test@nomail.com"│"test" │1584868556674 │"2020-03-22T02:15:56.674000000[America/Vancouver]"│"2020.03.22 02:15:56"│
33 | └──────────────────────────────────────┴─────────────────┴────────────┴────────────────┴──────────────────────────────────────────────────┴─────────────────────┘
34 |
35 | // List all registered users
36 | MATCH (u:User)
37 | RETURN u.id, u.email, u.username, u.firstName, u.lastName, u.registeredOn,
38 | apoc.temporal.format(datetime({epochmillis:u.registeredOn, timezone:'America/Vancouver'}), "yyyy.MM.dd HH:mm:ss") AS registered
39 | ORDER BY u.registeredOn DESC
40 | ╒══════════════════════════════════════╤═════════════════╤════════════╤═════════════╤════════════╤════════════════╤═════════════════════╕
41 | │"u.id" │"u.email" │"u.username"│"u.firstName"│"u.lastName"│"u.registeredOn"│"registered" │
42 | ╞══════════════════════════════════════╪═════════════════╪════════════╪═════════════╪════════════╪════════════════╪═════════════════════╡
43 | │"74d470da-350c-455e-b437-cfe4d63e5d57"│"test@nomail.com"│"testuser" │"Rob" │"Brennan" │1584916048536 │"2020.03.22 15:27:28"│
44 | └──────────────────────────────────────┴─────────────────┴────────────┴─────────────┴────────────┴────────────────┴─────────────────────┘
45 |
46 | // Find a user by email address
47 | MATCH (u:User { email: 'test@nomail.com' })
48 | RETURN u
49 |
50 | // Delete all User nodes and relationships for a user with a specific email address
51 | MATCH (u:User { email: 'test@nomail.com' })
52 | DETACH DELETE u
53 |
--------------------------------------------------------------------------------
/neo4j/v3.5.x/Dockerfile:
--------------------------------------------------------------------------------
1 | # Official Neo4j Docker Images available at https://hub.docker.com/_/neo4j
2 | FROM neo4j:3.5.26
3 |
4 | # Install curl explicitly; it is no longer included in Neo4j base images
5 | RUN apt-get update; apt-get install curl -y
6 |
7 | # Specify the user/password for your Neo4j database
8 | ENV NEO4J_AUTH=neo4j/letmein
9 |
10 | # The APOC (Awesome Procedures On Cypher) library consists of many (about 450) procedures and functions to help with many different
11 | # tasks in areas like data integration, graph algorithms or data conversion.
12 | # https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases
13 | ENV APOC_VERSION=3.5.0.14
14 | ENV APOC_URI https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/${APOC_VERSION}/apoc-${APOC_VERSION}-all.jar
15 | RUN sh -c 'cd /var/lib/neo4j/plugins && curl -L -O "${APOC_URI}"'
16 |
17 | # We need to expose port 7474 for HTTP, port 7473 for HTTPS, and port 7687 for Bolt
18 | EXPOSE 7474 7473 7687
19 |
20 | CMD ["neo4j"]
21 |
--------------------------------------------------------------------------------
/neo4j/v4.x.x/Dockerfile:
--------------------------------------------------------------------------------
1 | # Official Neo4j Docker Images available at https://hub.docker.com/_/neo4j
2 | FROM neo4j:4.2.2
3 |
4 | # Install curl explicitly; it is no longer included in Neo4j base images
5 | RUN apt-get update; apt-get install curl -y
6 |
7 | # Specify the user/password for your Neo4j database
8 | ENV NEO4J_AUTH=neo4j/letmein
9 |
10 | # The APOC (Awesome Procedures On Cypher) library consists of many (about 450) procedures and functions to help with many different
11 | # tasks in areas like data integration, graph algorithms or data conversion.
12 | # https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases
13 | ENV APOC_VERSION=4.1.0.2
14 | ENV APOC_URI https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/${APOC_VERSION}/apoc-${APOC_VERSION}-all.jar
15 | RUN sh -c 'cd /var/lib/neo4j/plugins && curl -L -O "${APOC_URI}"'
16 |
17 | # We need to expose port 7474 for HTTP, port 7473 for HTTPS, and port 7687 for Bolt
18 | EXPOSE 7474 7473 7687
19 |
20 | CMD ["neo4j"]
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-grandstack-starter-typescript-typescript",
3 | "version": "0.0.0",
4 | "description": "Starter project for building a GRANDstack (GraphQL, React, Apollo, Neo4j Database) application using Next.js",
5 | "keywords": [],
6 | "author": "Rob Brennan (therobbrennan.com)",
7 | "license": "ISC",
8 | "scripts": {
9 | "analyze": "cd app && npm run analyze",
10 | "deploy": "vercel",
11 | "dev": "npm run docker:start",
12 | "dev:clean": "npm run docker:build && npm run docker:start",
13 | "dev:stop": "npm run docker:stop",
14 | "dev:v4": "npm run docker:start:v4.x",
15 | "dev:v4:clean": "npm run docker:build:v4.x && npm run docker:start:v4.x",
16 | "dev:v4:stop": "npm run docker:stop:v4.x",
17 | "start": "cd app && npm run dev",
18 | "test": "npm run docker:test:nextjs",
19 | "test:ci": "npm run docker:test:nextjs:ci",
20 | "test:coverage": "npm run docker:test:nextjs:coverage",
21 | "test:coverage:view": "npm run docker:test:nextjs:coverage && open app/coverage/lcov-report/index.html",
22 | "test:verbose": "npm run docker:test:nextjs:verbose",
23 | "test:watch": "cd app && npm run test:watch",
24 | "open:repo": "npm repo",
25 | "open:vercel": "open https://vercel.com",
26 | "docker:build": "docker-compose up --remove-orphans --build --force-recreate",
27 | "docker:build:v4.x": "docker-compose -f docker-compose-neo4j-v4.x.yml up --remove-orphans --build --force-recreate",
28 | "docker:start": "docker-compose up",
29 | "docker:start:v4.x": "docker-compose -f docker-compose-neo4j-v4.x.yml up",
30 | "docker:stop": "docker-compose -v down && docker system prune -f --volumes",
31 | "docker:stop:v4.x": "docker-compose -f docker-compose-neo4j-v4.x.yml -v down && docker system prune -f --volumes",
32 | "docker:test:nextjs": "docker-compose exec nextjs npm test",
33 | "docker:test:nextjs:ci": "docker-compose exec nextjs npm run test:ci",
34 | "docker:test:nextjs:coverage": "docker-compose exec nextjs npm run test:coverage",
35 | "docker:test:nextjs:verbose": "docker-compose exec nextjs npm run test:verbose",
36 | "docker:destroy:global": "docker system prune -f --volumes && docker image prune -a -f"
37 | },
38 | "repository": {
39 | "type": "git",
40 | "url": "git+https://github.com/TheRobBrennan/nextjs-grandstack-starter-typescript.git"
41 | },
42 | "bugs": {
43 | "url": "https://github.com/TheRobBrennan/nextjs-grandstack-starter-typescript/issues"
44 | },
45 | "homepage": "https://github.com/TheRobBrennan/nextjs-grandstack-starter-typescript#readme"
46 | }
47 |
--------------------------------------------------------------------------------