Developer Guides
Defining and using Model Classes

Model Classes in AD4M

Model classes in AD4M provide a way to define, store, and query structured data in your application. Let's start with a simple example that we'll build upon:

Quick Start

Here's a basic recipe model:

import { Ad4mModel, ModelOptions, Property } from '@coasys/ad4m';
 
@ModelOptions({ name: "Recipe" })
class Recipe extends Ad4mModel {
  @Property({
    through: "recipe://name",
    resolveLanguage: "literal"
  })
  name: string = "";
}
 
// Using the model
const recipe = new Recipe(perspective);
recipe.name = "Chocolate Cake";
await recipe.save();
 
// Reading it back
const recipes = await Recipe.findAll(perspective);
console.log(recipes[0].name); // "Chocolate Cake"

Before using any model class, register it with your perspective:

await perspective.ensureSDNASubjectClass(Recipe);

Basic Concepts

Properties

Properties are the basic building blocks of your models. They can be required or optional:

@ModelOptions({ name: "Recipe" })
class Recipe extends Ad4mModel {
  // Required property
  @Property({
    through: "recipe://name",
    resolveLanguage: "literal"
  })
  name: string = "";
 
  // Optional property
  @Optional({
    through: "recipe://description",
    resolveLanguage: "literal"
  })
  description?: string;
}

Collections

Collections represent one-to-many relationships:

@ModelOptions({ name: "Recipe" })
class Recipe extends Ad4mModel {
  @Property({
    through: "recipe://name",
    resolveLanguage: "literal"
  })
  name: string = "";
 
  @Collection({ through: "recipe://ingredient" })
  ingredients: string[] = [];
}
 
// Using collections
const recipe = new Recipe(perspective);
recipe.name = "Chocolate Cake";
recipe.ingredients = ["flour", "sugar", "cocoa"];
await recipe.save();

Working with Models

Creating & Saving

// Create new instance
const recipe = new Recipe(perspective);
recipe.name = "Chocolate Cake";
await recipe.save();
 
// Create with specific ID
const recipe = new Recipe(perspective, "recipe://chocolate-cake");
await recipe.save();

Reading & Updating

// Get by ID
const recipe = new Recipe(perspective, existingId);
await recipe.get();
 
// Update
recipe.name = "Dark Chocolate Cake";
await recipe.update();

Basic Querying

// Get all recipes
const allRecipes = await Recipe.findAll(perspective);
 
// Get with simple conditions
const recipes = await Recipe.findAll(perspective, {
  where: { name: "Chocolate Cake" }
});

Understanding the Underlying System

Now that you've seen the basics, let's understand how it works under the hood.

Graph-Based Storage

Each model instance is stored as a subgraph in an AD4M perspective. For example, our recipe is stored as:

recipe://chocolate-cake-123 -[recipe://name]-> literal://string:"Chocolate Cake"
recipe://chocolate-cake-123 -[recipe://ingredient]-> ingredient://flour
recipe://chocolate-cake-123 -[recipe://ingredient]-> ingredient://sugar

Every node in this graph is a URL pointing to an Expression in some language.

Base Expressions

Each model instance has a unique identifier called a "base expression" that serves as the root node of its subgraph. You can specify it or let AD4M generate one:

// Auto-generated base expression
const recipe = new Recipe(perspective);
 
// Custom base expression
const recipe = new Recipe(perspective, "recipe://chocolate-cake-2024-03");

Literals for Simple Values

AD4M provides Literals for storing simple values without needing a full language:

// These are equivalent:
recipe.name = "Chocolate Cake";
// Stored as: literal://string:Chocolate Cake
 
recipe.cookingTime = 45;
// Stored as: literal://number:45

Advanced Features

Property Types

AD4M provides several specialized property decorators:

@ReadOnly

For computed or immutable properties:

@ReadOnly({
  through: "recipe://rating",
  getter: `
    findall(Rating, triple(Base, "recipe://user_rating", Rating), Ratings),
    sum_list(Ratings, Sum),
    length(Ratings, Count),
    Value is Sum / Count
  `
})
averageRating: number = 0;

@Flag

For immutable type markers:

@Flag({
  through: "ad4m://type",
  value: "recipe://main-course"
})
type: string = "";

Advanced Collections

Collections can be filtered and typed:

// Collection of other model instances
@Collection({
  through: "recipe://comment",
  where: { isInstance: Comment }
})
comments: Comment[] = [];
 
