Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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