@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:
-
A Central
placesTable: A single table holds all location entities, from continents and countries to virtual rooms and abstract zones. Aplace_typestable defines the nature of each place. -
Self-Referencing Hierarchy: A
parent_idforeign key on theplacestable 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). -
Flexible Geographic Data: A
geoJSON 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. -
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
idas the primary key. - The
place_typeslookup table has a uniqueslugfor human-readable lookups. - Convenience methods are provided for slug-based access (e.g.,
PlaceType.getBySlug()).
- All tables use a UUID
-
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. -
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 toplace_types.slug) - The type of entity this place represents.parent_id: (UUID, Foreign Key toplaces.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 RESTRICTforparent_idto 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 thegeoJSON 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 theparent_idof 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
SmrtObjectandSmrtCollectionframework. - @have/utils (Required): Used for common utilities like slug generation.
- @have/assets, @have/profiles, @have/content, @have/tags (Integration): These packages can link to
placesby adding aplaceIdforeign 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).
PlaceandPlaceTypemodel 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
getAncestorsandgetDescendantsimplementations (potentially using recursive SQL queries). - Advanced geo-query capabilities.
- AI-powered methods on the
Placeobject.
Phase 4: Testing & Documentation
- Comprehensive test coverage for hierarchy and geo-queries.
CLAUDE.mdpackage documentation for AI agents.README.mduser guide.- Integration tests with other SDK packages.