Skip to main content

@have/places

Package Overview​

The @have/places package provides a robust and flexible system for managing hierarchical, geographical, and abstract locations. It is designed as a SMRT-specific module, integrating seamlessly with other SMRT framework packages like @have/content, @have/tags, @have/assets, and @have/profiles.

Package Type: SMRT-specific module Location: packages/places/ Build Tool: Vite with smrtPlugin Target: Node.js only Dependencies: @have/smrt, @have/utils

Design Notes​

This document outlines the database schema and API architecture for the @have/places package. The design prioritizes flexibility, consistency, and extensibility.

The key architectural decisions are:

  1. A Central places Table: A single table holds all location entities, from continents and countries to virtual rooms and abstract zones. A place_types table defines the nature of each place.

  2. Self-Referencing Hierarchy: A parent_id foreign key on the places table itself creates a simple and efficient hierarchical model. This allows for infinitely deep nesting of places (e.g., galaxy -> planet -> continent -> country -> region -> city -> building -> floor -> room).

  3. Flexible Geographic Data: A geo JSON field stores geographic data (e.g., latitude, longitude, GeoJSON polygons). This accommodates standard geo-coordinates for physical places while remaining optional for abstract or virtual locations.

  4. Primary Key Strategy: Follows the standard SmrtObject pattern of UUID-based primary keys with unique slug fields for lookup tables (place_types).

    • All tables use a UUID id as the primary key.
    • The place_types lookup table has a unique slug for human-readable lookups.
    • Convenience methods are provided for slug-based access (e.g., PlaceType.getBySlug()).
  5. Rich SMRT Integration: By modeling the schema as SmrtObjects, the package leverages AI-powered operations, automatic code generation, and a rich object-oriented API for interacting with places.

  6. Multi-Layer API: A comprehensive API design with instance methods, static methods, utility functions, and collection methods provides flexibility and avoids code duplication.


Usage Examples​

Below are examples of how to model common scenarios. Slugs like 'country', 'city', 'building', and 'room' are assumed to be predefined in the place_types table.

Example 1: Modeling a Physical Hierarchy​

// 1. Create a country, city, and building
const usa = await createPlace({ typeSlug: 'country', name: 'United States' });
const nyc = await createPlace({ typeSlug: 'city', name: 'New York City', parentId: usa.id });
const empireState = await createPlace({
typeSlug: 'building',
name: 'Empire State Building',
parentId: nyc.id,
geo: { lat: 40.7484, lng: -73.9857 }
});

// 2. Add floors and rooms to the building
const floor102 = await createPlace({ typeSlug: 'floor', name: '102nd Floor Observatory', parentId: empireState.id });
const giftShop = await createPlace({ typeSlug: 'room', name: 'Gift Shop', parentId: floor102.id });

// 3. Retrieve the full hierarchy
const ancestors = await giftShop.getAncestors();
// Returns: [floor102, empireState, nyc, usa]

const descendants = await usa.getDescendants();
// Returns: [nyc, empireState, floor102, giftShop]

Example 2: Finding Places and Associated Assets​

// 1. Find all places of a certain type
const allBuildings = await findPlacesByType('building');

// 2. Find places within a geographic area
const nearbyPlaces = await findPlacesByLocation(40.748, -73.985, { radius: 500 }); // 500m radius

// 3. Assuming an Asset object has a `placeId` foreign key
// Find all assets located in a specific place
const assetsInBuilding = await AssetCollection.create().find({ placeId: empireState.id });

SMRT Integration​

The @have/places data model can be seamlessly integrated with the @have/smrt framework, leveraging its powerful features.

SMRT Object Definitions​

Here's how the core tables can be represented as SmrtObject classes:

import { SmrtObject, SmrtCollection } from '@have/smrt';
import { text, foreignKey, oneToMany, json } from '@have/smrt/fields';

