├── .github └── workflows │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README-DEV.md ├── README.md ├── api ├── .dockerignore ├── .env ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json └── src │ ├── graphql-schema.js │ ├── index.js │ └── schema.graphql ├── app.json ├── docker-compose-stage.yml ├── docker-compose.yml ├── img ├── app-browser.jpg ├── cypress-test-runner.jpg ├── docker-desktop.jpg ├── graphql-browser.jpg ├── neo4j-browser.jpg └── vscode-extensions.jpg ├── neo4j ├── Dockerfile └── README.md ├── test ├── .dockerignore ├── .gitignore ├── Dockerfile ├── cypress.json ├── cypress │ ├── fixtures │ │ ├── example.json │ │ └── seedDb.cypher │ ├── integration │ │ └── spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js ├── package-lock.json └── package.json └── ui ├── .dockerignore ├── .env ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── apollo.js ├── client.js ├── components │ ├── Nav.svelte │ └── StarRating.svelte ├── routes │ ├── _error.svelte │ ├── _layout.svelte │ ├── about.svelte │ ├── categories │ │ ├── [name].svelte │ │ └── index.svelte │ ├── index.svelte │ └── reviews.svelte ├── server.js ├── service-worker.js └── template.html └── static ├── favicon.png ├── global.css ├── great-success.png ├── logo-192.png ├── logo-512.png └── manifest.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Cypress Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-16.04 8 | timeout-minutes: 10 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Neo4j server 12 | run: docker-compose up -d --build 13 | - name: API server 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: "12.x" 17 | - run: | 18 | npm ci 19 | npm run build 20 | npm run start & 21 | working-directory: api 22 | - name: UI server 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: "12.x" 26 | - run: | 27 | npm ci 28 | npm run build 29 | npm run start & 30 | working-directory: ui 31 | - name: Cypress run 32 | uses: cypress-io/github-action@v1 33 | with: 34 | browser: electron 35 | wait-on: http://localhost:7474 36 | # record: true 37 | working-directory: test 38 | # group: "Default tests" 39 | # env: 40 | # CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 41 | - uses: actions/upload-artifact@v1 42 | if: failure() 43 | with: 44 | name: cypress-videos 45 | path: test/cypress/videos 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | yarn.lock 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | *~ 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.associations": { 4 | "style.grass": "css" 5 | } 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 vanbenj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-DEV.md: -------------------------------------------------------------------------------- 1 | # Setting up a development environment 2 | 3 | These are instructions for setting up a MacOS machine for development. 4 | 5 | ## Homebrew 6 | 7 | We recommemd using Homebrew to aid the install process. You can find the install [here](https://brew.sh/) 8 | 9 | ## Nodejs 10 | 11 | This project uses Node version 12. If you have node installed test which version you're running. 12 | 13 | ``` 14 | node -v 15 | ``` 16 | 17 | If you're not running version 12 then unlink node. 18 | 19 | ``` 20 | brew unlink node 21 | ``` 22 | 23 | Install Node 12 the desired version: 24 | 25 | ``` 26 | brew install node@12 27 | brew link node@12 28 | ``` 29 | 30 | ## Docker 31 | 32 | When developing on the Mac it is a good idea to install [Docker Desktop](https://www.docker.com/products/docker-desktop) 33 | 34 | This greatly simplifies running Docker on the Mac. Once installed you can access the Docker Dashboard to see when containers are running. 35 | 36 | 37 | 38 | ## Vscode 39 | 40 | This was developed using Visuals Studio Code for node development. 41 | Install from [here](https://code.visualstudio.com/) 42 | 43 | In addition we recommend the extensions for Graphql, Neo4j, Svelte and Prettier. The project is configured to automatically format on save and these extensions are necessary to ensure a common formatting is used. 44 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SANDstack Starter 2 | 3 | This project is a starter for building a SANDstack ([Sveltejs](https://svelte.dev/)/[Sapper](https://sapper.svelte.dev/), [Apollo GraphQL](https://www.apollographql.com/), [Neo4j Database](https://neo4j.com/neo4j-graph-database/)) application. There are two components to the starter, the UI application (a Svelte/Sapper app) and the API app (GraphQL server). 4 | 5 | This project used as a starting point the api component from the [GRANDstack](https://grandstack.io) project and the default [Sapper template](https://github.com/sveltejs/sapper-template). 6 | 7 | The master branch **DOES NOT USE** [Svelte Apollo](https://github.com/timhall/svelte-apollo) There is a separate branch that demonstrates it's use. 8 | 9 | If you are new to Svelete this is a good [introductory video](https://youtu.be/AdNJ3fydeao) 10 | 11 | The GRANDStack documentation provides a good overview of the [Neo4j GraphQL](https://grandstack.io/docs/neo4j-graphql-overview.html) integration. 12 | 13 | ## Setting up a development environment 14 | 15 | Follow [these instructions first](./README-DEV.md) 16 | 17 | ## Quickstart 18 | 19 | You can quickly start using Docker engine version 19 or later: 20 | 21 | ``` 22 | docker-compose -f docker-compose.yml -f docker-compose-stage.yml up --build 23 | ``` 24 | 25 | List the running containers: 26 | 27 | ``` 28 | docker ps 29 | ``` 30 | 31 | There should be three containers started: 32 | 33 | ``` 34 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 35 | 625acfa444fa sand-stack-starter_ui "docker-entrypoint.s…" 3 minutes ago Up 2 minutes 0.0.0.0:3000->3000/tcp sand-stack-starter_ui_1 36 | 843c5ea8d997 sand-stack-starter_api "docker-entrypoint.s…" 3 minutes ago Up 2 minutes 0.0.0.0:4001->4001/tcp sand-stack-starter_api_1 37 | 8ae79170f66e sand-stack-starter_neo4j "/sbin/tini -g -- /d…" 3 minutes ago Up 3 minutes 0.0.0.0:7474->7474/tcp, 7473/tcp, 0.0.0.0:7687->7687/tcp sand-stack-starter_neo4j_1 38 | ``` 39 | 40 | Initially the database is empty. Running the test suite automatically loads the database with sample data. 41 | 42 | Open a new terminal 43 | 44 | ``` 45 | cd test 46 | npm install 47 | npx cypress run 48 | ``` 49 | 50 | The application should be running at [localhost:3000](http://localhost:3000) 51 | ![](img/app-browser.jpg) 52 | 53 | ## Development mode 54 | 55 | The project is set up to allow the api and ui servers to run in dev mode. This mode enables a watch on all files and automatically deploys when changes are saved. The following instructions show you how to start each server component separately. 56 | 57 | If you've run the docker-compose script be sure to shut down the docker containers before starting in development mode. 58 | 59 | ``` 60 | docker-compose down 61 | ``` 62 | 63 | Be sure your development machine is running Node version 12. 64 | 65 | ``` 66 | node --version 67 | ``` 68 | 69 | ### Neo4j 70 | 71 | There are many ways to run Neo4j for development purposes. The [README](neo4j/README.md) in the `neo4j` directory describes several approaches. However the simplest is to start a stand alone Docker container using the DockerFile provided. 72 | 73 | ``` 74 | docker-compose up --detach --remove-orphans 75 | ``` 76 | 77 | List the running containers: 78 | 79 | ``` 80 | docker ps 81 | ``` 82 | 83 | You should see a neo4j container: 84 | 85 | ``` 86 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 87 | 3c95a2947d49 neo4j "/sbin/tini -g -- /d…" 9 seconds ago Up 8 seconds 0.0.0.0:7474->7474/tcp, 7473/tcp, 0.0.0.0:7687->7687/tcp neo4j-db 88 | ``` 89 | 90 | You should now be able to access the Neo4j database browser at [localhost:7474](http://localhost:7474) (you can log in using neo4j/letmein). Click the `*()` under `Node Labels` to list a sample of nodes. 91 | ![](img/neo4j-browser.jpg) 92 | 93 | ### Install dependencies 94 | 95 | ``` 96 | (cd ../ui && npm install) 97 | (cd ../api && npm install) 98 | ``` 99 | 100 | ### Start API Server 101 | 102 | Open a new terminal 103 | 104 | ``` 105 | cd api 106 | npm run dev 107 | ``` 108 | 109 | This will start the GraphQL API server in dev mode. 110 | 111 | You should also be able to access the Apollo GraphQL playground at [localhost:4001/graphql](http://localhost:4001/graphql) This allows you to interactively test your GraphQL before using it in your application. 112 | ![](img/graphql-browser.jpg) 113 | 114 | ### Start UI Server 115 | 116 | Open a new terminal 117 | 118 | ``` 119 | cd ui 120 | npm run dev 121 | ``` 122 | 123 | This will start the Svelte Sapper app in dev mode. In dev mode when a file change is saved in the `ui` directory the web page should automatically reload with the changes. 124 | 125 | ### Start Cypress Test Server 126 | 127 | Open a new terminal 128 | 129 | ``` 130 | cd test 131 | npx cypress open 132 | ``` 133 | 134 | The test runner lets you select the browser you wish to use for testing. 135 | ![](img/cypress-test-runner.jpg) 136 | 137 | We've found as as apps get larger and more complex it is often necessay to add [cy.wait(time)](https://docs.cypress.io/api/commands/wait.html#Syntax) to make the test stable on slower CI machines. These are end to end tests so many factors can affect test performance. 138 | 139 | ### Github Continuous Integration 140 | 141 | The `/.github/workflows/test.yml` is a Github action that runs the Cypress end to end tests and stores the test results to github on every push. 142 | 143 | ### Sapper Export 144 | 145 | This simple example is a readonly app and a candidate for [Sapper Export](https://sapper.svelte.dev/docs#Exporting) 146 | 147 | For the export to run properly the `neo4j` and `api` servers must be running. 148 | 149 | Open a new terminal 150 | 151 | ``` 152 | cd ui 153 | npx sapper export 154 | ``` 155 | 156 | This will create a `/ui/__sapper__/export` folder with a production-ready build of your site. You can launch it like so: 157 | 158 | ``` 159 | npx serve __sapper__/export 160 | ``` 161 | 162 | ### Sapper Environment 163 | 164 | The `ui` app makes use of the [Sapper Environment npm](https://www.npmjs.com/package/sapper-environment). These are environment variables that are replaced at build time by `rollup.config.js` so they can be used in client code. Any environment variable with prefix `SAPPER_APP_` is for use in the client code. 165 | 166 | We make especial use of this in `/ui/src/apollo.js`. The Apollo GraphQl client is used in both the browser and SSR. In dev mode they have the same value. 167 | 168 | ``` 169 | SSR_GRAPHQL_URI=http://localhost:4001/graphql 170 | SAPPER_APP_GRAPHQL_URI=http://localhost:4001/graphql 171 | ``` 172 | 173 | However in production they are often different as the `SAPPER_APP_GRAPHQL_URI` is the public address of the api server and `SSR_GRAPHQL_URI` is the internal address of the server. An example of this can be seen in `docker-compose-stage.yml`. 174 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /api/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | NEO4J_URI=bolt://localhost:7687 3 | NEO4J_USER=neo4j 4 | NEO4J_PASSWORD=letmein 5 | GRAPHQL_LISTEN_PORT=4001 6 | GRAPHQL_URI=http://localhost:4001/graphql 7 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | RUN mkdir -p /app 4 | WORKDIR /app 5 | 6 | COPY package.json . 7 | COPY package-lock.json . 8 | RUN npm install 9 | COPY src ./src 10 | COPY .env . 11 | 12 | EXPOSE 4001 13 | 14 | CMD ["npm", "start"] 15 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # GRANDstack Starter - GraphQL API 2 | 3 | 4 | ## Quick Start 5 | 6 | Install dependencies: 7 | 8 | ``` 9 | npm install 10 | ``` 11 | 12 | Start the GraphQL service: 13 | 14 | ``` 15 | npm start 16 | ``` 17 | 18 | This will start the GraphQL service (by default on localhost:4001/graphql) where you can issue GraphQL requests or access GraphQL Playground in the browser: 19 | 20 | ![GraphQL Playground](../img/graphql-browser.png) 21 | 22 | ## Configure 23 | 24 | Set your Neo4j connection string and credentials in `.env`. For example: 25 | 26 | *.env* 27 | 28 | ``` 29 | NEO4J_URI=bolt://localhost:7687 30 | NEO4J_USER=neo4j 31 | NEO4J_PASSWORD=letmein 32 | ``` 33 | 34 | ## Seeding The Database 35 | 36 | Optionally you can seed the GraphQL service by executing mutations that will write sample data to the database: 37 | 38 | ``` 39 | npm run seedDb 40 | ``` 41 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sand-stack-starter-api", 3 | "version": "0.1.0", 4 | "description": "API app for SANDstack", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "./node_modules/.bin/nodemon --watch src --ext js,graphql --exec babel-node src/index.js", 9 | "build": "npx babel src -d build; cp .env build; cp src/schema.graphql build", 10 | "start": "npm run build && node build/index.js" 11 | }, 12 | "license": "MIT", 13 | "dependencies": { 14 | "apollo-boost": "^0.3.1", 15 | "apollo-cache-inmemory": "^1.6.5", 16 | "apollo-client": "^2.6.8", 17 | "apollo-link-http": "^1.5.17", 18 | "apollo-server": "^2.14.2", 19 | "dotenv": "^8.2.0", 20 | "graphql-tag": "^2.10.3", 21 | "neo4j-driver": "^1.7.6", 22 | "neo4j-graphql-js": "^2.13.0", 23 | "node-fetch": "^2.6.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.8.4", 27 | "@babel/core": "^7.9.0", 28 | "@babel/node": "^7.8.7", 29 | "@babel/preset-env": "^7.9.5", 30 | "nodemon": "^1.19.4", 31 | "prettier": "^1.19.1" 32 | }, 33 | "babel": { 34 | "presets": [ 35 | [ 36 | "@babel/preset-env", 37 | { 38 | "corejs": "3.1.3", 39 | "useBuiltIns": "usage" 40 | } 41 | ] 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /api/src/graphql-schema.js: -------------------------------------------------------------------------------- 1 | import { neo4jgraphql } from "neo4j-graphql-js"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | /* 6 | * Check for GRAPHQL_SCHEMA environment variable to specify schema file 7 | * fallback to schema.graphql if GRAPHQL_SCHEMA environment variable is not set 8 | */ 9 | 10 | export const typeDefs = fs 11 | .readFileSync( 12 | process.env.GRAPHQL_SCHEMA || path.join(__dirname, "schema.graphql") 13 | ) 14 | .toString("utf-8"); 15 | 16 | export const resolvers = { 17 | // add resolvers that can't be autogenerated by neo4jgraphql 18 | Query: { 19 | async hello(_, args, ctx, info) { 20 | return "Hello World!"; 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /api/src/index.js: -------------------------------------------------------------------------------- 1 | import { typeDefs, resolvers } from "./graphql-schema"; 2 | import { ApolloServer } from "apollo-server-express"; 3 | import express from "express"; 4 | import { v1 as neo4j } from "neo4j-driver"; 5 | import { makeAugmentedSchema } from "neo4j-graphql-js"; 6 | import dotenv from "dotenv"; 7 | 8 | // set environment variables from ../.env 9 | dotenv.config(); 10 | const dev = process.env.NODE_ENV === "development"; 11 | 12 | const app = express(); 13 | 14 | /* 15 | * Create an executable GraphQL schema object from GraphQL type definitions 16 | * including autogenerated queries and mutations. 17 | * Optionally a config object can be included to specify which types to include 18 | * in generated queries and/or mutations. Read more in the docs: 19 | * https://grandstack.io/docs/neo4j-graphql-js-api.html#makeaugmentedschemaoptions-graphqlschema 20 | */ 21 | 22 | const schema = makeAugmentedSchema({ 23 | typeDefs, 24 | resolvers 25 | }); 26 | 27 | /* 28 | * Create a Neo4j driver instance to connect to the database 29 | * using credentials specified as environment variables 30 | */ 31 | const driver = neo4j.driver( 32 | process.env.NEO4J_URI, 33 | neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD) 34 | ); 35 | 36 | /* 37 | * Create a new ApolloServer instance, serving the GraphQL schema 38 | * created using makeAugmentedSchema above and injecting the Neo4j driver 39 | * instance into the context object so it is available in the 40 | * generated resolvers to connect to the database. 41 | */ 42 | const server = new ApolloServer({ 43 | context: { driver }, 44 | schema: schema, 45 | introspection: dev, 46 | playground: dev 47 | }); 48 | 49 | // Specify port and path for GraphQL endpoint 50 | const port = process.env.GRAPHQL_LISTEN_PORT || 4001; 51 | const path = "/graphql"; 52 | 53 | /* 54 | * Optionally, apply Express middleware for authentication, etc 55 | * This also also allows us to specify a path for the GraphQL endpoint 56 | */ 57 | server.applyMiddleware({ app, path }); 58 | 59 | app.listen({ port, path }, () => { 60 | console.log(`GraphQL server ready at http://localhost:${port}${path}`); 61 | }); 62 | -------------------------------------------------------------------------------- /api/src/schema.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! 3 | name: String 4 | friends: [User] @relation(name: "FRIENDS", direction: "BOTH") 5 | reviews: [Review] @relation(name: "WROTE", direction: "OUT") 6 | avgStars: Float 7 | @cypher( 8 | statement: "MATCH (this)-[:WROTE] RETURN toFloat(avg(r.stars))" 9 | ) 10 | numReviews: Int 11 | @cypher(statement: "MATCH (this)-[:WROTE]->(r:Review) RETURN COUNT(r)") 12 | recommendations(first: Int = 3): [Business] @cypher(statement: "MATCH (this)-[:WROTE]->(r:Review)-[:REVIEWS]->(:Business)<-[:REVIEWS]-(:Review)<-[:WROTE]-(:User)-[:WROTE]->(:Review)-[:REVIEWS]->(rec:Business) WHERE NOT EXISTS( (this)-[:WROTE]->(:Review)-[:REVIEWS]->(rec) )WITH rec, COUNT(*) AS num ORDER BY num DESC LIMIT $first RETURN rec") 13 | } 14 | 15 | type Business { 16 | id: ID! 17 | name: String 18 | address: String 19 | city: String 20 | state: String 21 | avgStars: Float @cypher(statement: "MATCH (this)<-[:REVIEWS]-(r:Review) RETURN coalesce(avg(r.stars),0.0)") 22 | reviews: [Review] @relation(name: "REVIEWS", direction: "IN") 23 | categories: [Category] @relation(name: "IN_CATEGORY", direction: "OUT") 24 | } 25 | 26 | type Review { 27 | id: ID! 28 | stars: Int 29 | text: String 30 | date: Date 31 | business: Business @relation(name: "REVIEWS", direction: "OUT") 32 | user: User @relation(name: "WROTE", direction: "IN") 33 | } 34 | 35 | type Category { 36 | name: ID! 37 | businesses: [Business] @relation(name: "IN_CATEGORY", direction: "IN") 38 | } 39 | 40 | type Query { 41 | usersBySubstring(substring: String): [User] @cypher(statement: "MATCH (u:User) WHERE u.name CONTAINS $substring RETURN u") 42 | hello: String 43 | } 44 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sand-stack-starter", 3 | "env": { 4 | "NEO4J_URI": { 5 | "description": "The bolt endpoint for your neo4j database. For example: bolt://grandstack.io:7687", 6 | "required": true 7 | }, 8 | "NEO4J_USER": { 9 | "description": "The database user that the API application will use to connect to Neo4j", 10 | "required": true 11 | }, 12 | "NEO4J_PASSWORD": { 13 | "description": "The password for the provided Neo4j database user", 14 | "required": true 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /docker-compose-stage.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: ./api 6 | ports: 7 | - 4001:4001 8 | environment: 9 | - NODE_ENV=staging 10 | - GRAPHQL_LISTEN_PORT=4001 11 | - GRAPHQL_URI=http://api:4001/graphql 12 | - NEO4J_URI=bolt://neo4j:7687 13 | - NEO4J_USER=neo4j 14 | - NEO4J_PASSWORD=letmein 15 | links: 16 | - neo4j 17 | depends_on: 18 | - neo4j 19 | ui: 20 | build: 21 | context: ./ui 22 | args: 23 | - SAPPER_APP_GRAPHQL_URI=http://localhost:4001/graphql 24 | - NODE_ENV=staging 25 | ports: 26 | - 3000:3000 27 | environment: 28 | - NODE_ENV=staging 29 | - SSR_GRAPHQL_URI=http://api:4001/graphql 30 | links: 31 | - api 32 | depends_on: 33 | - api 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | neo4j: 5 | build: ./neo4j 6 | ports: 7 | - 7474:7474 8 | - 7687:7687 9 | environment: 10 | - NEO4J_dbms_security_procedures_unrestricted=apoc.* 11 | - NEO4J_apoc_import_file_enabled=true 12 | - NEO4J_apoc_export_file_enabled=true 13 | - NEO4J_dbms_shell_enabled=true 14 | -------------------------------------------------------------------------------- /img/app-browser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanbenj/sand-stack-starter/e18208286f9cb45a97aa33984ed20ae735eff805/img/app-browser.jpg -------------------------------------------------------------------------------- /img/cypress-test-runner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanbenj/sand-stack-starter/e18208286f9cb45a97aa33984ed20ae735eff805/img/cypress-test-runner.jpg -------------------------------------------------------------------------------- /img/docker-desktop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanbenj/sand-stack-starter/e18208286f9cb45a97aa33984ed20ae735eff805/img/docker-desktop.jpg -------------------------------------------------------------------------------- /img/graphql-browser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanbenj/sand-stack-starter/e18208286f9cb45a97aa33984ed20ae735eff805/img/graphql-browser.jpg -------------------------------------------------------------------------------- /img/neo4j-browser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanbenj/sand-stack-starter/e18208286f9cb45a97aa33984ed20ae735eff805/img/neo4j-browser.jpg -------------------------------------------------------------------------------- /img/vscode-extensions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanbenj/sand-stack-starter/e18208286f9cb45a97aa33984ed20ae735eff805/img/vscode-extensions.jpg -------------------------------------------------------------------------------- /neo4j/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM neo4j:3.5.12 2 | 3 | ENV NEO4J_AUTH=neo4j/letmein \ 4 | APOC_VERSION=3.5.0.5 \ 5 | GRAPHQL_VERSION=3.5.0.4 6 | 7 | ENV APOC_URI https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/${APOC_VERSION}/apoc-${APOC_VERSION}-all.jar 8 | RUN sh -c 'cd /var/lib/neo4j/plugins && curl -L -O "${APOC_URI}"' 9 | 10 | ENV GRAPHQL_URI https://github.com/neo4j-graphql/neo4j-graphql/releases/download/${GRAPHQL_VERSION}/neo4j-graphql-${GRAPHQL_VERSION}.jar 11 | RUN sh -c 'cd /var/lib/neo4j/plugins && curl -L -O "${GRAPHQL_URI}"' 12 | 13 | EXPOSE 7474 7473 7687 14 | 15 | CMD ["neo4j"] 16 | -------------------------------------------------------------------------------- /neo4j/README.md: -------------------------------------------------------------------------------- 1 | # Neo4j 2 | 3 | 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) 4 | 5 | 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 ui app, try installing and using Neo4j locally instead. 6 | 7 | ## Sandbox setup 8 | A good tutorial can be found here: https://www.youtube.com/watch?v=rPC71lUhK_I 9 | 10 | ## Local setup 11 | 1. [Download Neo4j Desktop](https://neo4j.com/download/) 12 | 2. Install and open Neo4j Desktop. 13 | 3. Create a new DB by clicking "New Graph", and clicking "create local graph". 14 | 4. Set password to "letmein" (as suggested by `api/.env`), and click "Create". 15 | 5. Make sure that the default credentials in `api/.env` are used. Leave them as follows: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=letmein` 16 | 6. Click "Manage". 17 | 7. Click "Plugins". 18 | 8. Find "APOC" and click "Install". 19 | 9. Click the "play" button at the top of left the screen, which should start the server. _(screenshot 2)_ 20 | 10. Wait until it says "RUNNING". 21 | 11. Proceed forward with the rest of the tutorial. 22 | 23 | -------------------------------------------------------------------------------- /test/.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /cypress/screenshots/ 4 | /cypress/videos/ 5 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cypress/base:12.1.0 2 | 3 | RUN mkdir -p /app 4 | WORKDIR /app 5 | 6 | COPY package.json . 7 | COPY package-lock.json . 8 | COPY cypress.json . 9 | COPY cypress ./cypress 10 | 11 | # by setting CI environment variable we switch the Cypress install messages 12 | # to small "started / finished" and avoid 1000s of lines of progress messages 13 | # https://github.com/cypress-io/cypress/issues/1243 14 | ENV CI=1 15 | RUN npm ci 16 | # verify that Cypress has been installed correctly. 17 | # running this command separately from "cypress run" will also cache its result 18 | # to avoid verifying again when running the tests 19 | RUN npx cypress verify -------------------------------------------------------------------------------- /test/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "video": true, 4 | "chromeWebSecurity": false, 5 | "env": { 6 | "NEO4J_URI": "bolt://localhost:7687", 7 | "NEO4J_USER": "neo4j", 8 | "NEO4J_PASSWORD": "letmein" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /test/cypress/fixtures/seedDb.cypher: -------------------------------------------------------------------------------- 1 | 2 | // this script was generated using 3 | // https://neo4j.com/docs/labs/apoc/current/export/cypher/#export-cypher-neo4j-browser 4 | // you can run the command using http://localhost:7474/browser/ 5 | // after the command is run you can use the docker shell 6 | // docker exec -it sand-stack-starter_neo4j_1 sh 7 | // to find the file in the import folder on the neo4j server 8 | CREATE CONSTRAINT ON (node:`UNIQUE IMPORT LABEL`) ASSERT (node.`UNIQUE IMPORT ID`) IS UNIQUE; 9 | UNWIND [{_id:0, properties:{name:"Will", id:"u1"}}, {_id:1, properties:{name:"Bob", id:"u2"}}, {_id:2, properties:{name:"Jenny", id:"u3"}}, {_id:3, properties:{name:"Angie", id:"u4"}}] AS row 10 | CREATE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) SET n += row.properties SET n:User; 11 | UNWIND [{_id:21, properties:{name:"Coffee"}}, {_id:22, properties:{name:"Library"}}, {_id:23, properties:{name:"Beer"}}, {_id:24, properties:{name:"Restaurant"}}, {_id:25, properties:{name:"Ramen"}}, {_id:26, properties:{name:"Cafe"}}, {_id:27, properties:{name:"Deli"}}, {_id:28, properties:{name:"Breakfast"}}, {_id:29, properties:{name:"Brewery"}}] AS row 12 | CREATE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) SET n += row.properties SET n:Category; 13 | UNWIND [{_id:30, properties:{date:date('2016-01-03'), text:"Great IPA selection!", id:"r1", stars:4}}, {_id:31, properties:{date:date('2016-07-14'), text:"I love everything", id:"r2", stars:5}}, {_id:32, properties:{date:date('2018-09-10'), text:"It's lacking something", id:"r3", stars:3}}, {_id:33, properties:{date:date('2017-11-13'), text:"Love it more", id:"r4", stars:5}}, {_id:34, properties:{date:date('2018-01-03'), text:"Best breakfast sandwich at the Farmer's Market. Always get the works.", id:"r5", stars:4}}, {_id:35, properties:{date:date('2018-03-24'), text:"Uses mostly organic food, vegan friendly", id:"r6", stars:4}}, {_id:36, 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.", id:"r7", stars:3}}, {_id:37, properties:{date:date('2018-08-11'), text:"Zebra pale ale is nice", id:"r8", stars:5}}, {_id:38, properties:{date:date('2016-11-21'), text:"Love it!!", id:"r9", stars:5}}, {_id:39, properties:{date:date('2015-12-15'), text:"Somewhat lacking in imagination", id:"r10", stars:2}}] AS row 14 | CREATE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) SET n += row.properties SET n:Review; 15 | UNWIND [{_id:4, properties:{address:"313 N 1st St W", city:"Missoula", name:"KettleHouse Brewing Co.", state:"MT", id:"b1"}}, {_id:5, properties:{address:"1151 W Broadway St", city:"Missoula", name:"Imagine Nation Brewing", state:"MT", id:"b2"}}, {_id:6, properties:{address:"Food Truck - Farmers Market", city:"Missoula", name:"Ninja Mike's", id:"b3", state:"MT"}}, {_id:7, properties:{address:"201 E Front St", city:"Missoula", name:"Market on Front", state:"MT", id:"b4"}}, {_id:8, properties:{address:"301 E Main St", city:"Missoula", name:"Missoula Public Library", state:"MT", id:"b5"}}, {_id:9, properties:{address:"121 W Broadway St", city:"Missoula", name:"Zootown Brew", id:"b6", state:"MT"}}, {_id:10, properties:{address:"723 California Dr", city:"Burlingame", name:"Hanabi", id:"b7", state:"CA"}}, {_id:11, properties:{address:"113 B St", city:"San Mateo", name:"Philz Coffee", id:"b8", state:"CA"}}, {_id:12, properties:{address:"121 Industrial Rd #11", city:"Belmont", name:"Alpha Acid Brewing Company", id:"b9", state:"CA"}}, {_id:20, properties:{address:"55 W 3rd Ave", city:"San Mateo", name:"San Mateo Public Library Central Library", state:"CA", id:"b10"}}] AS row 16 | CREATE (n:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row._id}) SET n += row.properties SET n:Business; 17 | UNWIND [{start: {_id:0}, end: {_id:30}, properties:{}}, {start: {_id:2}, end: {_id:31}, properties:{}}, {start: {_id:3}, end: {_id:32}, properties:{}}, {start: {_id:2}, end: {_id:33}, properties:{}}, {start: {_id:0}, end: {_id:34}, properties:{}}, {start: {_id:1}, end: {_id:35}, properties:{}}, {start: {_id:0}, end: {_id:36}, properties:{}}, {start: {_id:3}, end: {_id:37}, properties:{}}, {start: {_id:2}, end: {_id:38}, properties:{}}, {start: {_id:1}, end: {_id:39}, properties:{}}] AS row 18 | MATCH (start:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.start._id}) 19 | MATCH (end:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.end._id}) 20 | CREATE (start)-[r:WROTE]->(end) SET r += row.properties; 21 | UNWIND [{start: {_id:4}, end: {_id:29}, properties:{}}, {start: {_id:5}, end: {_id:23}, properties:{}}, {start: {_id:5}, end: {_id:29}, properties:{}}, {start: {_id:6}, end: {_id:24}, properties:{}}, {start: {_id:6}, end: {_id:28}, properties:{}}, {start: {_id:7}, end: {_id:21}, properties:{}}, {start: {_id:7}, end: {_id:24}, properties:{}}, {start: {_id:7}, end: {_id:26}, properties:{}}, {start: {_id:7}, end: {_id:27}, properties:{}}, {start: {_id:7}, end: {_id:28}, properties:{}}, {start: {_id:8}, end: {_id:22}, properties:{}}, {start: {_id:9}, end: {_id:21}, properties:{}}, {start: {_id:10}, end: {_id:24}, properties:{}}, {start: {_id:10}, end: {_id:25}, properties:{}}, {start: {_id:11}, end: {_id:21}, properties:{}}, {start: {_id:11}, end: {_id:28}, properties:{}}, {start: {_id:12}, end: {_id:29}, properties:{}}, {start: {_id:20}, end: {_id:22}, properties:{}}, {start: {_id:4}, end: {_id:23}, properties:{}}] AS row 22 | MATCH (start:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.start._id}) 23 | MATCH (end:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.end._id}) 24 | CREATE (start)-[r:IN_CATEGORY]->(end) SET r += row.properties; 25 | UNWIND [{start: {_id:30}, end: {_id:4}, properties:{}}, {start: {_id:31}, end: {_id:4}, properties:{}}, {start: {_id:32}, end: {_id:5}, properties:{}}, {start: {_id:33}, end: {_id:6}, properties:{}}, {start: {_id:34}, end: {_id:6}, properties:{}}, {start: {_id:35}, end: {_id:7}, properties:{}}, {start: {_id:36}, end: {_id:8}, properties:{}}, {start: {_id:37}, end: {_id:9}, properties:{}}, {start: {_id:38}, end: {_id:10}, properties:{}}, {start: {_id:39}, end: {_id:5}, properties:{}}] AS row 26 | MATCH (start:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.start._id}) 27 | MATCH (end:`UNIQUE IMPORT LABEL`{`UNIQUE IMPORT ID`: row.end._id}) 28 | CREATE (start)-[r:REVIEWS]->(end) SET r += row.properties; 29 | MATCH (n:`UNIQUE IMPORT LABEL`) WITH n LIMIT 20000 REMOVE n:`UNIQUE IMPORT LABEL` REMOVE n.`UNIQUE IMPORT ID`; 30 | DROP CONSTRAINT ON (node:`UNIQUE IMPORT LABEL`) ASSERT (node.`UNIQUE IMPORT ID`) IS UNIQUE; -------------------------------------------------------------------------------- /test/cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | describe("Sapper template app", () => { 2 | before(() => { 3 | cy.seedDb(); 4 | }); 5 | 6 | it("has the correct

