Skip to content

Latest commit

 

History

History
209 lines (152 loc) · 8.61 KB

File metadata and controls

209 lines (152 loc) · 8.61 KB

Models

import "github.com/CaliLuke/go-typeql/v3/gotype" -- pkg.go.dev

Models are Go structs that map to TypeDB entities and relations. They use struct tags to define attribute names, annotations, and role players.

Defining Entities

Embed gotype.BaseEntity and tag each exported field with typedb:"...":

type Person struct {
    gotype.BaseEntity
    Name  string `typedb:"name,key"`
    Email string `typedb:"email,unique"`
    Age   *int   `typedb:"age"`
}
  • BaseEntity provides the Entity marker interface plus GetIID() / SetIID() methods.
  • The TypeDB type name is derived from the Go struct name in kebab-case (UserAccount becomes user-account).
  • Pointer fields are optional attributes. Non-pointer fields are required.

Defining Relations

Embed gotype.BaseRelation. Role player fields use role:name tags; attribute fields use the same syntax as entities:

type Employment struct {
    gotype.BaseRelation
    Employee  *Person  `typedb:"role:employee"`
    Employer  *Company `typedb:"role:employer"`
    StartDate *string  `typedb:"start-date"`
}
  • BaseRelation provides the Relation marker interface plus GetIID() / SetIID().
  • Role player fields must be pointers to registered entity types.

Struct Tag Reference

Tags follow the format typedb:"name[,option1][,option2]...":

Tag Example Description
attribute name typedb:"name" Maps field to a TypeDB attribute
key typedb:"name,key" @key annotation (unique identifier)
unique typedb:"email,unique" @unique annotation
card=M..N typedb:"items,card=0..5" Cardinality constraint
role:name typedb:"role:employee" Role player in a relation
abstract typedb:"abstract" Marks the type as abstract
type:name typedb:"type:custom_name" Overrides the TypeDB type name
sub:name typedb:"sub:artifact" Declares an explicit supertype
value:decimal typedb:"price,value:decimal" Stores the attribute as TypeDB decimal
- typedb:"-" Skip this field

Cardinality formats: 0..1, 1..5, 2.. (unbounded max), 0+ (shorthand for 0..). Negative or inverted ranges (card=5..2) are rejected at registration.

value:decimal overrides the value type derived from the Go field (a float64 field would otherwise map to double). It is allowed on float64/float32 fields (idiomatic, but lossy for fractions a binary float cannot represent) and on string fields (exact: digits round-trip verbatim, e.g. "0.1"), including pointers and slices of those; any other field kind is rejected at registration. Write paths emit dec-suffixed literals (12.5dec; integral values keep a fraction: 3.0dec), and values read back arrive as decimal strings and are parsed into the field type automatically.

Type-level options (abstract, type:, sub:) may be placed on the embedded base field (e.g. gotype.BaseEntity `typedb:"sub:artifact"` ) or on a blank field (_ byte). The declared supertype is emitted as entity child sub parent in the generated schema and populates ModelInfo.Supertype / SubtypesOf.

Registration validates tags strictly and returns an error for: field-level options without an attribute name (typedb:"key" alone), unsupported Go field types (map/chan/complex/non-time structs), attribute names duplicated within a model or registered elsewhere with a different value type, reserved-word or malformed role names, conflicting type:/sub: declarations, and invalid cardinality ranges.

Schema Documentation

TypeDB 3.12 @doc annotations can be emitted from Go models.

Use typedb_doc for field ownership documentation:

type Person struct {
    gotype.BaseEntity
    Name string `typedb:"name,key" typedb_doc:"Primary display name."`
}

Use SchemaDoc() string for type-level documentation:

func (Person) SchemaDoc() string {
    return "A person record."
}

Use SchemaMeta() map[string]string for type-level metadata:

func (Person) SchemaMeta() map[string]string {
    return map[string]string{
        "owner": "identity",
        "ui":    "person",
    }
}

Schema generation emits these as TypeQL @doc("...") and @meta("key", "value") annotations. Metadata keys are sorted for deterministic schema output. There is no general typedb_meta field tag because repeatable key/value metadata does not fit Go struct tags cleanly.

Go Type to TypeDB Value Type Mapping

Go Type TypeDB Value Type
string string
bool boolean
int, int8..int64 long
uint, uint8..uint64 long
float32, float64 double
time.Time datetime

Registration

All model types must be registered before use. Registration extracts metadata via reflection and stores it in a global registry. Registration rejects the 42 reserved TypeQL keywords, such as define, match, and entity. The check matches case.

// Register returns an error if the type is invalid
err := gotype.Register[Person]()

// MustRegister panics on error (convenient for init())
gotype.MustRegister[Person]()

The registry is global and shared. In tests, call ClearRegistry() and re-register per test since other tests may clear it.

Lookup functions let you find registered types by TypeDB name, Go type, or Go struct name. SubtypesOf and ResolveType support polymorphic type hierarchies.

Hydration

Hydration populates struct fields from map[string]any data returned by TypeDB queries:

// Populate an existing struct
err := gotype.Hydrate(&person, data)

// Create and populate in one step
person, err := gotype.HydrateNew[Person](data)

// Polymorphic: uses the "_type" field in data to pick the concrete type
instance, err := gotype.HydrateAny(data)

Hydration handles nested role player structs recursively, with a depth limit of 10 (MaxHydrationDepth) to prevent infinite loops when the database graph contains cycles.

Serialization

Convert between struct instances and map[string]any:

alice := &Person{Name: "Alice", Email: "alice@example.com"}

// Struct -> map
dict, err := gotype.ToDict(alice)
// {"name": "Alice", "email": "alice@example.com"}

// Map -> struct
person, err := gotype.FromDict[Person](dict)

Generate TypeQL strings directly from struct instances:

alice := &Person{Name: "Alice", Email: "alice@example.com"}

insertQL, err := gotype.ToInsertQuery(alice)
// insert $e isa person, has name "Alice", has email "alice@example.com";

matchQL, err := gotype.ToMatchQuery(alice)
// match $e isa person, has name "Alice";

ToMatchQuery uses key fields only. Both require the type to be registered.

Polymorphism

For type hierarchies (using sub), the registry supports polymorphic operations:

// Get all registered subtypes of "artifact"
subtypes := gotype.SubtypesOf("artifact")

// Resolve a type label returned by TypeDB
info, ok := gotype.ResolveType("task")

See GetByIIDPolymorphic and GetByIIDPolymorphicAny in the CRUD docs for fetching instances polymorphically.

Key Internals

  • ModelInfo holds all extracted metadata for a registered type: Go type, kind (entity/relation), TypeDB name, fields, roles, key fields. You can look up fields by Go name or TypeDB attribute name.
  • ModelStrategy is the internal strategy pattern (entityStrategy / relationStrategy) that builds TypeQL strings for different type kinds. You don't interact with it directly.
  • Reserved words: Registration checks names against the 42 reserved TypeQL keywords (TypeQLReservedWords). If a type, attribute, or role name is one of them, Register returns a ReservedWordError. The check matches case, like the server: match is reserved, and Match is not. Other TypeQL words, such as label and count, are valid names.
  • Identifier validation: Type names, attribute names, and role names are validated during registration. Valid identifiers start with a letter or underscore and contain only letters, digits, hyphens, or underscores. Invalid identifiers produce an InvalidIdentifierError. Use ValidateIdentifier(name, context) to check programmatically.