// Represents the place_types table
class PlaceType extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
slug = text({ unique: true, required: true }); // e.g., 'country', 'city', 'room'
name = text({ required: true });
description = text();

// Convenience method for slug-based lookup
static async getBySlug(slug: string): Promise<PlaceType | null> {
const collection = await PlaceTypeCollection.create();
return await collection.get({ slug });
}
}

// Represents the places table
class Place extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
typeId = foreignKey(PlaceType, { required: true });
parentId = foreignKey(Place, { nullable: true }); // Self-referencing for hierarchy
name = text({ required: true });
description = text();
geo = json(); // For lat/lng, GeoJSON, etc.

// Relationships to other SMRT modules
// These assume the other objects have a `placeId` foreign key
assets = oneToMany(Asset, { foreignKey: 'placeId' });
profiles = oneToMany(Profile, { foreignKey: 'placeId' });
content = oneToMany(Content, { foreignKey: 'placeId' });

// Convenience method to get type slug
async getTypeSlug(): Promise<string> {
const type = await this.loadRelated('typeId');
return type?.slug || '';
}

// Convenience method to set type by slug
async setTypeBySlug(slug: string): Promise<void> {
const type = await PlaceType.getBySlug(slug);
if (!type) throw new Error(`Place type '${slug}' not found`);
this.typeId = type.id;
}

// Hierarchy methods
async getParent(): Promise<Place | null> {
return await this.loadRelated('parentId');
}

async getChildren(): Promise<Place[]> {
const collection = await PlaceCollection.create();
return await collection.find({ parentId: this.id });
}

async getAncestors(): Promise<Place[]> {
// Implementation would traverse up the parentId chain
}

async getDescendants(): Promise<Place[]> {
// Implementation would traverse down the parentId chain (e.g., using a recursive CTE)
}

// AI-powered methods
async generateDescription() {
return await this.do(`Write a brief, engaging description for the place named "${this.name}".`);
}
}

SMRT Collection Classes​

We can define SmrtCollection classes to manage these objects:

class PlaceCollection extends SmrtCollection<Place> {
static readonly _itemClass = Place;

async findByType(typeSlug: string): Promise<Place[]> {
const placeType = await PlaceType.getBySlug(typeSlug);
if (!placeType) return [];
return await this.find({ typeId: placeType.id });
}
}

class PlaceTypeCollection extends SmrtCollection<PlaceType> {
static readonly _itemClass = PlaceType;
}

Core Tables​

place_types​

A lookup table that defines the nature of a place.

  • slug: (String, Primary Key) - A unique, human-readable identifier (e.g., 'country', 'region', 'building', 'room', 'zone').
  • name: (String) - A user-friendly display name (e.g., 'Country', 'Building').
  • description: (Text, Nullable) - A brief explanation of the place type.

places​

Stores the core information for any location, physical or abstract.

  • id: (UUID, Primary Key) - The unique identifier for the place.
  • type_slug: (String, Foreign Key to place_types.slug) - The type of entity this place represents.
  • parent_id: (UUID, Foreign Key to places.id, Nullable) - The parent place in the hierarchy.
  • name: (String) - The display name for the place.
  • description: (Text, Nullable) - A short description of the place.
  • geo: (JSON, Nullable) - Geographic data, such as {"lat": 40.7, "lng": -73.9} or a GeoJSON object.
  • created_at: (Datetime)
  • updated_at: (Datetime)

Data Integrity and Performance​

  • Cascading Deletes: Foreign key constraints should be configured with ON DELETE RESTRICT for parent_id to prevent accidental deletion of a place that still has children. Child places must be moved or deleted first.
  • Uniqueness Constraints: A unique constraint on (parent_id, name) could be considered to prevent sibling places from having the same name, but this may be too restrictive for some use cases.
  • Indexing: For optimal query performance, indexes should be created on parent_id, type_slug, and any keys within the geo JSON object that are frequently queried. A spatial index on the geo data is highly recommended if the database supports it.

