Skip to content
neode 1.0 alpha

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)
Relationships →