// Collection with custom filter
@Collection({
  through: "recipe://review",
  where: { condition: `triple(Target, "review://rating", Rating), Rating > 4` }
})
topReviews: string[] = [];

Advanced Querying

The query builder provides a fluent interface for complex queries:

const recipes = await Recipe.query(perspective)
  .where({ 
    category: "MainCourse",
    rating: { gt: 4 }
  })
  .order({ createdAt: "DESC" })
  .limit(5)
  .get();

Real-time Updates

Subscribe to changes in your data:

// Create a query builder
const builder = Recipe.query(perspective)
  .where({ status: "cooking" });
 
// Subscribe to updates
const initialRecipes = await builder.subscribe(recipes => {
  console.log("Currently cooking:", recipes);
});
 
// Important: Clean up when done
builder.dispose();

The query builder's subscription system:

  1. Returns initial results immediately
  2. Calls your callback whenever the results change
  3. Maintains an active connection to receive updates
  4. Automatically cleans up resources when disposed

Always call dispose() when you're done with the subscription to:

  • Stop keepalive signals
  • Unsubscribe from updates
  • Notify the backend to clean up resources
  • Prevent memory leaks

You can also subscribe to just the count of matching items:

const builder = Recipe.query(perspective)
  .where({ category: "Dessert" });
 
const initialCount = await builder.countSubscribe(count => {
  console.log("Number of desserts:", count);
});
 
// Remember to clean up
builder.dispose();

Under the Hood

Social DNA and Prolog

When you define a model class, AD4M generates a Social DNA (SDNA) representation in Prolog:

@ModelOptions({ name: "Recipe" })
class Recipe extends Ad4mModel {
  @Property({
    through: "recipe://name",
    resolveLanguage: "literal",
    required: true
  })
  name: string = "";
}

Relationship with SHACL and Linked Data

AD4M's model classes share concepts with SHACL (Shapes Constraint Language):

  • Both define property constraints and validation rules
  • Both work with graph-based data
  • AD4M adds TypeScript integration, CRUD operations, and real-time features

Best Practices

  1. Use Meaningful Predicates: Structure your predicate URIs logically:

    through: "recipe://name"      // Good
    through: "myapp://x"          // Bad
  2. Type Safety: Always define proper TypeScript types:

    @Property({ through: "recipe://servings" })
    servings: number = 0;         // Good
     
    @Property({ through: "recipe://servings" })
    servings: any;               // Bad
  3. Collection Filtering: Use where conditions to filter collections:

    @Collection({
      through: "recipe://review",
      where: { isInstance: Review }  // Only include Review instances
    })
    reviews: Review[] = [];
  4. Query Optimization: Use specific queries instead of filtering in memory:

    // Good
    const topRated = await Recipe.query(perspective)
      .where({ rating: { gt: 4 } })
      .get();
     
    // Bad
    const all = await Recipe.findAll(perspective);
    const topRated = all.filter(r => r.rating > 4);
  5. Subscriptions: Clean up subscriptions when they're no longer needed:

    const subscription = await Recipe.query(perspective)
      .where({ status: "active" })
      .subscribe(recipes => {
        // Handle updates
      });
     
    // Later...
    subscription.unsubscribe();
  6. Use Literals Appropriately:

    // Good - Using literals for simple values
    @Property({
      through: "recipe://name",
      resolveLanguage: "literal"
    })
    name: string = "";
     
    // Good - Using specific language for rich content
    @Property({
      through: "recipe://instructions",
      resolveLanguage: "markdown"
    })
    instructions: string = "";
  7. Meaningful Base Expressions:

    // Good - Descriptive base expression
    const recipe = new Recipe(perspective, "recipe://chocolate-cake-2024-03");
     
    // Bad - Random or meaningless base expression
    const recipe = new Recipe(perspective, "x://123");

Error Handling

Always handle potential errors when working with models:

try {
  const recipe = new Recipe(perspective);
  recipe.name = "Failed Recipe";
  await recipe.save();
} catch (error) {
  console.error("Failed to save recipe:", error);
  // Handle error appropriately
}

Integration with AD4M Perspectives

Before using a model class, register it with the perspective:

// Register the model class
await perspective.ensureSDNASubjectClass(Recipe);
 
// Now you can use it
const recipe = new Recipe(perspective);
await recipe.save();

This ensures the perspective knows how to handle instances of your model class.