Core Functions​

Hierarchy Functions​

  • getAncestors(placeId): Retrieves the full parent hierarchy for a place.
  • getDescendants(placeId): Retrieves all children, grandchildren, etc., for a place.
  • getChildren(placeId): Retrieves only the direct children of a place.
  • movePlace(placeId, newParentId): Updates the parent_id of a place to move it within the hierarchy.

Query Functions​

  • findPlacesByType(typeSlug): Finds all places of a specific type.
  • findPlacesByLocation({lat, lng}, {radius}): Finds all places within a certain radius of a geographic point.
  • findPlacesByGeo(geoQuery): Finds all places matching a more complex GeoJSON query (e.g., within, intersects).

API Layer Architecture​

The package provides a comprehensive, multi-layer API, mirroring the structure of other SMRT modules.

1. Instance Methods​

Direct manipulation on place objects:

const place = await places.get({ id: 'place-123' });

// Hierarchy operations
const parent = await place.getParent();
const children = await place.getChildren();
await place.move(newParentId);

// AI-powered operations
const description = await place.generateDescription();

2. Static Methods​

Class-level queries:

// Find places by type or location
const buildings = await Place.findByType('building');
const nearby = await Place.findByLocation({lat: 40.7, lng: -73.9}, {radius: 1000});

3. Utility Functions​

Standalone helpers in utils.ts:

import { getPlaceAncestors, findPlacesByLocation } from '@have/places/utils';

const ancestors = await getPlaceAncestors('place-123');
const nearby = await findPlacesByLocation({lat: 40.7, lng: -73.9}, {radius: 1000});

4. Collection Methods​

Extended batch operations on PlaceCollection:

const places = await PlaceCollection.create();

// Find by type using the collection method
const buildings = await places.findByType('building');

// Batch move places
await places.batchUpdate(
[{ id: 'place-1', parentId: 'new-parent' }, { id: 'place-2', parentId: 'new-parent' }]
);

Dependencies & SDK Integration​

The @have/places package is designed to be a central hub for location data, integrating with multiple other SDK packages.

  • @have/smrt (Required): Provides the core SmrtObject and SmrtCollection framework.
  • @have/utils (Required): Used for common utilities like slug generation.
  • @have/assets, @have/profiles, @have/content, @have/tags (Integration): These packages can link to places by adding a placeId foreign key to their main objects, establishing a one-to-many relationship where one place can have many assets, profiles, etc.

Package Structure​

packages/places/
├── src/
│ ├── models/
│ │ ├── Place.ts
│ │ ├── PlaceType.ts
│ │ ├── __tests__/
│ │ │ ├── Place.test.ts
│ │ └── index.ts
│ ├── collections/
│ │ ├── PlaceCollection.ts
│ │ ├── PlaceTypeCollection.ts
│ │ └── index.ts
│ ├── utils.ts
│ ├── utils.test.ts
│ ├── types.ts
│ └── index.ts
├── package.json
├── tsconfig.json
├── vite.config.ts
├── vitest.config.ts
├── SPEC.md
├── CLAUDE.md
└── README.md

Implementation Phases​

Phase 1: Foundation​

  • Package infrastructure (package.json, build configs).
  • Place and PlaceType model definitions with SMRT decorators.
  • Basic collection classes.

Phase 2: Core Functionality​

  • Instance methods for hierarchy traversal (getParent, getChildren).
  • Static query methods (findByType).
  • Utility functions for hierarchy and location-based searches.

Phase 3: Advanced Features​

  • Efficient getAncestors and getDescendants implementations (potentially using recursive SQL queries).
  • Advanced geo-query capabilities.
  • AI-powered methods on the Place object.

Phase 4: Testing & Documentation​

  • Comprehensive test coverage for hierarchy and geo-queries.
  • CLAUDE.md package documentation for AI agents.
  • README.md user guide.
  • Integration tests with other SDK packages.