npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mikro-orm-neo4j

v0.2.4

Published

Neo4j driver for MikroORM (OGM-style graph support)

Downloads

400

Readme

@mikro-orm/neo4j

NPM Version License

A native Neo4j driver for MikroORM.

This package provides seamless integration between MikroORM and Neo4j, enabling graph-native operations, complex Cypher query building, and advanced graph data modeling (such as relationship properties) while keeping the familiar MikroORM API.

Features

  • 🚀 Full MikroORM EntityManager and EntityRepository support
  • 🕸️ Graph-native relationships: Support for proper directed relationships in Neo4j (IN, OUT).
  • 💎 Relationship Properties (Pivot Entities): Model complex graph relationships natively.
  • 🏗️ Neo4jQueryBuilder: Fluent API wrapping @neo4j/cypher-builder to write raw Cypher natively with ORM parameter injection, pattern matching, and relationship navigation (.related()).
  • 🏷️ Polymorphic Queries: Support for multi-label inheritance and querying.
  • 🗂️ Indexes & Constraints: @Index() / @Unique() declarations are materialized as real Neo4j indexes and constraints via orm.schema.ensureIndexes().
  • 🧩 Native Decorator Extensions: Fully type-safe strongly-defined neo4j and relation configuration parameters integrated cleanly inside MikroORM properties via declaration merging.
  • 📦 Dual-format support (ESM & CommonJS).

Installation

pnpm add @mikro-orm/neo4j neo4j-driver
pnpm add -D @mikro-orm/core @mikro-orm/reflection

Quick Start

1. Initialize the ORM

Create your MikroORM instance using the Neo4j driver:

import { MikroORM } from '@mikro-orm/neo4j';
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';

const orm = await MikroORM.init({
  clientUrl: 'bolt://localhost:7687', // Your Neo4j URI
  user: 'neo4j',
  password: 'password',
  entities: ['./dist/entities'],
  entitiesTs: ['./src/entities'],
  metadataProvider: TsMorphMetadataProvider,
});

const em = orm.em;

2. Define Entities

You can define entities using standard MikroORM decorators, but with native Neo4j extensions.

Decorator Approach

Support for native extension properties relation inside @ManyToOne/@ManyToMany decorators, and neo4j inside @Entity() decorators allows you to configure graph-specific metadata.

import { Entity, PrimaryKey, Property, ManyToOne, Collection, OneToMany } from '@mikro-orm/core';

@Entity({ neo4j: { labels: ['User', 'Person'] } })
export class User {
  @PrimaryKey()
  id!: string;

  @Property()
  name!: string;

  @OneToMany(() => Post, post => post.author)
  posts = new Collection<Post>(this);
}

@Entity()
export class Post {
  @PrimaryKey()
  id!: string;

  @Property()
  title!: string;

  // Utilize the natively-augmented relation object to denote Neo4j specifics
  @ManyToOne(() => User, { relation: { type: 'CREATED', direction: 'IN' } })
  author!: User;
}

Functional Approach (defineEntity)

For those who prefer a functional style or are building dynamic schemas, @mikro-orm/neo4j provides a specialized defineEntity wrapper that includes the neo4j helper for property configuration.

import { defineEntity, neo4j } from '@mikro-orm/neo4j';
import * as crypto from 'node:crypto';

export const MovieSchema = defineEntity({
  name: 'Movie',
  labels: ['Cinema', 'Show'], // Native Neo4j labels
  properties(p) {
    return {
      id: p.uuid().primary().onCreate(() => crypto.randomUUID()),
      title: p.string(),
      released: p.integer(),
      actors: () => neo4j(
        p.manyToMany(ActorSchema).mappedBy('movies'),
        { type: 'ACTED_IN', direction: 'IN' }
      ),
    };
  },
});

// To add logic or methods, use setClass
export class Movie extends MovieSchema.class {
  get isNew(): boolean {
    return this.released > 2020;
  }
}
MovieSchema.setClass(Movie);

Interface inheritance and relationships

You can define a reusable interface entity that captures common fields and declared relationship fields, and then implement it in concrete Neo4j node entities.

const ProductionSchema = defineEntity({
  name: 'Production',
  abstract: true,
  inheritance: 'interface',
  properties(p) {
    return {
      title: p.string(),
      actors: () => p.manyToMany(PersonSchema),
    };
  },
});

