@happyvertical/smrt-properties

Digital properties (websites, apps) with hierarchical zones for content and ad placement.

v0.20.44PropertiesZones

Overview

smrt-properties manages digital properties (websites, apps, publications) and their hierarchical zones for content and ad placement. Property is STI-enabled with domain, URL, and optional repository/owner links. Zones form an arbitrarily nested tree within each property.

Installation

bash
npm install @happyvertical/smrt-properties

Quick Start

typescript
import { PropertyCollection, ZoneCollection } from '@happyvertical/smrt-properties';

// Initialize collections
const properties = await PropertyCollection.create({ db: {...} });
const zones = await ZoneCollection.create({ db: {...} });

// Create property
const site = await properties.create({
  name: 'Oak Creek News',
  domain: 'oakcreeknews.com',
  url: 'https://oakcreeknews.com',
  status: 'active'
});
await site.save();

// Create page zone
const homePage = await zones.create({
  propertyId: site.id,
  name: 'Home Page',
  type: 'page',
  path: '/'
});
await homePage.save();

// Create ad slot
const headerSlot = await zones.create({
  propertyId: site.id,
  parentId: homePage.id,
  name: 'Header Leaderboard',
  type: 'slot',
  width: 728,
  height: 90,
  allowedFormats: ['display', 'video']
});
await headerSlot.save();

// Get zone tree
const tree = await zones.getTree(site.id);
console.log(`Property has ${tree.roots.length} top-level zones`);

Core Concepts

Property Model

typescript
class Property extends SmrtObject {
  name: string
  domain: string
  url: string
  status: 'active' | 'inactive' | 'pending'
  ownerId?: string          // Optional profile link
  repositoryId?: string     // Optional project link
  metadata?: Record<string, any>

  async getZones(): Promise<Zone[]>
  async getZoneTree(): Promise<ZoneTree>
  async createZone(options): Promise<Zone>
  async summarize(): Promise<string>  // AI-powered
}

Zone Model

typescript
class Zone extends SmrtObject {
  propertyId: string
  parentId?: string         // Self-referencing hierarchy
  name: string
  type: string              // 'page', 'section', 'slot', 'container', 'widget'
  path?: string             // URL path pattern
  selector?: string         // CSS selector
  width?: number            // Nullable independently -- check hasDimensions()
  height?: number
  allowedFormats?: string[] // Empty array = all formats (no restrictions)
  metadata?: Record<string, any>

  // Hierarchy traversal
  async getAncestors(): Promise<Zone[]>
  async getDescendants(): Promise<Zone[]>
  async getFullPath(): string

  // Format/dimension helpers
  isFormatAllowed(format: string): boolean
  hasDimensions(): boolean   // Check before using width/height
  getDimensionString(): string
}

API Reference

PropertyCollection

typescript
await properties.findByDomain(domain: string)
await properties.findByOwner(ownerId: string)
await properties.findActive()
await properties.getOrCreateByDomain(domain, defaults)
await properties.countByStatus()

ZoneCollection

typescript
await zones.findByProperty(propertyId: string)
await zones.getTree(propertyId: string): Promise<ZoneTree>  // Builds nested structure
await zones.getAncestors(zoneId: string)
await zones.getDescendants(zoneId: string)
await zones.moveZone(zoneId: string, newParentId?: string)  // Validates against descendant cycles
await zones.deleteZone(zoneId: string, cascade: boolean)    // cascade=false orphans children to parent
await zones.findWithGlobals(tenantId: string)               // Tenant + global zones

Examples

Example 1: Multi-Zone Website

typescript
// Create property
const site = await properties.create({
  name: 'News Site',
  domain: 'news.com',
  status: 'active'
});
await site.save();

// Create pages
const home = await zones.create({
  propertyId: site.id,
  name: 'Home',
  type: 'page',
  path: '/'
});
await home.save();

const articles = await zones.create({
  propertyId: site.id,
  name: 'Articles',
  type: 'page',
  path: '/articles/*'
});
await articles.save();

// Create slots
const headerAd = await zones.create({
  propertyId: site.id,
  parentId: home.id,
  name: 'Header Ad',
  type: 'slot',
  width: 728,
  height: 90
});
await headerAd.save();

Example 2: Zone Traversal

typescript
// Get tree structure
const tree = await zones.getTree(site.id);

// Get full path
const path = await headerAd.getFullPath(); // "Home > Header Ad"

// Get ancestors
const ancestors = await headerAd.getAncestors(); // [home]

// Get all descendants of page
const slots = await home.getDescendants();

Example 3: Format Validation

typescript
// Configure allowed formats
const videoSlot = await zones.create({
  propertyId: site.id,
  name: 'Video Player',
  allowedFormats: ['video', 'native']
});
await videoSlot.save();

// Check compatibility
if (videoSlot.isFormatAllowed('video')) {
  console.log('Video ads allowed');
}

// Find zones by dimensions
const leaderboards = await zones.list({
  where: {
    propertyId: site.id,
    width: 728,
    height: 90
  }
});

Best Practices

✓ DOs

  • Save properties before creating zones
  • Cache zone trees in-memory for performance
  • Use moveZone() for reparenting (prevents cycles)
  • Validate formats with isFormatAllowed() before assignment
  • Use findByDimensions() to pre-filter ad slots

✗ DON'Ts

  • Don't manually set parentId to a descendant (causes cycles)
  • Don't delete properties without handling zones first
  • Don't assume unlimited nesting without depth checks
  • Don't query zones repeatedly in loops (use batch operations)
  • Don't skip save() after creation (id requires persistence)

Related Modules