", () => { 7 | cy.visit("/"); 8 | cy.contains("h1", "Great success!"); 9 | }); 10 | 11 | it("navigates to /about", () => { 12 | cy.visit("/"); 13 | cy.get("#about").click(); 14 | cy.url().should("include", "/about"); 15 | }); 16 | 17 | it("navigates to /reviews", () => { 18 | cy.visit("/"); 19 | cy.get("#reviews").click(); 20 | cy.wait(500); 21 | cy.url().should("include", "/reviews"); 22 | cy.get("ul#users").children().should("have.length", 4); 23 | }); 24 | 25 | it("load /reviews by ssr", () => { 26 | cy.visit("/reviews"); 27 | cy.wait(1000); 28 | cy.url().should("include", "/reviews"); 29 | cy.get("ul#users").children().should("have.length", 4); 30 | }); 31 | 32 | it("navigates to /categories", () => { 33 | cy.visit("/"); 34 | cy.get("#categories").click(); 35 | cy.wait(500); 36 | cy.url().should("include", "/categories"); 37 | cy.get("ul#categories li").its("length").should("eq", 9); 38 | cy.get("ul#categories li").contains("Breakfast").click(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /test/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | import { v1 as neo4j } from "neo4j-driver"; 27 | 28 | Cypress.Commands.add( 29 | "selectNth", 30 | { prevSubject: "element" }, 31 | (subject, pos) => { 32 | cy.wrap(subject) 33 | .children("option") 34 | .eq(pos) 35 | .then((e) => { 36 | cy.wrap(subject).select(e.val()); 37 | }); 38 | } 39 | ); 40 | 41 | Cypress.Commands.add("seedDb", () => { 42 | cy.fixture("seedDb.cypher").then((seedDb) => { 43 | const DELETE_ALL = "MATCH (n) DETACH DELETE n"; 44 | 45 | const escapedSeedDb = seedDb.replace(/'/g, "\\'"); 46 | const LOAD_SEED_DB = ` 47 | CALL apoc.cypher.runMany(' 48 | ${escapedSeedDb} 49 | ', 50 | {}) 51 | YIELD row, result 52 | RETURN row, result; 53 | `; 54 | console.log("Resetting database"); 55 | (async () => { 56 | const driver = neo4j.driver( 57 | Cypress.env("NEO4J_URI"), 58 | neo4j.auth.basic( 59 | Cypress.env("NEO4J_USER"), 60 | Cypress.env("NEO4J_PASSWORD") 61 | ) 62 | ); 63 | const session = driver.session(); 64 | try { 65 | await session.run(DELETE_ALL); 66 | await session.run(LOAD_SEED_DB); 67 | } finally { 68 | await session.close(); 69 | await driver.close(); 70 | } 71 | })(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "description": "Cypress tests", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "license": "ISC", 11 | "private": true, 12 | "devDependencies": { 13 | "@babel/cli": "^7.6.3", 14 | "@babel/core": "^7.6.3", 15 | "@babel/node": "^7.6.3", 16 | "@babel/preset-env": "^7.6.3", 17 | "cypress": "4.5.0", 18 | "neo4j-driver": "^1.7.6" 19 | }, 20 | "babel": { 21 | "presets": [ 22 | [ 23 | "@babel/preset-env", 24 | { 25 | "corejs": "3.1.3", 26 | "useBuiltIns": "usage" 27 | } 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /ui/.env: -------------------------------------------------------------------------------- 1 | SSR_GRAPHQL_URI=http://localhost:4001/graphql 2 | SAPPER_APP_GRAPHQL_URI=http://localhost:4001/graphql 3 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: [ 7 | 'standard' 8 | ], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly' 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2019, 15 | sourceType: 'module' 16 | }, 17 | plugins: [ 18 | 'svelte3', 19 | 'prettier', 20 | 'json' 21 | ], 22 | overrides: [ 23 | { 24 | files: ['**/*.svelte'], 25 | processor: 'svelte3/svelte3', 26 | }, 27 | ], 28 | rules: { 29 | 'camelcase': 0, 30 | 'comma-dangle': 0, 31 | 'linebreak-style': ['error', 'unix'], 32 | 'space-before-function-paren': 0, 33 | 'prettier/prettier': ['error', { 34 | trailingComma: 'es5', 35 | semi: false, 36 | singleQuote: true, 37 | bracketSpacing: true, 38 | printWidth: 80, 39 | tabWidth: 2, 40 | useTabs: false 41 | }] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /src/node_modules/@sapper/ 4 | yarn-error.log 5 | /cypress/screenshots/ 6 | /__sapper__/ 7 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | RUN mkdir -p /app 4 | WORKDIR /app 5 | 6 | ARG SAPPER_APP_GRAPHQL_URI 7 | ARG NODE_ENV 8 | 9 | # set build time environment 10 | ENV SAPPER_APP_GRAPHQL_URI $SAPPER_APP_GRAPHQL_URI 11 | ENV NODE_ENV $NODE_ENV 12 | 13 | COPY package.json . 14 | COPY package-lock.json . 15 | # we need --production=false so dev dependencies are installed for Sapper build 16 | RUN npm --production=false install 17 | COPY . . 18 | RUN export NODE_OPTIONS=--max_old_space_size=2048 \ 19 | && export SAPPER_APP_TIMESTAMP=$(date +%s%3N) \ 20 | && npm run build 21 | 22 | EXPOSE 3000 23 | 24 | CMD ["node", "__sapper__/build"] 25 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # sapper-template 2 | 3 | This component is based on the default [Sapper template project](https://github.com/sveltejs/sapper-template). 4 | 5 | 6 | ## Getting started 7 | 8 | ### Running the project 9 | 10 | However you get the code, you can install dependencies and run the project in development mode with: 11 | 12 | ```bash 13 | cd ui 14 | npm install # or yarn 15 | npm run dev 16 | ``` 17 | 18 | Open up [localhost:3000](http://localhost:3000) and start clicking around. 19 | 20 | Consult [sapper.svelte.dev](https://sapper.svelte.dev) for help getting started. 21 | 22 | 23 | ## Structure 24 | 25 | Sapper expects to find two directories in the root of your project — `src` and `static`. 26 | 27 | 28 | ### src 29 | 30 | The [src](src) directory contains the entry points for your app — `client.js`, `server.js` and (optionally) a `service-worker.js` — along with a `template.html` file and a `routes` directory. 31 | 32 | 33 | #### src/routes 34 | 35 | This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*. 36 | 37 | **Pages** are Svelte components written in `.svelte` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.) 38 | 39 | **Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example. 40 | 41 | There are three simple rules for naming the files that define your routes: 42 | 43 | * A file called `src/routes/about.svelte` corresponds to the `/about` route. A file called `src/routes/blog/[slug].svelte` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route 44 | * The file `src/routes/index.svelte` (or `src/routes/index.js`) corresponds to the root of your app. `src/routes/about/index.svelte` is treated the same as `src/routes/about.svelte`. 45 | * Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `src/routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route 46 | 47 | 48 | ### static 49 | 50 | The [static](static) directory contains any static assets that should be available. These are served using [sirv](https://github.com/lukeed/sirv). 51 | 52 | In your [service-worker.js](src/service-worker.js) file, you can import these as `files` from the generated manifest... 53 | 54 | ```js 55 | import { files } from '@sapper/service-worker'; 56 | ``` 57 | 58 | ...so that you can cache them (though you can choose not to, for example if you don't want to cache very large files). 59 | 60 | 61 | ## Bundler config 62 | 63 | Sapper uses Rollup or webpack to provide code-splitting and dynamic imports, as well as compiling your Svelte components. With webpack, it also provides hot module reloading. As long as you don't do anything daft, you can edit the configuration files to add whatever plugins you'd like. 64 | 65 | 66 | ## Production mode and deployment 67 | 68 | To start a production version of your app, run `npm run build && npm start`. This will disable live reloading, and activate the appropriate bundler plugins. 69 | 70 | You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands: 71 | 72 | ```bash 73 | npm install -g now 74 | now 75 | ``` 76 | 77 | 78 | ## Using external components 79 | 80 | When using Svelte components installed from npm, such as [@sveltejs/svelte-virtual-list](https://github.com/sveltejs/svelte-virtual-list), Svelte needs the original component source (rather than any precompiled JavaScript that ships with the component). This allows the component to be rendered server-side, and also keeps your client-side app smaller. 81 | 82 | Because of that, it's essential that the bundler doesn't treat the package as an *external dependency*. You can either modify the `external` option under `server` in [rollup.config.js](rollup.config.js) or the `externals` option in [webpack.config.js](webpack.config.js), or simply install the package to `devDependencies` rather than `dependencies`, which will cause it to get bundled (and therefore compiled) with your app: 83 | 84 | ```bash 85 | npm install -D @sveltejs/svelte-virtual-list 86 | ``` 87 | 88 | 89 | ## Bugs and feedback 90 | 91 | Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues). -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sand-stack-starter-ui", 3 | "version": "0.0.1", 4 | "description": "UI app for SANDstack", 5 | "scripts": { 6 | "dev": "export SAPPER_APP_TIMESTAMP=$(date +%s%3N) && sapper dev", 7 | "build": "export SAPPER_APP_TIMESTAMP=$(date +%s%3N) && sapper build --legacy", 8 | "export": "sapper export --legacy --concurrent=1 --timeout=60000", 9 | "start": "node __sapper__/build", 10 | "cy:run": "cypress run", 11 | "cy:open": "cypress open", 12 | "test": "run-p --race start cy:run" 13 | }, 14 | "browserslist": [ 15 | ">0.2%", 16 | "not dead", 17 | "not ie <= 11", 18 | "not op_mini all" 19 | ], 20 | "private": true, 21 | "license": "UNLICENSED", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/vanbenj/HouseParty.git" 25 | }, 26 | "dependencies": { 27 | "compression": "^1.7.1", 28 | "polka": "next", 29 | "sirv": "^0.4.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.9.0", 33 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 34 | "@babel/plugin-transform-runtime": "^7.9.0", 35 | "@babel/preset-env": "^7.9.0", 36 | "@babel/runtime": "^7.9.2", 37 | "@rollup/plugin-commonjs": "^11.0.0", 38 | "@rollup/plugin-node-resolve": "^7.0.0", 39 | "@rollup/plugin-replace": "^2.3.1", 40 | "apollo-boost": "^0.4.7", 41 | "apollo-cache-inmemory": "^1.6.5", 42 | "apollo-client": "^2.6.8", 43 | "autoprefixer": "^9.7.4", 44 | "devalue": "^2.0.1", 45 | "dotenv": "^8.2.0", 46 | "eslint": "^6.8.0", 47 | "eslint-config-prettier": "^6.10.0", 48 | "eslint-config-standard": "^14.1.1", 49 | "eslint-plugin-import": "^2.20.1", 50 | "eslint-plugin-json": "^1.4.0", 51 | "eslint-plugin-node": "^10.0.0", 52 | "eslint-plugin-prettier": "^3.1.2", 53 | "eslint-plugin-promise": "^4.2.1", 54 | "eslint-plugin-standard": "^4.0.1", 55 | "eslint-plugin-svelte3": "^2.7.3", 56 | "graphql": "^14.6.0", 57 | "node-fetch": "^2.6.0", 58 | "node-sass": "^4.13.1", 59 | "npm-run-all": "^4.1.5", 60 | "prettier": "^1.19.1", 61 | "prettier-plugin-svelte": "^0.7.0", 62 | "rollup": "^1.32.1", 63 | "rollup-plugin-babel": "^4.4.0", 64 | "rollup-plugin-json": "^4.0.0", 65 | "rollup-plugin-svelte": "^5.0.1", 66 | "rollup-plugin-terser": "^5.3.0", 67 | "sapper": "^0.27.11", 68 | "svelte": "^3.20.1", 69 | "sapper-environment": "1.0.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ui/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import replace from "@rollup/plugin-replace"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import svelte from "rollup-plugin-svelte"; 5 | import babel from "rollup-plugin-babel"; 6 | import { terser } from "rollup-plugin-terser"; 7 | import config from "sapper/config/rollup.js"; 8 | import pkg from "./package.json"; 9 | import sapperEnv from "sapper-environment"; 10 | 11 | const mode = process.env.NODE_ENV; 12 | const dev = mode === "development"; 13 | const legacy = !!process.env.SAPPER_LEGACY_BUILD; 14 | 15 | const onwarn = (warning, onwarn) => 16 | (warning.code === "CIRCULAR_DEPENDENCY" && 17 | /[/\\]@sapper[/\\]/.test(warning.message)) || 18 | onwarn(warning); 19 | 20 | export default { 21 | client: { 22 | input: config.client.input(), 23 | output: config.client.output(), 24 | plugins: [ 25 | replace({ 26 | ...sapperEnv(), 27 | "process.browser": true, 28 | "process.env.NODE_ENV": JSON.stringify(mode) 29 | }), 30 | svelte({ 31 | dev, 32 | hydratable: true, 33 | emitCss: true 34 | }), 35 | resolve({ 36 | browser: true, 37 | dedupe: ["svelte"] 38 | }), 39 | commonjs(), 40 | 41 | legacy && 42 | babel({ 43 | extensions: [".js", ".mjs", ".html", ".svelte"], 44 | runtimeHelpers: true, 45 | exclude: ["node_modules/@babel/**"], 46 | presets: [ 47 | [ 48 | "@babel/preset-env", 49 | { 50 | targets: "> 0.25%, not dead" 51 | } 52 | ] 53 | ], 54 | plugins: [ 55 | "@babel/plugin-syntax-dynamic-import", 56 | [ 57 | "@babel/plugin-transform-runtime", 58 | { 59 | useESModules: true 60 | } 61 | ] 62 | ] 63 | }), 64 | 65 | !dev && 66 | terser({ 67 | module: true 68 | }) 69 | ], 70 | 71 | onwarn 72 | }, 73 | 74 | server: { 75 | input: config.server.input(), 76 | output: config.server.output(), 77 | plugins: [ 78 | replace({ 79 | ...sapperEnv(), 80 | "process.browser": false, 81 | "process.env.NODE_ENV": JSON.stringify(mode) 82 | }), 83 | svelte({ 84 | generate: "ssr", 85 | dev 86 | }), 87 | resolve({ 88 | dedupe: ["svelte"] 89 | }), 90 | commonjs() 91 | ], 92 | external: Object.keys(pkg.dependencies).concat( 93 | require("module").builtinModules || 94 | Object.keys(process.binding("natives")) 95 | ), 96 | 97 | onwarn 98 | }, 99 | 100 | serviceworker: { 101 | input: config.serviceworker.input(), 102 | output: config.serviceworker.output(), 103 | plugins: [ 104 | resolve(), 105 | replace({ 106 | ...sapperEnv(), 107 | "process.browser": true, 108 | "process.env.NODE_ENV": JSON.stringify(mode) 109 | }), 110 | commonjs(), 111 | !dev && terser() 112 | ], 113 | 114 | onwarn 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /ui/src/apollo.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { ApolloClient } from "apollo-boost"; 3 | import { InMemoryCache } from "apollo-cache-inmemory"; 4 | import { HttpLink } from "apollo-link-http"; 5 | 6 | let defaultOptions; 7 | let graphqlUri; 8 | 9 | // this code can be run on either browser or ssr 10 | // when location is defined we're 11 | // running in the browser 12 | if (typeof location !== "undefined") { 13 | graphqlUri = process.env.SAPPER_APP_GRAPHQL_URI; 14 | defaultOptions = {}; 15 | } else { 16 | require("dotenv").config(); 17 | graphqlUri = process.env.SSR_GRAPHQL_URI; 18 | defaultOptions = { 19 | watchQuery: { 20 | fetchPolicy: "no-cache" 21 | }, 22 | query: { 23 | fetchPolicy: "no-cache" 24 | } 25 | }; 26 | } 27 | console.log(`Init ApolloClient ${graphqlUri}`); 28 | 29 | const client = new ApolloClient({ 30 | cache: new InMemoryCache(), 31 | link: new HttpLink({ 32 | uri: graphqlUri, 33 | fetch 34 | }), 35 | defaultOptions 36 | }); 37 | 38 | export default client; 39 | -------------------------------------------------------------------------------- /ui/src/client.js: -------------------------------------------------------------------------------- 1 | import * as sapper from "@sapper/app"; 2 | 3 | sapper.start({ 4 | target: document.querySelector("#sapper") 5 | }); 6 | -------------------------------------------------------------------------------- /ui/src/components/Nav.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 50 | 51 | 87 | -------------------------------------------------------------------------------- /ui/src/components/StarRating.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 24 | 25 |

★★★★★

26 | -------------------------------------------------------------------------------- /ui/src/routes/_error.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 32 | {status} 33 | 34 | 35 |

{status}

36 | 37 |

{error.message}

38 | 39 | {#if dev && error.stack} 40 |
{error.stack}
41 | {/if} 42 | -------------------------------------------------------------------------------- /ui/src/routes/_layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 |