const MovieSchema = defineEntity({
  name: 'Movie',
  extends: ProductionSchema,
  labels: ['Movie'],
  properties(p) {
    return {
      released: p.integer(),
      actors: () => neo4j(
        p.manyToMany(PersonSchema).owner().pivotEntity(() => ActedInSchema),
        { type: 'ACTED_IN', direction: 'IN' },
      ),
    };
  },
});

const SeriesSchema = defineEntity({
  name: 'Series',
  extends: ProductionSchema,
  labels: ['Series'],
  properties(p) {
    return {
      episodes: p.integer(),
      actors: () => neo4j(
        p.manyToMany(PersonSchema).owner().pivotEntity(() => ActedInSeriesSchema),
        { type: 'ACTED_IN', direction: 'IN' },
      ),
    };
  },
});

This produces GraphQL SDL where Production is emitted as an interface with the declared relationship:

interface Production {
  title: String!
  actors: [Person!]! @declareRelationship
}

type Movie implements Production @node {
  released: Int!
  actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

type Series implements Production @node {
  episodes: Int!
  actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedInSeries")
}

Setting up TypeScript Typings

Because mikro-orm-neo4j uses global declaration merging to augment @mikro-orm/core, you get autocomplete natively without requiring any as any casts! To ensure your TypeScript compiler (tsc) correctly registers these definitions in your project, simply add @mikro-orm/neo4j/types to your tsconfig.json compiler options, or add a triple-slash reference in your global.d.ts:

// tsconfig.json
{
  "compilerOptions": {
    "types": [
      "node",
      "@mikro-orm/neo4j/types"
    ]
  }
}

Or programmatically in any entry file:

/// <reference types="@mikro-orm/neo4j/types" />

3. Relationship Properties (Pivot Entities)

In Neo4j, relationships can have their own properties. You can model this using a standard @Entity() configured as a relationshipEntity.

import { Entity, PrimaryKey, Property, ManyToOne, Collection, ManyToMany } from '@mikro-orm/core';

@Entity()
export class Actor {
  @PrimaryKey()
  id!: string;

  @Property()
  name!: string;
  
  @ManyToMany(() => Movie, undefined, {
    pivotEntity: () => ActedIn,
    inversedBy: 'actors',
    relation: { type: 'ACTED_IN', direction: 'OUT' }
  })
  movies = new Collection<Movie>(this);
}

@Entity()
export class Movie {
  @PrimaryKey()
  id!: string;

  @Property()
  title!: string;
}

#### Using decorators

Mark this entity as a Neo4j Relationship instead of a Node!

```typescript
@Entity({ neo4j: { relationshipEntity: true, type: 'ACTED_IN' } })
export class ActedIn {
  @PrimaryKey()
  id!: string;

  @ManyToOne(() => Actor, { primary: true })
  actor!: Actor;

  @ManyToOne(() => Movie, { primary: true })
  movie!: Movie;

  @Property()
  roles!: string[]; // Relationship property stored inside Neo4j relation data!
}

Using defineEntity

const ActedInSchema = defineEntity({
  name: 'ActedIn',
  relationship: { type: 'ACTED_IN' },
  properties(p) {
    return {
      id: p.uuid().primary(),
      actor: () => p.manyToOne(ActorSchema).primary(),
      movie: () => p.manyToOne(MovieSchema).primary(),
      roles: p.array('string'),
    };
  },
});

[!TIP] Why relation instead of custom decorators like @Rel? Relying on MikroORM's built-in PropertyOptions ensures better compatibility with the internal lifecycle hook systems and removes the buggy reflection extraction complexities of scanning custom external decorators during schema generation.

4. Constructing Complex Cypher Queries (Neo4jQueryBuilder)

The custom Neo4jQueryBuilder extends MikroORM principles and seamlessly bridges them to robust graph traversal queries via Cypher.

Filtering & Relation traversal with match and related

const qb = em.createQueryBuilder(User);

// Finds users named John Doe who created a specific post, and returns the post title
const result = await qb
  .match()
  .where('name', 'John Doe')
  .related(User, 'posts') // automatically extracts 'CREATED' relationship metadata
  .where('title', 'Graph Databases 101')
  .return(['title'])
  .execute();

Complex Multi-Path Traversals

const qb = em.createQueryBuilder(Actor);
const Cypher = qb.getCypher(); // Direct access to underlying @neo4j/cypher-builder toolkit

// Find actors who acted in "The Matrix" AND also directed it
const { cypher, params } = qb
  .match()
  .related(Actor, 'movies')
  .where('title', 'The Matrix')
  .match() // Starts a new MATCH statement linking the context
  // Use raw pattern building for complex graph spans
  .rawCypherPattern(new Cypher.Pattern(qb.getCurrentNode()).related(new Cypher.Relationship({ type: 'DIRECTED' })).to(new Cypher.Node({ labels: ['Movie'] })))
  .return(['name'])
  .build();

5. Read Replicas & Load Balancing

For large-scale applications, @mikro-orm/neo4j supports read-replicas out of the box. You can configure multiple read-only connections in MikroORM.init().

const orm = await MikroORM.init({
  clientUrl: 'bolt://primary:7687',
  user: 'neo4j',
  password: 'password',
  replicas: [
    { clientUrl: 'bolt://replica-1:7687', user: 'neo4j', password: 'password' },
    { clientUrl: 'bolt://replica-2:7687', user: 'neo4j', password: 'password' },
  ],
});
  • Automatic Splitting: By default, em.find() and em.findOne() operations will be automatically load-balanced across your replicas.
  • Manual Control: You can explicitly request a connection type if needed:
    const readConn = em.getDriver().getConnection('read');
    const writeConn = em.getDriver().getConnection('write');

6. Transaction Management

Proper transactional support is essential for data integrity. @mikro-orm/neo4j fully supports MikroORM's transaction API.

Declarative Transactions

Use em.transactional() to wrap multiple operations in a single Neo4j transaction. If the callback throws, the transaction is automatically rolled back.

await em.transactional(async (txEm) => {
  const user = txEm.create(User, { name: 'Alice' });
  txEm.persist(user);
  
  const post = txEm.create(Post, { title: 'First Post', author: user });
  txEm.persist(post);
  
  await txEm.flush();
});

Manual Transaction Control

const fork = em.fork();
await fork.begin();
try {
  // ... operations
  await fork.commit();
} catch (e) {
  await fork.rollback();
  throw e;
}

7. Exception Handling

The driver automatically maps Neo4j-specific error codes to standard MikroORM exceptions:

| Exception | Neo4j Error Code Example | | :--- | :--- | | UniqueConstraintViolationException | Neo.ClientError.Schema.ConstraintValidationFailed | | NotNullConstraintViolationException | Neo.ClientError.Schema.PropertyExistenceError | | SyntaxErrorException | Neo.ClientError.Statement.SyntaxError | | ReadOnlyException | Neo.ClientError.Statement.AccessMode (Write on Read Replica) | | DeadlockException | Neo.TransientError.Transaction.DeadlockDetected | | ConnectionException | Neo.TransientError.Network.ConnectivityError |

8. Indexes & Constraints (ensureIndexes)

A graph has no tables to create: its schema is its indexes and constraints. orm.schema.ensureIndexes() reads the indexes / uniques you declared on your entities and creates them in Neo4j.

@Entity({ tableName: 'Document' })
@Index({ properties: ['tenant', 'id'] })
@Unique({ properties: ['tenant', 'externalId'] })
export class Document {
  @PrimaryKey()
  id!: string;

  @Property()
  tenant!: string;

  @Property()
  externalId!: string;
}

// Typically at bootstrap:
await orm.schema.ensureIndexes();

Every statement is emitted with IF NOT EXISTS, so ensureIndexes() is idempotent and safe to run on every boot — no diffing against the live schema. orm.schema.create() delegates to it, and orm.schema.getCreateSchemaSQL() returns the Cypher without executing it, which is useful as a dry-run:

console.log(await orm.schema.getCreateSchemaSQL());
// CREATE RANGE INDEX `Document_tenant_id_idx` IF NOT EXISTS FOR (n:`Document`) ON (n.`tenant`, n.`id`);
// CREATE CONSTRAINT `Document_tenant_externalId_unique` IF NOT EXISTS FOR (n:`Document`) REQUIRE (n.`tenant`, n.`externalId`) IS UNIQUE

Index types

type maps onto the Neo4j index kinds. Omitting it gives you a RANGE index, which is what you want for equality and range lookups.

| type | Neo4j index | Notes | |---|---|---| | (omitted) / 'range' | RANGE | Supports composite keys. | | 'text' | TEXT | Single property only. | | 'point' | POINT | Single property only. | | 'fulltext' | FULLTEXT | Spans several properties (ON EACH [...]). |

Property order matters for composite indexes: ['tenant', 'id'] can serve a lookup by tenant alone, but not by id alone.

Nodes, edges and labels

  • Multi-label entities are indexed on their primary label only. Neo4j indexes per label, and a query matching a secondary label already seeks through the primary one, so indexing all of them would multiply indexes for no gain.
  • Relationship entities (@Entity({ relationship: { type: 'ACTED_IN' } })) are indexed as edges: FOR ()-[r:ACTED_IN]-() ON (r.billing).
  • Indexes are built on the property name as written on the node — the JS key, which is what the driver persists. The naming strategy affects the label, not the properties.

Options that have no Neo4j equivalent

Rather than emit an index that quietly means something different from what you declared, the generator is explicit about what it cannot map:

| Option | Behaviour | |---|---| | where (partial index/constraint) | Throws. A partial unique emitted as a total one would reject legitimate rows. Model the filtered subset with a dedicated label instead. | | expression (functional index) | Warns and skips. | | type: 'vector' | Throws — vector indexes need an explicit dimension and similarity function; create them with raw Cypher. | | include, fillFactor, invisible, deferMode, clustered | Ignored — they are SQL planner hints with no Neo4j meaning. |

NODE KEY and IS NOT NULL constraints are Enterprise-only in Neo4j and are not emitted.

Not covered yet: update() (which would require diffing against SHOW INDEXES) and drop(). Since ensureIndexes() is idempotent, removing a declaration does not remove the index — drop it manually with DROP INDEX <name>.

9. Schema Generation & GraphQL Support

The driver includes a Neo4jSchemaGenerator that can export your MikroORM metadata as a GraphQL SDL (Schema Definition Language) compatible with the @neo4j/graphql library.

This is particularly powerful for:

  • 🤖 AI-Ready Schemas: AI agents and LLMs perform significantly better when provided with a detailed SDL including semantic descriptions.
  • Instant APIs: Generate a standard GraphQL schema for Neo4j based on your ORM models.

Generating SDL

You can access the generator through the standard MikroORM schema API:

const sdl = orm.schema.getGraphSdl();
console.log(sdl);

Enrichment with comment

Both the decorator and functional APIs support a comment property. These comments are automatically translated into GraphQL docstrings (triple-quoted strings) in the generated SDL.

Decorator Approach
@Entity({ comment: 'Represents a human user in the system.' })
export class User {
  @PrimaryKey()
  id!: string;

  @Property({ comment: 'The display name used in public profiles.' })
  name!: string;
}
Functional Approach
export const ProductSchema = defineEntity({
  name: 'Product',
  comment: 'An item available for purchase.',
  properties(p) {
    return {
      id: p.uuid().primary(),
      price: p.number({ comment: 'Retail price in USD.' }),
    };
  },
});

Resulting SDL Example

"""
An item available for purchase.
"""
type Product @node {
  id: ID!
  """
  Retail price in USD.
  """
  price: Float!
}

10. Advanced Usage

Custom labels via defineEntity

You can specify multiple labels for an entity which will be used during query generation and node creation.

const AuthorSchema = defineEntity({
  name: 'Author',
  labels: ['Author', 'Person'],
  properties(p) {
    return {
      id: p.uuid().primary(),
      name: p.string(),
    };
  },
});

Running Raw Cypher

For raw parameterized queries, use em.run():

const users = await em.run(
  `MATCH (u:User)-[:CREATED]->(p:Post) WHERE p.title = $title RETURN u`,
  { title: 'Graph Databases 101' }
);

Appendices

In-depth notes on specific fixes and behaviours live under docs/:

Troubleshooting

Node.js globSync SyntaxError

If you encounter SyntaxError: The requested module 'node:fs' does not provide an export named 'globSync', it means you are running a version of Node.js older than 22.0.0.

Solution: Ensure your environment (including CI runners and Docker containers) is using Node.js 22+.

Running Workflows with act on Apple Silicon

When using act to test GitHub Actions locally on an Apple M-series (M1/M2/M3) chip, you may encounter an exit code 137 (OOM or Architecture crash) during the setup-node step.

Solution: Specify the container architecture explicitly to avoid emulation crashes:

act --container-architecture linux/amd64

Additionally, ensure your Docker Desktop has at least 4GB-6GB of RAM allocated in Settings > Resources.

License

MIT License

Author

Manuel Antunes