Neo4j Infrastructure
Neo4j is the primary database for the FSA Platform, using @neo4j/graphql for schema-driven development.
Schema Location
All schemas: packages/core/src/infrastructure/neo4j/schemas/
Schema Template
type Entity @node {
id: ID! @id
name: String!
description: String
status: String! @default(value: "draft")
email: String! @unique
# Relationships
parent: Parent! @relationship(type: "BELONGS_TO", direction: OUT)
children: [Child!]! @relationship(type: "HAS", direction: OUT)
# Computed (needs custom resolver)
computed: Boolean @customResolver
# Timestamps
createdAt: DateTime! @timestamp(operations: [CREATE])
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE])
}Directives Reference
| Directive | Purpose | Example |
|---|---|---|
@node | Marks as Neo4j node | type User @node |
@id | Auto-generate unique ID | id: ID! @id |
@unique | Enforce uniqueness | email: String! @unique |
@default | Default value | status: String! @default(value: "draft") |
@timestamp | Auto-manage dates | createdAt: DateTime! @timestamp(operations: [CREATE]) |
@relationship | Define graph edge | See below |
@customResolver | Needs custom resolver | computed: Boolean @customResolver |
Relationship Patterns
Simple Relationship
organisation: Organisation! @relationship(type: "FOR", direction: OUT)With Edge Properties
type UserAccountEdge @relationshipProperties {
role: String!
joinedAt: DateTime!
}
type Account @node {
users: [User!]! @relationship(type: "MEMBER_OF", direction: IN, properties: "UserAccountEdge")
}Many-to-Many
# Both sides reference the same relationship type
frameworks: [Framework!]! @relationship(type: "USES", direction: OUT)
engagements: [Engagement!]! @relationship(type: "USES", direction: IN)Custom Resolvers
Location: packages/core/src/infrastructure/neo4j/resolvers/
import { getDriver } from '../client';
export const organisationResolvers = {
Organisation: {
provisional: async (parent: { id: string }) => {
const driver = getDriver();
const session = driver.session();
try {
const result = await session.run(
`MATCH (o:Organisation {id: $id}) RETURN o:Provisional as provisional`,
{ id: parent.id }
);
return result.records[0]?.get('provisional') ?? false;
} finally {
await session.close();
}
},
},
};Cypher Executor
For custom Cypher queries: packages/core/src/infrastructure/neo4j/cypher-executor.ts
import { executeCypher } from '../neo4j/cypher-executor';
// Read query
const result = await executeCypher(
`MATCH (o:Organisation)-[:HAS]->(l:Location) WHERE o.id = $id RETURN l`,
{ id: organisationId },
{ accessMode: 'READ' }
);
// Write query
await executeCypher(
`MATCH (o:Organisation {id: $id}) SET o.name = $name RETURN o`,
{ id, name },
{ accessMode: 'WRITE' }
);Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Node Labels | PascalCase | User, Organisation |
| Relationship Types | SCREAMING_SNAKE_CASE | MEMBER_OF, BELONGS_TO |
| Properties | camelCase | createdAt, firstName |
After Schema Changes
pnpm codegen # Regenerate typesBest Practices
- Use directives - @timestamp, @id, @unique over manual handling
- Define relationships explicitly - clear direction and type
- Edge properties for metadata - role, timestamp on relationships
- Custom resolvers sparingly - only when directives don't suffice
- Index important fields - for query performance
Related Files
| Type | Location |
|---|---|
| Schemas | packages/core/src/infrastructure/neo4j/schemas/ |
| Client | packages/core/src/infrastructure/neo4j/client.ts |
| Resolvers | packages/core/src/infrastructure/neo4j/resolvers/ |
| Cypher Executor | packages/core/src/infrastructure/neo4j/cypher-executor.ts |