/nestjs-neo4j

Neo4j module for Nestjs

Primary LanguageTypeScriptMIT LicenseMIT

nhogs-logo

@nhogs/nestjs-neo4j

Description

Neo4j module for Nest.js.

e2e-test Docker-tested-version Maintainability Test Coverage

Peer Dependencies

npm peer dependency version NestJS) npm peer dependency version neo4j-driver)

Installation

npm

$ npm i --save @nhogs/nestjs-neo4j

Usage

In static module definition:

@Module({
  imports: [
    Neo4jModule.forRoot({
      scheme: 'neo4j',
      host: 'localhost',
      port: '7687',
      database: 'neo4j',
      username: 'neo4j',
      password: 'test',
      global: true, // to register in the global scope
    }),
    CatsModule,
  ],
})
export class AppModule {}

In async module definition:

@Module({
  imports: [
    Neo4jModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService): Neo4jConfig => ({
        scheme: configService.get('NEO4J_SCHEME'),
        host: configService.get('NEO4J_HOST'),
        port: configService.get('NEO4J_PORT'),
        username: configService.get('NEO4J_USERNAME'),
        password: configService.get('NEO4J_PASSWORD'),
        database: configService.get('NEO4J_DATABASE'),
      }),
      global: true,
    }),
    PersonModule,
    ConfigModule.forRoot({
      envFilePath: './test/src/.test.env',
    }),
  ],
})
export class AppAsyncModule {}
@Injectable()
/** See https://neo4j.com/docs/api/javascript-driver/current/ for details ...*/
export class Neo4jService implements OnApplicationShutdown {
  constructor(
    @Inject(NEO4J_CONFIG) private readonly config: Neo4jConfig,
    @Inject(NEO4J_DRIVER) private readonly driver: Driver,
  ) {}

  /** Verifies connectivity of this driver by trying to open a connection with the provided driver options...*/
  verifyConnectivity(options?: { database?: string }): Promise<ServerInfo> {...}

  /** Regular Session. ...*/
  getSession(options?: SessionOptions): Session {...}

  /** Reactive session. ...*/
  getRxSession(options?: SessionOptions): RxSession {...}

  /** Run Cypher query in regular session and close the session after getting results. ...*/
  run(
    query: Query,
    sessionOptions?: SessionOptions,
    transactionConfig?: TransactionConfig,
  ): Promise<QueryResult> {...}

  /** Run Cypher query in reactive session. ...*/
  rxRun(
    query: Query,
    sessionOptions?: SessionOptions,
    transactionConfig?: TransactionConfig,
  ): RxResult {...}

  /** Returns constraints as runnable Cypher queries defined with decorators on models. ...*/
  getCypherConstraints(label?: string): string[] {...}

  onApplicationShutdown() {
    return this.driver.close();
  }
}
/**
 * Cat Service example
 */

@Injectable()
export class CatService {
  constructor(private readonly neo4jService: Neo4jService) {}

  async create(cat: Cat): Promise<Cat> {
    const queryResult = await this.neo4jService.run(
      {
        cypher: 'CREATE (c:`Cat`) SET c=$props RETURN properties(c) AS cat',
        parameters: {
          props: cat,
        },
      },
      { write: true },
    );

    return queryResult.records[0].toObject().cat;
  }

  async findAll(): Promise<Cat[]> {
    return (
      await this.neo4jService.run({
        cypher: 'MATCH (c:`Cat`) RETURN properties(c) AS cat',
      })
    ).records.map((record) => record.toObject().cat);
  }
}

Run with reactive session

neo4jService
  .rxRun({ cypher: 'MATCH (n) RETURN count(n) AS count' })
  .records()
  .subscribe({
    next: (record) => {
      console.log(record.get('count'));
    },
    complete: () => {
      done();
    },
  });

Define constraints with decorators on Dto

https://neo4j.com/docs/cypher-manual/current/constraints/

  • @NodeKey():
    • Node key constraints
  • @Unique():
    • Unique node property constraints
  • @NotNull():
    • Node property existence constraints
    • Relationship property existence constraints

🔗 Constraint decorators - source code

@Node({ label: 'Person' })
export class PersonDto {
  @NodeKey({ additionalKeys: ['firstname'] })
  name: string;

