Fair Supply LogoFair Supply - Docs

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

DirectivePurposeExample
@nodeMarks as Neo4j nodetype User @node
@idAuto-generate unique IDid: ID! @id
@uniqueEnforce uniquenessemail: String! @unique
@defaultDefault valuestatus: String! @default(value: "draft")
@timestampAuto-manage datescreatedAt: DateTime! @timestamp(operations: [CREATE])
@relationshipDefine graph edgeSee below
@customResolverNeeds custom resolvercomputed: 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

TypeConventionExample
Node LabelsPascalCaseUser, Organisation
Relationship TypesSCREAMING_SNAKE_CASEMEMBER_OF, BELONGS_TO
PropertiescamelCasecreatedAt, firstName

After Schema Changes

pnpm codegen  # Regenerate types

Best Practices

  1. Use directives - @timestamp, @id, @unique over manual handling
  2. Define relationships explicitly - clear direction and type
  3. Edge properties for metadata - role, timestamp on relationships
  4. Custom resolvers sparingly - only when directives don't suffice
  5. Index important fields - for query performance
TypeLocation
Schemaspackages/core/src/infrastructure/neo4j/schemas/
Clientpackages/core/src/infrastructure/neo4j/client.ts
Resolverspackages/core/src/infrastructure/neo4j/resolvers/
Cypher Executorpackages/core/src/infrastructure/neo4j/cypher-executor.ts