Primitives.org.ai

Things

Entity instances with CRUD operations

Things are entity instances that implement Nouns. They store your actual data with automatic ID generation, timestamps, and type tracking.

What is a Thing?

A Thing is an instance of a Noun with data and metadata:

interface Thing<T = Record<string, unknown>> {
  id: string          // Unique identifier
  noun: string        // References noun.name ('Post', 'User')
  data: T             // Your actual data
  createdAt: Date     // When created
  updatedAt: Date     // When last modified
}

Creating Things

Basic Creation

import { MemoryProvider } from 'digital-objects'

const provider = new MemoryProvider()

// Create with auto-generated ID
const post = await provider.create('Post', {
  title: 'Hello World',
  content: 'This is my first post',
})
// {
//   id: 'abc123-...',
//   noun: 'Post',
//   data: { title: 'Hello World', content: '...' },
//   createdAt: Date,
//   updatedAt: Date
// }

With Custom ID

// Create with a specific ID
const post = await provider.create('Post', {
  title: 'Hello World',
}, 'post_001')
// { id: 'post_001', noun: 'Post', ... }

Typed Creation

interface Post {
  title: string
  content: string
  published: boolean
}

const post = await provider.create<Post>('Post', {
  title: 'Hello World',
  content: 'This is my first post',
  published: false,
})
// post.data is typed as Post

Reading Things

Get by ID

const post = await provider.get('abc123')
if (post) {
  console.log(post.data.title)  // 'Hello World'
}

Get with Type

interface Post {
  title: string
  content: string
}

const post = await provider.get<Post>('abc123')
if (post) {
  console.log(post.data.title)   // Typed as string
  console.log(post.data.content) // Typed as string
}

List by Noun

// List all posts
const posts = await provider.list('Post')
for (const post of posts) {
  console.log(post.data.title)
}

List with Options

// Pagination
const posts = await provider.list('Post', {
  limit: 10,
  offset: 20,
})

// Ordering
const posts = await provider.list('Post', {
  orderBy: 'title',
  order: 'asc',
})

// Filtering
const posts = await provider.list('Post', {
  where: { published: true },
})

// Combined
const posts = await provider.list('Post', {
  where: { published: true },
  orderBy: 'createdAt',
  order: 'desc',
  limit: 10,
  offset: 0,
})

Find by Properties

// Find posts by author
const posts = await provider.find('Post', {
  authorId: 'user_123',
})

// Find users by email
const users = await provider.find('User', {
  email: 'alice@example.com',
})

// Find with multiple conditions
const posts = await provider.find('Post', {
  published: true,
  category: 'tech',
})
// Full-text search across all fields
const results = await provider.search('hello world')

// Search with limit
const results = await provider.search('typescript', { limit: 5 })

Updating Things

Partial Update

// Update specific fields
const updated = await provider.update('abc123', {
  title: 'Updated Title',
})
// Only title is changed, other fields preserved

Full Update

interface Post {
  title: string
  content: string
  published: boolean
}

const post = await provider.get<Post>('abc123')
if (post) {
  // Spread existing data to preserve all fields
  const updated = await provider.update<Post>('abc123', {
    ...post.data,
    published: true,
  })
}

Update Returns New State

const updated = await provider.update('abc123', { title: 'New Title' })

console.log(updated.data.title)      // 'New Title'
console.log(updated.updatedAt)       // New timestamp
console.log(updated.createdAt)       // Original timestamp (unchanged)

Deleting Things

Delete by ID

const deleted = await provider.delete('abc123')
if (deleted) {
  console.log('Post deleted')
} else {
  console.log('Post not found')
}

Delete Returns Boolean

const success = await provider.delete('nonexistent')
// false (thing didn't exist)

const success = await provider.delete('abc123')
// true (thing was deleted)

Batch Operations

Create Many

const posts = await provider.createMany('Post', [
  { title: 'Post 1', content: 'Content 1' },
  { title: 'Post 2', content: 'Content 2' },
  { title: 'Post 3', content: 'Content 3' },
])
// Returns array of created Things

Update Many

