Skip to content

Commit a5e144d

Browse files
andy-k-improvingacarbonettoplmrry
authored
feat: Add Neptune Analytics example (#1262)
### Description Introduce a new example to showcase how Neptune Analytics service can be used to build a next.js backend service with OpenCypher query. ### Type of Change - [x] New Example ### Demo URL Sample demo page looks like this: <img width="1123" height="757" alt="505380612-f0a848c8-2ec4-4cfb-b9dc-089bc564f8d9" src="https://github.com/user-attachments/assets/631232e4-9de3-4970-96fd-9a96d50764dd" /> Create node: <img width="1060" height="730" alt="505386395-99e29036-262e-47d2-bede-ff75ccbe7d4f" src="https://github.com/user-attachments/assets/5a3b7573-ceda-45d6-976f-c4f6d658c09b" /> Create edge: <img width="1081" height="604" alt="505386789-fe6f63ba-5402-4bc7-b24a-70c3da024626" src="https://github.com/user-attachments/assets/4a8571ac-f1c9-4a79-a1fa-0ccc2c67d1d3" /> ### New Example Checklist - [x] 🛫 `npm run new-example` was used to create the example --------- Signed-off-by: Andy Kwok <andy.kwok@improving.com> Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com> Co-authored-by: Andrew Carbonetto <andrew.carbonetto@improving.com> Co-authored-by: Paul Murray <paul.murray@vercel.com>
1 parent 8f3e300 commit a5e144d

File tree

16 files changed

+5879
-0
lines changed

16 files changed

+5879
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
AWS_ACCESS_KEY_ID=
2+
AWS_SECRET_ACCESS_KEY=
3+
AWS_REGION=us-west-2
4+
GRAPH_ID=neptune-analytics-graph-id
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"root": true,
3+
"extends": "next/core-web-vitals"
4+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# Dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# Testing
9+
/coverage
10+
11+
# Next.js
12+
/.next/
13+
/out/
14+
next-env.d.ts
15+
16+
# Production
17+
build
18+
dist
19+
20+
# Misc
21+
.DS_Store
22+
*.pem
23+
24+
# Debug
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
28+
29+
# Local ENV files
30+
.env.local
31+
.env.development.local
32+
.env.test.local
33+
.env.production.local
34+
35+
# Vercel
36+
.vercel
37+
38+
# Turborepo
39+
.turbo
40+
41+
# typescript
42+
*.tsbuildinfo
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
name: AWS Neptune Analytics with Next.js API Routes
3+
slug: aws-neptune-analytics-nextjs-api-routes
4+
description: Learn to use AWS Neptune Analytics with Next.js API Routes for graph database operations.
5+
framework: Next.js
6+
deployUrl: https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/solutions/aws-neptune-analytics&project-name=aws-neptune-analytics&repository-name=aws-neptune-analytics&env=GRAPH_ID&envDescription=AWS%20Neptune%20Analytics%20Graph%20ID
7+
---
8+
9+
# Next.js + AWS Neptune Analytics
10+
11+
This is an example of a Next.js application using AWS Neptune Analytics for creating, reading, updating, and deleting graph nodes and edges with OpenCypher queries.
12+
13+
## How to Use
14+
15+
### **Option 1: Use an existing Neptune Analytics graph.**
16+
17+
Retrieve your existing graph ID and ensure proper AWS credentials are configured. Provide the graph ID after clicking "Deploy" to automatically set the environment variable.
18+
19+
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/solutions/aws-neptune-analytics&project-name=aws-neptune-analytics&repository-name=aws-neptune-analytics&env=GRAPH_ID&envDescription=AWS%20Neptune%20Analytics%20Graph%20ID)
20+
21+
### **Option 2: Create a new Neptune Analytics graph.**
22+
23+
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [pnpm](https://pnpm.io/installation) to bootstrap the example:
24+
25+
```bash
26+
pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/aws-neptune-analytics
27+
```
28+
29+
1. Create a new [IAM role](https://aws.amazon.com/iam/) that includes permissions `neptune-graph:ReadDataViaQuery`, `neptune-graph:WriteDataViaQuery` and `neptune-graph:DeleteDataViaQuery`
30+
2. Save the access key and secret key or configure AWS credentials (see [AWS CLI configuration guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) for details).
31+
3. Create a new Neptune Analytics graph on AWS console
32+
In the Neptune Analytics Console, create a new graph with public endpoint enabled and 16 NCUs to start.
33+
4. Save the graph ID from the Neptune Analytics console
34+
5. Create an `.env.local` file and add your graph ID:
35+
```
36+
GRAPH_ID=your-graph-id-here
37+
```
38+
Alternatively, you can set it directly in your terminal:
39+
```
40+
export GRAPH_ID=your-graph-id-here
41+
```
42+
6. Run `pnpm dev` to start the Next app at http://localhost:3000
43+
44+
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=edge-middleware-eap) ([Documentation](https://nextjs.org/docs/deployment)).
45+
46+
## Credentials and Environment Variables
47+
48+
AWS credentials (e.g. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) and region configuration (e.g. `AWS_REGION`) can be used directly as environment variables for Vercel deployments.
49+
50+
The AWS SDK will automatically pick up these credentials from the environment:
51+
52+
```js
53+
const client = new NeptuneGraphClient({})
54+
```
55+
56+
## API Endpoints
57+
58+
The application provides a RESTful API for graph node and edge operations:
59+
60+
### Node Operations
61+
62+
- `GET /api/node?id={id}` - Retrieve a node by ID
63+
- `POST /api/node` - Create a new node
64+
- `PUT /api/node` - Update an existing node
65+
- `DELETE /api/node?id={id}` - Delete a node and its relationships
66+
67+
### Edge Operations
68+
69+
- `GET /api/edge?id={id}` - Retrieve an edge by ID
70+
- `POST /api/edge` - Create a new edge
71+
- `PUT /api/edge` - Update an existing edge
72+
- `DELETE /api/edge?id={id}` - Delete an edge
73+
74+
## Testing
75+
76+
### Create Node (POST)
77+
78+
```bash
79+
curl -X POST http://localhost:3000/api/node \
80+
-d '{"id": "user-123", "name": "John Doe", "type": "user"}' \
81+
-H "Content-type: application/json"
82+
```
83+
84+
### Get Node (GET)
85+
86+
```bash
87+
curl "http://localhost:3000/api/node?id=user-123"
88+
```
89+
90+
### Update Node (PUT)
91+
92+
```bash
93+
curl -X PUT http://localhost:3000/api/node \
94+
-d '{"id": "user-123", "name": "John Smith", "type": "user", "active": true}' \
95+
-H "Content-type: application/json"
96+
```
97+
98+
### Delete Node (DELETE)
99+
100+
```bash
101+
curl -X DELETE "http://localhost:3000/api/node?id=user-123"
102+
```
103+
104+
### Create Edge (POST)
105+
106+
```bash
107+
curl -X POST http://localhost:3000/api/edge \
108+
-d '{"fromId": "user-123", "toId": "user-456", "type": "FOLLOWS"}' \
109+
-H "Content-type: application/json"
110+
```
111+
112+
### Get Edge (GET)
113+
114+
```bash
115+
curl "http://localhost:3000/api/edge?id=follows-001"
116+
```
117+
118+
### Update Edge (PUT)
119+
120+
```bash
121+
curl -X PUT http://localhost:3000/api/edge \
122+
-d '{"id": "follows-001", "since": "2024-01-15", "strength": "strong"}' \
123+
-H "Content-type: application/json"
124+
```
125+
126+
### Delete Edge (DELETE)
127+
128+
```bash
129+
curl -X DELETE "http://localhost:3000/api/edge?id=follows-001"
130+
```
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { NextResponse } from 'next/server'
2+
import * as NG from '@aws-sdk/client-neptune-graph'
3+
4+
const client = new NG.NeptuneGraphClient({})
5+
const GRAPH_ID = process.env.GRAPH_ID
6+
7+
if (!GRAPH_ID) {
8+
throw new Error('GRAPH_ID environment variable is required')
9+
}
10+
11+
/**
12+
* Execute Neptune Analytics query with parameters
13+
*/
14+
async function executeQuery(
15+
queryString: string,
16+
parameters: Record<string, any>
17+
) {
18+
const input = {
19+
graphIdentifier: GRAPH_ID,
20+
queryString,
21+
language: NG.QueryLanguage.OPEN_CYPHER,
22+
parameters,
23+
}
24+
25+
const cmd = new NG.ExecuteQueryCommand(input)
26+
const response = await client.send(cmd)
27+
const responseStr = await response.payload.transformToString()
28+
return JSON.parse(responseStr)
29+
}
30+
31+
/**
32+
* Handle errors with consistent logging and response format
33+
*/
34+
function handleError(error: any, method: string) {
35+
console.error(`${method} /api/edge error:`, error)
36+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
37+
}
38+
39+
/**
40+
* Validate ID parameter from query string
41+
*/
42+
function validateId(id: string | null) {
43+
if (!id) {
44+
return NextResponse.json(
45+
{ error: 'id parameter is required' },
46+
{ status: 400 }
47+
)
48+
}
49+
return null
50+
}
51+
52+
/**
53+
* Validate request body contains required fields for edge creation
54+
*/
55+
function validateEdgeBody(body: any) {
56+
if (!body?.fromId || !body?.toId || !body?.type) {
57+
return NextResponse.json(
58+
{ error: 'Request body with fromId, toId, and type is required' },
59+
{ status: 400 }
60+
)
61+
}
62+
return null
63+
}
64+
65+
export async function GET(request: Request) {
66+
try {
67+
const { searchParams } = new URL(request.url)
68+
const id = searchParams.get('id')
69+
70+
const error = validateId(id)
71+
if (error) return error
72+
73+
const result = await executeQuery(
74+
'MATCH()-[r]->() WHERE id(r) = $EDGE_ID RETURN r',
75+
{ EDGE_ID: id }
76+
)
77+
78+
return NextResponse.json(result)
79+
} catch (error) {
80+
return handleError(error, 'GET')
81+
}
82+
}
83+
84+
export async function POST(request: Request) {
85+
try {
86+
const body = await request.json()
87+
88+
const error = validateEdgeBody(body)
89+
if (error) return error
90+
91+
const { fromId, toId, type, ...properties } = body
92+
93+
const result = await executeQuery(
94+
'MATCH (from), (to) WHERE id(from) = $FROM_ID AND id(to) = $TO_ID CREATE (from)-[r:' +
95+
type +
96+
']->(to) SET r += $PROPERTIES RETURN r',
97+
{ FROM_ID: fromId, TO_ID: toId, PROPERTIES: properties }
98+
)
99+
100+
return NextResponse.json(result, { status: 201 })
101+
} catch (error) {
102+
return handleError(error, 'POST')
103+
}
104+
}
105+
106+
export async function PUT(request: Request) {
107+
try {
108+
const body = await request.json()
109+
110+
if (!body?.id) {
111+
return NextResponse.json(
112+
{ error: 'Request body with id is required' },
113+
{ status: 400 }
114+
)
115+
}
116+
117+
const { id, ...properties } = body
118+
119+
const result = await executeQuery(
120+
'MATCH()-[r]->() WHERE id(r) = $EDGE_ID SET r = $PROPERTIES RETURN r',
121+
{ EDGE_ID: id, PROPERTIES: properties }
122+
)
123+
124+
return NextResponse.json(result)
125+
} catch (error) {
126+
return handleError(error, 'PUT')
127+
}
128+
}
129+
130+
export async function DELETE(request: Request) {
131+
try {
132+
const { searchParams } = new URL(request.url)
133+
const id = searchParams.get('id')
134+
135+
const error = validateId(id)
136+
if (error) return error
137+
138+
const result = await executeQuery(
139+
'MATCH()-[r]->() WHERE id(r) = $EDGE_ID DELETE r',
140+
{ EDGE_ID: id }
141+
)
142+
143+
return NextResponse.json(result)
144+
} catch (error) {
145+
return handleError(error, 'DELETE')
146+
}
147+
}

0 commit comments

Comments
 (0)