  @NotNull()
  firstname: string;

  @NotNull()
  @Unique()
  surname: string;

  @NotNull()
  age: number;
}

@Relationship({ type: 'WORK_IN' })
export class WorkInDto {
  @NotNull()
  since: Date;
}

Will generate the following constraints:

CREATE CONSTRAINT `person_name_key` IF NOT EXISTS FOR (p:`Person`) REQUIRE (p.`name`, p.`firstname`) IS NODE KEY
CREATE CONSTRAINT `person_firstname_exists` IF NOT EXISTS FOR (p:`Person`) REQUIRE p.`firstname` IS NOT NULL
CREATE CONSTRAINT `person_surname_unique` IF NOT EXISTS FOR (p:`Person`) REQUIRE p.`surname` IS UNIQUE
CREATE CONSTRAINT `person_surname_exists` IF NOT EXISTS FOR (p:`Person`) REQUIRE p.`surname` IS NOT NULL
CREATE CONSTRAINT `person_age_exists` IF NOT EXISTS FOR (p:`Person`) REQUIRE p.`age` IS NOT NULL

CREATE CONSTRAINT `work_in_since_exists` IF NOT EXISTS FOR ()-[p:`WORK_IN`]-() REQUIRE p.`since` IS NOT NULL

Extends Neo4j Model Services helpers to get basic CRUD methods for node or relationships:

classDiagram
class Neo4jModelService~T~
<<abstract>> Neo4jModelService
class Neo4jNodeModelService~N~
<<abstract>> Neo4jNodeModelService
class Neo4jRelationshipModelService~R~
<<abstract>> Neo4jRelationshipModelService
Neo4jModelService : string label*
Neo4jModelService : runCypherConstraints()
Neo4jModelService <|--Neo4jNodeModelService
Neo4jNodeModelService : create()
Neo4jNodeModelService : merge()
Neo4jNodeModelService : update()
Neo4jNodeModelService : delete()
Neo4jNodeModelService : findAll()
Neo4jNodeModelService : findBy()
Neo4jModelService <|--Neo4jRelationshipModelService
Neo4jRelationshipModelService : create()
Loading

See source code for more details:

Examples:

Look at 🔗 E2e tests usage for more details

/**
 * Cat Service example
 */

@Injectable()
export class CatsService extends Neo4jNodeModelService<Cat> {
  constructor(protected readonly neo4jService: Neo4jService) {
    super();
  }

  label = 'Cat';
  logger = undefined;

  fromNeo4j(model: Record<string, any>): Cat {
    return super.fromNeo4j({
      ...model,
      age: model.age.toNumber(),
    });
  }

  toNeo4j(cat: Record<string, any>): Record<string, any> {
    let result: Record<string, any> = { ...cat };

    if (!isNaN(result.age)) {
      result.age = int(result.age);
    }

    return super.toNeo4j(result);
  }

  // Add a property named 'created' with timestamp on creation
  protected timestamp = 'created';

  findByName(
    name: string,
    options?: {
      skip?: number;
      limit?: number;
      orderBy?: string;
      descending?: boolean;
    },
  ) {
    return super.findBy({ name }, options);
  }

  searchByName(
    name: string,
    options?: {
      skip?: number;
      limit?: number;
    },
  ) {
    return super.searchBy('name', name.split(' '), options);
  }
}
/**
 * WORK_IN Controller example
 */

@Controller('WORK_IN')
export class WorkInController {
  constructor(
    private readonly personService: PersonService,
    private readonly workInService: WorkInService,
    private readonly companyService: CompanyService,
  ) {}

  @Post('/:from/:to')
  async workIn(
    @Param('from') from: string,
    @Param('to') to: string,
    @Body() workInDto: WorkInDto,
  ): Promise<[PersonDto, WorkInDto, CompanyDto][]> {
    return this.workInService
      .create(
        workInDto,
        { name: from },
        { name: to },
        this.personService,
        this.companyService,
      )
      .run();
  }

  @Get()
  async findAll(): Promise<[PersonDto, WorkInDto, CompanyDto][]> {
    return this.workInService.findAll();
  }
}

License

MIT licensed.