const updated = await provider.updateMany([
  { id: 'post_1', data: { published: true } },
  { id: 'post_2', data: { published: true } },
  { id: 'post_3', data: { published: false } },
])
// Returns array of updated Things

Delete Many

const results = await provider.deleteMany(['post_1', 'post_2', 'post_3'])
// Returns array of booleans: [true, true, false]

Type Safety

Define Interfaces

interface User {
  name: string
  email: string
  role: 'admin' | 'user'
}

interface Post {
  title: string
  content: string
  authorId: string
  published: boolean
}

Use Throughout

// Create with type
const user = await provider.create<User>('User', {
  name: 'Alice',
  email: 'alice@example.com',
  role: 'admin',
})

// Get with type
const retrieved = await provider.get<User>(user.id)
retrieved?.data.role  // 'admin' | 'user'

// List with type
const users = await provider.list<User>('User')
users[0].data.email  // Typed as string

// Update with type
const updated = await provider.update<User>(user.id, {
  role: 'user',
})

ListOptions

Control query behavior:

interface ListOptions {
  limit?: number                    // Max results (default: 100, max: 1000)
  offset?: number                   // Skip first N results
  where?: Record<string, unknown>   // Filter by field values
  orderBy?: string                  // Field to sort by
  order?: 'asc' | 'desc'            // Sort direction
}

Query Limits

To prevent memory exhaustion, queries have automatic limits:

// Default limit is 100
const posts = await provider.list('Post')
// Returns max 100 posts

// Maximum limit is 1000
const posts = await provider.list('Post', { limit: 5000 })
// Returns max 1000 posts

// Explicit limit respected up to max
const posts = await provider.list('Post', { limit: 50 })
// Returns max 50 posts

Common Patterns

Repository Pattern

class PostRepository {
  constructor(private provider: DigitalObjectsProvider) {}

  async create(data: Omit<Post, 'id'>): Promise<Thing<Post>> {
    return this.provider.create<Post>('Post', data)
  }

  async findById(id: string): Promise<Thing<Post> | null> {
    return this.provider.get<Post>(id)
  }

  async findPublished(): Promise<Thing<Post>[]> {
    return this.provider.find<Post>('Post', { published: true })
  }

  async publish(id: string): Promise<Thing<Post>> {
    return this.provider.update<Post>(id, { published: true })
  }
}

Soft Delete

interface Post {
  title: string
  content: string
  deletedAt?: string
}

// Soft delete
await provider.update<Post>(id, {
  deletedAt: new Date().toISOString(),
})

// Query only non-deleted
const posts = await provider.list<Post>('Post')
const active = posts.filter(p => !p.data.deletedAt)

Versioning

interface VersionedPost {
  title: string
  content: string
  version: number
}

// Create with version
await provider.create<VersionedPost>('Post', {
  title: 'Hello',
  content: 'World',
  version: 1,
})

// Increment version on update
const post = await provider.get<VersionedPost>(id)
if (post) {
  await provider.update<VersionedPost>(id, {
    content: 'Updated content',
    version: post.data.version + 1,
  })
}

Audit Fields

interface AuditedPost {
  title: string
  content: string
  createdBy: string
  updatedBy: string
}

// Create with audit fields
await provider.create<AuditedPost>('Post', {
  title: 'Hello',
  content: 'World',
  createdBy: currentUserId,
  updatedBy: currentUserId,
})

// Update with audit
await provider.update<AuditedPost>(id, {
  content: 'Updated',
  updatedBy: currentUserId,
})

Error Handling

Thing Not Found

const thing = await provider.get('nonexistent')
if (!thing) {
  throw new Error('Thing not found')
}

// Or with update
try {
  await provider.update('nonexistent', { title: 'New' })
} catch (error) {
  // Error: Thing not found: nonexistent
}

Type Mismatches

// TypeScript catches type errors at compile time
const post = await provider.create<Post>('Post', {
  title: 'Hello',
  // Error: missing 'content' field
})

Next Steps

  • Actions - Record events on things
  • Graph - Query relationships between things
  • Providers - Choose storage backend
Was this page helpful?

On this page