Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moving to upstash and new integration #22436

Merged
merged 14 commits into from
Feb 24, 2021
94 changes: 30 additions & 64 deletions examples/with-redis/README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,62 @@
# Redis Example
# Redis Example (with Upstash)

This example showcases how to use Redis as a data store in a Next.js project. [Lambda Store](https://lambda.store/) is used as managed Redis service.
This example showcases how to use Redis as a data store in a Next.js project.

The example is a basic roadmap voting application where users can enter and vote for feature requests. It features the following:
The example is a roadmap voting application where users can enter and vote for feature requests. It features the following:

- Users can add and upvote items (features in the roadmap), and enter their email addresses to be notified about the released items.
- The API records the ip-addresses of the voters, so it does not allow multiple votes on the same item from the same IP address.
- To find the id of any item, click the vote button, you will see its id on the url.

## Demo
See
[https://roadmap.upstash.com](https://roadmap.upstash.com)

## Deploy Your Own
You can deploy Roadmap Voting App for your project/company using [Vercel and Upstash](https://vercel.com/integrations/upstash) clicking the below button:

[https://roadmap-voting-demo.vercel.app/](https://roadmap-voting-demo.vercel.app/)

## Deploy your own

Once you have access to [the environment variables you'll need](#configuration), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-redis&project-name=with-redis&repository-name=with-redis&env=REDIS_URL&envDescription=Required%20to%20connect%20the%20app%20to%20Redis&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-redis%23configuration)

## How to use

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npx create-next-app --example with-redis with-redis-app
# or
yarn create next-app --example with-redis with-redis-app
```
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fupstash%2Fserverless-tutorials%2Ftree%2Fmaster%2Froadmap-voting-app&env=LOGO&envDescription=Enter%20URL%20for%20your%20project%2Fcompany%20logo&envLink=https%3A%2F%2Fdocs.upstash.com%2Fdocs%2Ftutorials%2Froadmap_voting_app&project-name=roadmap-voting&repo-name=roadmap-voting&demo-title=Roadmap%20Voting&demo-description=Roadmap%20Voting%20Page%20for%20Your%20Project&demo-url=https%3A%2F%2Froadmap.upstash.com&integration-ids=oac_V3R1GIpkoJorr6fqyiwdhl17)

## Configuration
The application uses [Upstash](https://upstash.com) (Serverless Redis Database) as its data storage. During deployment, you will be asked to integrate Upstash. The integration dialog will help you create an Upstash database for free and link it to your Vercel project with the following steps:

A data store with Redis is required for the app to work. In the steps below we'll integrate Lambda Store as the data store.

### Without Vercel

If you are planning to deploy your application to somewhere other than Vercel, you'll need to integrate Lambda Store by setting an environment variable.

First, create an account and a database in the [Lambda Store console](https://console.lambda.store/).

To connect to Redis, you will need your Redis connection string. You can get the connection string by clicking on **Connect** in the Database page within the Lambda Store dashboard as below:

![setup without vercel](./docs/lstr6.png)

Next, create a file called `.env.local` in the root directory and copy your connection string:

```bash
REDIS_URL="YOUR_REDIS_CONNECTION_STRING"
REDIS_PASSWORD="YOUR_REDIS_CONNECTION_PASSWORD"
```

Your app is now connected to a remote Redis database!

### Using Vercel
### Deployment Steps
After clicking the deploy button, enter a name for your project. Then you will be asked to install Upstash integration.
<br/>
<img src="./docs/s2.png" width="300" />
<br/>

You can add the Lambda Store integration to your Vercel account. Once you set up the integration you won't have to visit the Lambda Store console anymore. Follow the next steps to setup the integration:
You can sign up/sign in the following dialog:

#### Step 1. Deploy Your Local Project
<img src="./docs/s3.png" width="300" />

To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example).
Create a free database:

#### Step 2. Add the Lambda Store Integration to Your Vercel Account
<img src="./docs/s4.png" width="300" />

Visit Vercel [Lambda Store Integration](https://vercel.com/integrations/lambdastore) page and click the `Add` button.

#### Step 3. Configure the Integration
Select your database and the Vercel project:

The integration requires a [Developer API Key](howto/developerapi.md) that can be created from the [Lambda Store console](https://console.lambda.store).
<img src="./docs/s5.png" width="300" />

Enter the API key and your registered email address in the integration setup page:

![setup](./docs/lstr1.png)
Click `COMPLETE ON VERCEL` button:

#### Step 4. Create a Database
<img src="./docs/s6.png" width="300" />

In the next page of the integration setup, your databases will be automatically listed. A new database can be created from the Vercel Integration page as well as in the Lambda Store Console:

![new db](./docs/lstr2.png)
Finish you deployment by choosing a repository to host the project. In the next step, set the URL of your project's logo:

Click the **New Database**, you should be able to see the page below:
<img src="./docs/s7.png" width="300" />

![new db form](./docs/lstr3.png)

Fill out the form and click on **Create** to have your new database.
Your Roadmap Voting Page should be ready:

#### Step 5. Link the Database to Your Project
<img src="./docs/s8.png" width="300" />

Select your project from the dropdown menu then click on **Link To Project** for any database.

`REDIS_URL` will be automatically set as an environment variable for your application.
### Maintenance
The application uses a Redis database to store the feature requests and emails. The features requests are kept in a sorted set with name `roadmap`. You can connect to it via Redis-cli and manage the data using the command `zrange roadmap 0 1000 WITHSCORES`. The emails are stored in a set with name `emails`. So you can get the list by the command `smembers emails`.

![link project](./docs/lstr4.png)

![redis url env](./docs/lstr5.png)

**Important:** You will need to re-deploy your application for the change to be effective.
Binary file removed examples/with-redis/docs/lstr1.png
Binary file not shown.
Binary file removed examples/with-redis/docs/lstr2.png
Binary file not shown.
Binary file removed examples/with-redis/docs/lstr3.png
Binary file not shown.
Binary file removed examples/with-redis/docs/lstr4.png
Binary file not shown.
Binary file removed examples/with-redis/docs/lstr5.png
Binary file not shown.
Binary file removed examples/with-redis/docs/lstr6.png
Binary file not shown.
Binary file added examples/with-redis/docs/s1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s5.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s6.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s7.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/with-redis/docs/s8.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 7 additions & 6 deletions examples/with-redis/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
{
"name": "with-redis",
"version": "0.1.0",
"version": "0.1.1",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-toastify": "^6.0.8",
"redis": "^3.0.2",
"uuid": "^8.2.0"
"ioredis": "^4.22.0"
},
"license": "MIT"
"devDependencies": {
"prettier": "^2.2.1"
}
}
2 changes: 1 addition & 1 deletion examples/with-redis/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './index.css'
import 'react-toastify/dist/ReactToastify.css'
import '../styles/base.css'

// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }) {
Expand Down
23 changes: 8 additions & 15 deletions examples/with-redis/pages/api/addemail.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
import redis from 'redis'
import { promisify } from 'util'
import { getRedis } from './utils'

export default async function addEmail(req, res) {
const client = redis.createClient({
url: process.env.REDIS_URL,
})
if (process.env.REDIS_PASSWORD) {
client.auth(process.env.REDIS_PASSWORD)
}
const saddAsync = promisify(client.sadd).bind(client)
module.exports = async (req, res) => {
let redis = getRedis()

const body = req.body
const email = body['email']

client.on('error', function (err) {
redis.on('error', function (err) {
throw err
})

if (email && validateEmail(email)) {
await saddAsync('emails', email)
client.quit()
await redis.sadd('emails', email)
redis.quit()
res.json({
body: 'success',
})
} else {
client.quit()
redis.quit()
res.json({
error: 'Invalid email',
})
}
}

function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return re.test(String(email).toLowerCase())
}
38 changes: 13 additions & 25 deletions examples/with-redis/pages/api/create.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
import redis from 'redis'
import { promisify } from 'util'
import { v4 as uuidv4 } from 'uuid'

export default async function create(req, res) {
const client = redis.createClient({
url: process.env.REDIS_URL,
})
if (process.env.REDIS_PASSWORD) {
client.auth(process.env.REDIS_PASSWORD)
}
const hsetAsync = promisify(client.hset).bind(client)
const zaddAsync = promisify(client.zadd).bind(client)
import { getRedis } from './utils'

module.exports = async (req, res) => {
let redis = getRedis()
const body = req.body
const title = body['title']
const id = uuidv4()

client.on('error', function (err) {
throw err
})

if (title) {
await zaddAsync('roadmap', 0, id)
await hsetAsync(id, 'title', title)
client.quit()
if (!title) {
redis.quit()
res.json({
error: 'Feature can not be empty',
})
} else if (title.length < 70) {
await redis.zadd('roadmap', 'NX', 1, title)
redis.quit()
res.json({
body: 'success',
})
} else {
client.quit()
redis.quit()
res.json({
error: 'Feature can not be empty',
error: 'Max 70 characters please.',
})
}
}
35 changes: 9 additions & 26 deletions examples/with-redis/pages/api/list.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import redis from 'redis'
import { promisify } from 'util'
import { getRedis } from './utils'

export default async function list(req, res) {
const client = redis.createClient({
url: process.env.REDIS_URL,
})
if (process.env.REDIS_PASSWORD) {
client.auth(process.env.REDIS_PASSWORD)
}
const hgetallAsync = promisify(client.hgetall).bind(client)
const zrevrangeAsync = promisify(client.zrevrange).bind(client)

let n = await zrevrangeAsync('roadmap', 0, 50, 'WITHSCORES')
module.exports = async (req, res) => {
let redis = getRedis()
let n = await redis.zrevrange('roadmap', 0, 100, 'WITHSCORES')
let result = []
const promises = []
for (let i = 0; i < n.length - 1; i += 2) {
let id = n[i]
let p = hgetallAsync(id).then((item) => {
if (item) {
item['id'] = id
item['score'] = n[i + 1]
result.push(item)
}
})
promises.push(p)
let item = {}
item['title'] = n[i]
item['score'] = n[i + 1]
result.push(item)
}

await Promise.all(promises).then(() => {
client.quit()
})
redis.quit()

res.json({
body: result,
Expand Down
18 changes: 18 additions & 0 deletions examples/with-redis/pages/api/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Redis from 'ioredis'

function fixUrl(url) {
if (!url) {
return ''
}
if (url.startsWith('redis://') && !url.startsWith('redis://:')) {
return url.replace('redis://', 'redis://:')
}
if (url.startsWith('rediss://') && !url.startsWith('rediss://:')) {
return url.replace('rediss://', 'rediss://:')
}
return url
}

export function getRedis() {
return new Redis(fixUrl(process.env.REDIS_URL))
}
30 changes: 9 additions & 21 deletions examples/with-redis/pages/api/vote.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
import redis from 'redis'
import { promisify } from 'util'

export default async function list(req, res) {
const client = redis.createClient({
url: process.env.REDIS_URL,
})
if (process.env.REDIS_PASSWORD) {
client.auth(process.env.REDIS_PASSWORD)
}
client.on('error', function (err) {
throw err
})
import { getRedis } from './utils'

module.exports = async (req, res) => {
let redis = getRedis()
const body = req.body
const id = body['id']
let ip = req.headers['x-forwarded-for']
const saddAsync = promisify(client.sadd).bind(client)
let c = await saddAsync('s:' + id, ip ? ip : '-')
const title = body['title']
let ip = req.headers['x-forwarded-for'] || req.headers['Remote_Addr'] || 'NA'
let c = ip === 'NA' ? 1 : await redis.sadd('s:' + title, ip)
if (c === 0) {
client.quit()
redis.quit()
res.json({
error: 'You can not vote an item multiple times',
})
} else {
const zincrbyAsync = promisify(client.zincrby).bind(client)
let v = await zincrbyAsync('roadmap', 1, id)
client.quit()
let v = await redis.zincrby('roadmap', 1, title)
redis.quit()
res.json({
body: v,
})
Expand Down