Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions solutions/aws-neptune-analytics/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-west-2
GRAPH_ID=neptune-analytics-graph-id
4 changes: 4 additions & 0 deletions solutions/aws-neptune-analytics/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": true,
"extends": "next/core-web-vitals"
}
42 changes: 42 additions & 0 deletions solutions/aws-neptune-analytics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
/node_modules
/.pnp
.pnp.js

# Testing
/coverage

# Next.js
/.next/
/out/
next-env.d.ts

# Production
build
dist

# Misc
.DS_Store
*.pem

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local ENV files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Turborepo
.turbo

# typescript
*.tsbuildinfo
130 changes: 130 additions & 0 deletions solutions/aws-neptune-analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
name: AWS Neptune Analytics with Next.js API Routes
slug: aws-neptune-analytics-nextjs-api-routes
description: Learn to use AWS Neptune Analytics with Next.js API Routes for graph database operations.
framework: Next.js
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
---

# Next.js + AWS Neptune Analytics

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.

## How to Use

### **Option 1: Use an existing Neptune Analytics graph.**

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.

[![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)

### **Option 2: Create a new Neptune Analytics graph.**

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:

```bash
pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/aws-neptune-analytics
```

1. Create a new [IAM role](https://aws.amazon.com/iam/) that includes permissions `neptune-graph:ReadDataViaQuery`, `neptune-graph:WriteDataViaQuery` and `neptune-graph:DeleteDataViaQuery`
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).
3. Create a new Neptune Analytics graph on AWS console
In the Neptune Analytics Console, create a new graph with public endpoint enabled and 16 NCUs to start.
4. Save the graph ID from the Neptune Analytics console
5. Create an `.env.local` file and add your graph ID:
```
GRAPH_ID=your-graph-id-here
```
Alternatively, you can set it directly in your terminal:
```
export GRAPH_ID=your-graph-id-here
```
6. Run `pnpm dev` to start the Next app at http://localhost:3000

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)).

## Credentials and Environment Variables

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.

The AWS SDK will automatically pick up these credentials from the environment:

```js
const client = new NeptuneGraphClient({})
```

## API Endpoints

The application provides a RESTful API for graph node and edge operations:

### Node Operations

- `GET /api/node?id={id}` - Retrieve a node by ID
- `POST /api/node` - Create a new node
- `PUT /api/node` - Update an existing node
- `DELETE /api/node?id={id}` - Delete a node and its relationships

### Edge Operations

- `GET /api/edge?id={id}` - Retrieve an edge by ID
- `POST /api/edge` - Create a new edge
- `PUT /api/edge` - Update an existing edge
- `DELETE /api/edge?id={id}` - Delete an edge

## Testing

### Create Node (POST)

```bash
curl -X POST http://localhost:3000/api/node \
-d '{"id": "user-123", "name": "John Doe", "type": "user"}' \
-H "Content-type: application/json"
```

### Get Node (GET)

```bash
curl "http://localhost:3000/api/node?id=user-123"
```

### Update Node (PUT)

```bash
curl -X PUT http://localhost:3000/api/node \
-d '{"id": "user-123", "name": "John Smith", "type": "user", "active": true}' \
-H "Content-type: application/json"
```

### Delete Node (DELETE)

```bash
curl -X DELETE "http://localhost:3000/api/node?id=user-123"
```

### Create Edge (POST)

```bash
curl -X POST http://localhost:3000/api/edge \
-d '{"fromId": "user-123", "toId": "user-456", "type": "FOLLOWS"}' \
-H "Content-type: application/json"
```

### Get Edge (GET)

```bash
curl "http://localhost:3000/api/edge?id=follows-001"
```

### Update Edge (PUT)

```bash
curl -X PUT http://localhost:3000/api/edge \
-d '{"id": "follows-001", "since": "2024-01-15", "strength": "strong"}' \
-H "Content-type: application/json"
```

### Delete Edge (DELETE)

```bash
curl -X DELETE "http://localhost:3000/api/edge?id=follows-001"
```
147 changes: 147 additions & 0 deletions solutions/aws-neptune-analytics/app/api/edge/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { NextResponse } from 'next/server'
import * as NG from '@aws-sdk/client-neptune-graph'

const client = new NG.NeptuneGraphClient({})
const GRAPH_ID = process.env.GRAPH_ID

if (!GRAPH_ID) {
throw new Error('GRAPH_ID environment variable is required')
}

/**
* Execute Neptune Analytics query with parameters
*/
async function executeQuery(
queryString: string,
parameters: Record<string, any>
) {
const input = {
graphIdentifier: GRAPH_ID,
queryString,
language: NG.QueryLanguage.OPEN_CYPHER,
parameters,
}

const cmd = new NG.ExecuteQueryCommand(input)
const response = await client.send(cmd)
const responseStr = await response.payload.transformToString()
return JSON.parse(responseStr)
}

/**
* Handle errors with consistent logging and response format
*/
function handleError(error: any, method: string) {
console.error(`${method} /api/edge error:`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}

/**
* Validate ID parameter from query string
*/
function validateId(id: string | null) {
if (!id) {
return NextResponse.json(
{ error: 'id parameter is required' },
{ status: 400 }
)
}
return null
}

/**
* Validate request body contains required fields for edge creation
*/
function validateEdgeBody(body: any) {
if (!body?.fromId || !body?.toId || !body?.type) {
return NextResponse.json(
{ error: 'Request body with fromId, toId, and type is required' },
{ status: 400 }
)
}
return null
}

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')

const error = validateId(id)
if (error) return error

const result = await executeQuery(
'MATCH()-[r]->() WHERE id(r) = $EDGE_ID RETURN r',
{ EDGE_ID: id }
)

return NextResponse.json(result)
} catch (error) {
return handleError(error, 'GET')
}
}

export async function POST(request: Request) {
try {
const body = await request.json()

const error = validateEdgeBody(body)
if (error) return error

const { fromId, toId, type, ...properties } = body

const result = await executeQuery(
'MATCH (from), (to) WHERE id(from) = $FROM_ID AND id(to) = $TO_ID CREATE (from)-[r:' +
type +
']->(to) SET r += $PROPERTIES RETURN r',
{ FROM_ID: fromId, TO_ID: toId, PROPERTIES: properties }
)

return NextResponse.json(result, { status: 201 })
} catch (error) {
return handleError(error, 'POST')
}
}

export async function PUT(request: Request) {
try {
const body = await request.json()

if (!body?.id) {
return NextResponse.json(
{ error: 'Request body with id is required' },
{ status: 400 }
)
}

const { id, ...properties } = body

const result = await executeQuery(
'MATCH()-[r]->() WHERE id(r) = $EDGE_ID SET r = $PROPERTIES RETURN r',
{ EDGE_ID: id, PROPERTIES: properties }
)

return NextResponse.json(result)
} catch (error) {
return handleError(error, 'PUT')
}
}

export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')

const error = validateId(id)
if (error) return error

const result = await executeQuery(
'MATCH()-[r]->() WHERE id(r) = $EDGE_ID DELETE r',
{ EDGE_ID: id }
)

return NextResponse.json(result)
} catch (error) {
return handleError(error, 'DELETE')
}
}
Loading