NornicDB GraphQL API
NornicDB provides a full-featured GraphQL API powered by gqlgen, offering an alternative to the Cypher query interface. The GraphQL API supports:
- Full CRUD operations on nodes and relationships
- Advanced search with vector similarity and BM25
- Cypher query pass-through for complex graph operations
- Schema introspection
- Graph traversal queries
Quick Start
Enable GraphQL
GraphQL is enabled by default. The endpoints are:
- GraphQL Endpoint:
POST /graphql
- GraphQL Playground:
GET /playground
Using the Playground
Navigate to http://localhost:7474/playground to access the interactive GraphQL Playground with full schema introspection and auto-complete.
- NOTE: Must add headers
{ "Authorization": "Bearer asdfsgagsaga....."} to your http headers in the playground to use authentication!
Schema Overview
Core Types
type Node {
id: ID!
internalId: String!
labels: [String!]!
properties: JSON!
createdAt: DateTime
updatedAt: DateTime
hasEmbedding: Boolean!
embeddingDimensions: Int!
# Traversal
relationships(
types: [String!]
direction: RelationshipDirection
limit: Int
): [Relationship!]!
outgoing(types: [String!], limit: Int): [Relationship!]!
incoming(types: [String!], limit: Int): [Relationship!]!
neighbors(
direction: RelationshipDirection
relationshipTypes: [String!]
labels: [String!]
limit: Int
): [Node!]!
similar(limit: Int, threshold: Float): [SimilarNode!]!
}
type Relationship {
id: ID!
type: String!
startNode: Node!
endNode: Node!
properties: JSON!
createdAt: DateTime
confidence: Float
autoGenerated: Boolean!
}
Custom Scalars
JSON - Arbitrary JSON objects for properties
DateTime - RFC3339 timestamps
FloatArray - Float32 arrays for embeddings
Example Queries
Basic Queries
# Get database statistics
query Stats {
stats {
nodeCount
relationshipCount
embeddedNodeCount
uptimeSeconds
labels {
label
count
}
}
}
# Get a node by ID
query GetNode($id: ID!) {
node(id: $id) {
id
labels
properties
createdAt
}
}
# List nodes by label
query ListPersons {
allNodes(labels: ["Person"], limit: 10) {
id
labels
properties
}
}
# Get all labels
query Labels {
labels
}
Creating Data
# Create a node
mutation CreatePerson {
createNode(
input: {
labels: ["Person"]
properties: { name: "Alice Smith", age: 30, email: "alice@example.com" }
}
) {
id
labels
properties
}
}
# Create a relationship
mutation CreateKnows {
createRelationship(
input: {
startNodeId: "alice-id"
endNodeId: "bob-id"
type: "KNOWS"
properties: { since: "2024-01-01" }
}
) {
id
type
startNode {
id
}
endNode {
id
}
}
}
# Bulk create nodes
mutation BulkCreate {
bulkCreateNodes(
input: {
nodes: [
{ labels: ["Person"], properties: { name: "Alice" } }
{ labels: ["Person"], properties: { name: "Bob" } }
]
}
) {
created
skipped
errors
}
}
Search Queries
# Hybrid search (vector + BM25)
query Search {
search(
query: "software engineer"
options: { limit: 10, labels: ["Person"], method: HYBRID }
) {
results {
node {
id
labels
properties
}
score
rrfScore
vectorRank
bm25Rank
foundBy
}
totalCount
executionTimeMs
}
}
# Find similar nodes
query FindSimilar {
similar(nodeId: "node-id", limit: 5, threshold: 0.7) {
node {
id
labels
properties
}
similarity
}
}
Cypher Pass-through
# Execute Cypher queries
query CypherQuery {
cypher(
input: {
statement: "MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name LIMIT 10"
}
) {
columns
rows
rowCount
}
}
# Cypher with parameters
query CypherWithParams {
cypher(
input: {
statement: "MATCH (p:Person) WHERE p.age > $minAge RETURN p"
parameters: { minAge: 25 }
}
) {
columns
rows
}
}
# Cypher mutations
mutation CypherCreate {
executeCypher(
input: {
statement: "CREATE (n:Person {name: $name}) RETURN n"
parameters: { name: "Charlie" }
}
) {
rowCount
}
}
Graph Traversal
# Get node with relationships
query NodeWithRelationships {
node(id: "person-id") {
id
labels
outgoing(limit: 10) {
type
endNode {
id
labels
properties
}
}
incoming(limit: 10) {
type
startNode {
id
labels
}
}
}
}
# Find shortest path
query ShortestPath {
shortestPath(
startNodeId: "alice-id"
endNodeId: "charlie-id"
maxDepth: 5
relationshipTypes: ["KNOWS"]
) {
id
labels
properties
}
}
# Get neighborhood subgraph
query Neighborhood {
neighborhood(
nodeId: "center-node-id"
depth: 2
labels: ["Person", "Company"]
) {
nodes {
id
labels
}
relationships {
id
type
startNode {
id
}
endNode {
id
}
}
}
}
Update Operations
# Update node properties
mutation UpdatePerson {
updateNode(
input: { id: "node-id", properties: { age: 31, title: "Senior Engineer" } }
) {
id
properties
}
}
# Merge (upsert) node
mutation MergePerson {
mergeNode(
labels: ["Person"]
matchProperties: { email: "alice@example.com" }
setProperties: { lastLogin: "2024-12-16" }
) {
id
properties
}
}
# Delete node
mutation DeleteNode {
deleteNode(id: "node-id")
}
Admin Operations
# Trigger embedding generation
mutation TriggerEmbedding {
triggerEmbedding(regenerate: false) {
pending
embedded
total
workerRunning
}
}
# Rebuild search index
mutation RebuildIndex {
rebuildSearchIndex
}
# Get schema information
query Schema {
schema {
nodeLabels
relationshipTypes
nodePropertyKeys
}
}
# Clear all data (dangerous!)
mutation ClearAll {
clearAll(confirmPhrase: "DELETE ALL DATA")
}
Integration
Go Client
import "github.com/orneryd/nornicdb/pkg/graphql"
// Create handler
handler := graphql.NewHandler(db)
// Register with HTTP server
http.Handle("/graphql", handler)
http.Handle("/playground", handler.Playground())
HTTP API
# Query
curl -X POST http://localhost:7474/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ stats { nodeCount } }"}'
# Mutation
curl -X POST http://localhost:7474/graphql \
-H "Content-Type: application/json" \
-d '{"query": "mutation { createNode(input: { labels: [\"Test\"], properties: {} }) { id } }"}'
JavaScript/TypeScript
const response = await fetch("/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query GetNode($id: ID!) {
node(id: $id) {
id
labels
properties
}
}
`,
variables: { id: "node-123" },
}),
});
const { data, errors } = await response.json();
Architecture
pkg/graphql/
├── schema/
│ └── schema.graphql # GraphQL schema definition
├── generated/
│ └── generated.go # gqlgen generated code
├── models/
│ └── models.go # Custom model definitions
├── resolvers/
│ ├── resolver.go # Root resolver
│ ├── schema.resolvers.go # Generated resolver stubs
│ ├── query_impl.go # Query implementations
│ ├── mutation_impl.go # Mutation implementations
│ ├── node_impl.go # Node field resolvers
│ ├── relationship_impl.go # Relationship field resolvers
│ └── helpers.go # Conversion helpers
├── handler.go # HTTP handler
└── gqlgen.yml # gqlgen configuration
Regenerating Schema
After modifying schema/schema.graphql:
cd pkg/graphql
go generate ./...
This regenerates generated/generated.go and updates resolver stubs.
Testing
# Run all GraphQL tests
go test ./pkg/graphql/... -v
# Run only resolver tests
go test ./pkg/graphql/resolvers/... -v
# Run with coverage
go test ./pkg/graphql/... -coverprofile=coverage.out
go tool cover -html=coverage.out
- Batching: Use bulk operations for creating/deleting multiple nodes
- Pagination: Always use
limit and offset for large result sets
- Field Selection: Only request fields you need to minimize data transfer
- Cypher Fallback: For complex queries, use the Cypher pass-through for optimal performance
Subscriptions
✅ Real-time subscriptions are now fully implemented!
Subscribe to live updates when nodes and relationships are created, updated, or deleted:
type Subscription {
nodeCreated(labels: [String!]): Node!
nodeUpdated(id: ID, labels: [String!]): Node!
nodeDeleted(labels: [String!]): ID!
relationshipCreated(types: [String!]): Relationship!
relationshipUpdated(id: ID, types: [String!]): Relationship!
relationshipDeleted(types: [String!]): ID!
}
See the GraphQL User Guide for complete subscription documentation and examples.