$ npm i --save @nhogs/nestjs-neo4j
@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 {}
@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);
}
}
neo4jService
.rxRun({ cypher: 'MATCH (n) RETURN count(n) AS count' })
.records()
.subscribe({
next: (record) => {
console.log(record.get('count'));
},
complete: () => {
done();
},
});
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
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()
See source code for more details:
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();
}
}