Defining models
A model is a class with decorators, or a map passed to defineModel(). Both feed the same metamodel and
runtime, so you can mix them freely.
Decorators
Standard TC39 decorators on plain classes. No experimentalDecorators and no reflect-metadata.
import { NodeEntity, Property, Uuid } from 'neode'
@NodeEntity()
export class Person {
@Uuid({ primary: true })
id: string
@Property({ type: 'string', unique: true, required: true, min: 2, max: 50 })
name: string
@Property({ type: 'int' })
age: number
}Map API
The same model without decorators. It is also the migration path for schemas written against 0.x.
import { defineModel } from 'neode'
export const Person = defineModel('Person', {
id: { type: 'uuid', primary: true },
name: { type: 'string', unique: true, required: true, min: 2, max: 50 },
age: 'int', // shorthand: property : type
})Property types and guards
- Types
- strings, numbers, temporal (date, datetime, time, duration), point, uuid, arrays, defaults
- Constraints
- primary, unique, index
- Visibility
- hidden, protected, readonly
- Timestamps
- @CreatedAt and @UpdatedAt are written by the database clock, not the application
Validation
Rules live in the property config — required, min/max, email,
regex, and the rest of the Zod rule names. Every write path validates, and all failures arrive together
in one error before a statement runs.
See it
import { Direction, NodeEntity, OneToMany, Property, Uuid, fromEnv } from 'neode'
@NodeEntity()
export class Person {
@Uuid({ primary: true })
id: string
@Property({ type: 'string', unique: true, required: true, min: 2, max: 50 })
name: string
@Property({ type: 'int' })
age: number
}
@NodeEntity()
export class Movie {
@Property({ type: 'string', unique: true, primary: true })
title: string
// direction 'in': the stored relationship is (Person)-[:ACTED_IN]->(Movie)
@OneToMany(() => Person, 'ACTED_IN', { direction: 'in', eager: true })
actors: Person[]
}
async function main(): Promise<void> {
const neode = fromEnv()
try {
// CREATE — validated against the model before the write
const ray = await neode.create(Person, { name: 'Ray Liotta', age: 40 })
// READ — by primary key
const found = await neode.find(Person, ray.id)
// UPDATE, then relate: Ray Liotta ACTED_IN Goodfellas — the relationship
// key is declared on the Movie model, so the relate runs from that side
await neode.update(found, { age: 41 })
const goodfellas = await neode.create(Movie, { title: 'Goodfellas' })
await neode.relate(Movie, goodfellas, found, 'actors')
// One eager read — the movie and its cast, hydrated in one statement
const movie = await neode.find(Movie, goodfellas.title, { with: { actors: true } })
console.log(`${movie?.title}: ${movie?.actors.length} actor(s) — ${movie?.actors[0]?.name}`)
} finally {
await neode.close()
}
}
main().catch(console.error)