@happyvertical/smrt-properties 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,38 @@
1
+ # @happyvertical/smrt-properties
2
+
3
+ Digital properties (websites, apps) with hierarchical zones for content/ad placement.
4
+
5
+ ## Models
6
+
7
+ - **Property** (STI): `domain`, `url`, optional repository/owner links. Status: active/inactive/pending. Methods dynamically import/create ZoneCollection for lazy loading.
8
+ - **Zone**: hierarchical via `parentId`. `path`/`selector`/`dimensions` for placement. `allowedFormats` array. Tree operations: `getAncestors()`, `getDescendants()`, `getFullPath()`, `moveZone()` (with cycle detection).
9
+
10
+ ## ZoneCollection Key Methods
11
+
12
+ `getTree()` (builds nested structure), `moveZone()` (validates against descendant cycles), `deleteZone(cascade?)` (cascade=false orphans children to parent), `findWithGlobals(tenantId)`.
13
+
14
+ In-memory depth caching prevents N+1 queries during tree operations.
15
+
16
+ ## Property Key Methods
17
+
18
+ - `getZones()` / `getZoneTree()` / `createZone()` — ZoneCollection wrappers loaded lazily via dynamic import.
19
+ - `isActive()` — convenience status check.
20
+ - AI: `summarize()` (uses `smrtProperties.property.summarize` prompt via `@happyvertical/smrt-prompts`).
21
+
22
+ ## Prompt Registry
23
+
24
+ `Property.summarize()` is registered with `@happyvertical/smrt-prompts` so tenants can override the template/model/params at runtime:
25
+
26
+ ```typescript
27
+ import { smrtPropertiesSummarizePrompt } from '@happyvertical/smrt-properties';
28
+ // key: 'smrtProperties.property.summarize'
29
+ ```
30
+
31
+ Only non-PII fields are passed to the AI provider: `name`, `domain`, `description`, `status`, plus aggregate zone information (count + top-level zone names). Internal foreign-key fields (`ownerId`, `repositoryId`, `tenantId`) and the extensible `metadata` blob are intentionally excluded — `metadata` may contain analytics IDs or tenant-private configuration.
32
+
33
+ ## Gotchas
34
+
35
+ - **Empty allowedFormats = all formats**: empty array means no restrictions, not no formats
36
+ - **Zone dimensions nullable independently**: check `hasDimensions()` before using width/height
37
+ - **deleteZone(cascade=false)**: doesn't delete children — moves them to parent (orphan pattern)
38
+ - **Optional tenancy** on both models
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @happyvertical/smrt-properties
2
+
3
+ Digital property and hierarchical zone management for the SMRT framework. Properties represent websites, apps, or publications. Zones form an arbitrarily nested tree within each property for content and ad placement.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @happyvertical/smrt-properties
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import {
15
+ Property, PropertyCollection,
16
+ Zone, ZoneCollection
17
+ } from '@happyvertical/smrt-properties';
18
+
19
+ // Create a property
20
+ const properties = await PropertyCollection.create({ db });
21
+ const site = await properties.create({
22
+ name: 'Oak Creek News',
23
+ domain: 'oakcreeknews.com',
24
+ url: 'https://oakcreeknews.com',
25
+ status: 'active',
26
+ });
27
+ await site.save();
28
+
29
+ // Build a zone hierarchy: page -> section -> ad slot
30
+ const zones = await ZoneCollection.create({ db });
31
+ const homePage = await zones.create({
32
+ propertyId: site.id,
33
+ name: 'Home Page',
34
+ type: 'page',
35
+ path: '/',
36
+ });
37
+ await homePage.save();
38
+
39
+ const sidebar = await zones.create({
40
+ propertyId: site.id,
41
+ parentId: homePage.id,
42
+ name: 'Sidebar',
43
+ type: 'section',
44
+ selector: '.sidebar',
45
+ });
46
+ await sidebar.save();
47
+
48
+ const adSlot = await zones.create({
49
+ propertyId: site.id,
50
+ parentId: sidebar.id,
51
+ name: 'Sidebar Ad',
52
+ type: 'slot',
53
+ width: 300,
54
+ height: 250,
55
+ allowedFormats: ['image', 'html'],
56
+ });
57
+ await adSlot.save();
58
+
59
+ // Retrieve the full zone tree for a property
60
+ const tree = await zones.getTree(site.id);
61
+
62
+ // Move a zone (validates against descendant cycles)
63
+ await zones.moveZone(adSlot.id, homePage.id);
64
+
65
+ // Delete a zone — cascade=false moves children to parent
66
+ await zones.deleteZone(sidebar.id, false);
67
+ ```
68
+
69
+ ### Zone hierarchy
70
+
71
+ Zones use `parentId` self-referencing to form a tree. Common zone types include `page`, `section`, `slot`, `container`, and `widget`. Each zone can specify a URL `path` pattern, CSS `selector`, `position` hint, pixel `width`/`height`, and an `allowedFormats` array for filtering content or ad types. An empty `allowedFormats` array means all formats are allowed (no restrictions).
72
+
73
+ The `ZoneCollection` provides tree operations: `getTree()` builds a nested structure in memory, `getAncestors()`/`getDescendants()` walk the hierarchy, and `moveZone()` prevents cycles by checking descendants before reparenting. `deleteZone(cascade)` either removes descendants or orphans children to the deleted zone's parent.
74
+
75
+ ## API
76
+
77
+ ### Models
78
+
79
+ | Export | Description |
80
+ |--------|------------|
81
+ | `Property` | Digital property (STI) with domain, URL, status, and owner/repository links |
82
+ | `Zone` | Hierarchical zone within a property for content/ad placement |
83
+
84
+ ### Collections
85
+
86
+ | Export | Description |
87
+ |--------|------------|
88
+ | `PropertyCollection` | CRUD and queries for Property records |
89
+ | `ZoneCollection` | Zone CRUD plus tree ops: `getTree()`, `moveZone()`, `deleteZone()`, `getAncestors()`, `getDescendants()` |
90
+
91
+ ### Key Types
92
+
93
+ `PropertyOptions`, `PropertyStatus`, `ZoneOptions`, `ZoneTree`, `ZoneTreeNode`
94
+
95
+ ## Dependencies
96
+
97
+ - `@happyvertical/smrt-core` -- ORM and code generation
98
+ - `@happyvertical/smrt-tenancy` -- optional multi-tenant scoping
99
+ - Peer: `@happyvertical/smrt-profiles`, `@happyvertical/smrt-projects` (optional)
@@ -0,0 +1,431 @@
1
+ import { PromptDefinition } from '@happyvertical/smrt-prompts';
2
+ import { ResolvedPromptAI } from '@happyvertical/smrt-prompts';
3
+ import { SmrtCollection } from '@happyvertical/smrt-core';
4
+ import { SmrtHierarchical } from '@happyvertical/smrt-core';
5
+ import { SmrtObject } from '@happyvertical/smrt-core';
6
+ import { SmrtObjectOptions } from '@happyvertical/smrt-core';
7
+
8
+ export declare function promptMessageOptions(ai: ResolvedPromptAI): {
9
+ maxTokens?: number | undefined;
10
+ temperature?: number | undefined;
11
+ model?: string | undefined;
12
+ };
13
+
14
+ export declare class Property extends SmrtObject {
15
+ /**
16
+ * Tenant ID for multi-tenant isolation
17
+ */
18
+ tenantId: string | null;
19
+ /**
20
+ * Display name of the property
21
+ */
22
+ name: string;
23
+ /**
24
+ * Domain name (e.g., "oakcreeknews.com")
25
+ */
26
+ domain: string;
27
+ /**
28
+ * Full URL (e.g., "https://oakcreeknews.com")
29
+ */
30
+ url: string;
31
+ /**
32
+ * Property description
33
+ */
34
+ description: string;
35
+ /**
36
+ * Link to Repository (from smrt-projects)
37
+ * Optional - used when property is backed by a git repository
38
+ */
39
+ repositoryId: string | null;
40
+ /**
41
+ * Link to Profile (from smrt-profiles)
42
+ * Optional - owner/publisher of the property
43
+ */
44
+ ownerId: string | null;
45
+ /**
46
+ * Property status
47
+ */
48
+ status: PropertyStatus;
49
+ /**
50
+ * Extensible metadata (analytics IDs, config, etc.)
51
+ */
52
+ metadata: Record<string, unknown>;
53
+ constructor(options?: PropertyOptions);
54
+ /**
55
+ * Get all zones for this property
56
+ */
57
+ getZones(): Promise<Zone[]>;
58
+ /**
59
+ * Get zone tree for this property
60
+ */
61
+ getZoneTree(): Promise<ZoneTree<Zone>>;
62
+ /**
63
+ * Create a zone on this property
64
+ */
65
+ createZone(options: Omit<ZoneOptions, 'propertyId'>): Promise<Zone>;
66
+ /**
67
+ * Check if property is active
68
+ */
69
+ isActive(): boolean;
70
+ /**
71
+ * AI-powered: Generate a summary of the property and its zones.
72
+ *
73
+ * Uses the `smrtProperties.property.summarize` prompt registered via
74
+ * `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
75
+ * overrides of the template, model, and parameters at runtime.
76
+ *
77
+ * Only non-PII fields (name, domain, description, status) plus aggregate
78
+ * zone information are sent to the AI provider. Internal foreign-key
79
+ * fields and the extensible `metadata` blob are intentionally excluded.
80
+ *
81
+ * @returns Generated summary text
82
+ */
83
+ summarize(): Promise<string>;
84
+ }
85
+
86
+ export declare class PropertyCollection extends SmrtCollection<Property> {
87
+ static readonly _itemClass: typeof Property;
88
+ /**
89
+ * Find a property by domain
90
+ *
91
+ * @param domain - Domain name
92
+ * @returns Property or null
93
+ */
94
+ findByDomain(domain: string): Promise<Property | null>;
95
+ /**
96
+ * Find properties by repository ID
97
+ *
98
+ * @param repositoryId - Repository ID
99
+ * @returns Array of properties
100
+ */
101
+ findByRepository(repositoryId: string): Promise<Property[]>;
102
+ /**
103
+ * Find properties by owner ID
104
+ *
105
+ * @param ownerId - Owner profile ID
106
+ * @returns Array of properties
107
+ */
108
+ findByOwner(ownerId: string): Promise<Property[]>;
109
+ /**
110
+ * Find active properties
111
+ *
112
+ * @returns Array of active properties
113
+ */
114
+ findActive(): Promise<Property[]>;
115
+ /**
116
+ * Find properties by status
117
+ *
118
+ * @param status - Property status
119
+ * @returns Array of properties with the given status
120
+ */
121
+ findByStatus(status: PropertyStatus): Promise<Property[]>;
122
+ /**
123
+ * Get or create a property by domain
124
+ *
125
+ * @param domain - Domain name
126
+ * @param defaults - Default values for creation
127
+ * @returns Property (existing or newly created)
128
+ */
129
+ getOrCreateByDomain(domain: string, defaults?: {
130
+ name?: string;
131
+ url?: string;
132
+ repositoryId?: string | null;
133
+ ownerId?: string | null;
134
+ }): Promise<Property>;
135
+ /**
136
+ * Count properties by status
137
+ *
138
+ * @returns Object with counts per status
139
+ */
140
+ countByStatus(): Promise<Record<PropertyStatus, number>>;
141
+ /**
142
+ * Find properties belonging to a specific tenant
143
+ *
144
+ * @param tenantId - Tenant ID to filter by
145
+ * @returns Array of properties for the tenant
146
+ */
147
+ findByTenant(tenantId: string): Promise<Property[]>;
148
+ /**
149
+ * Find global properties (no tenant association)
150
+ *
151
+ * @returns Array of global properties
152
+ */
153
+ findGlobal(): Promise<Property[]>;
154
+ /**
155
+ * Find properties for a tenant including global properties
156
+ *
157
+ * @param tenantId - Tenant ID to filter by
158
+ * @returns Array of tenant-specific and global properties
159
+ */
160
+ findWithGlobals(tenantId: string): Promise<Property[]>;
161
+ }
162
+
163
+ /**
164
+ * Options for creating a Property instance
165
+ */
166
+ export declare interface PropertyOptions extends SmrtObjectOptions {
167
+ tenantId?: string | null;
168
+ name?: string;
169
+ domain?: string;
170
+ url?: string;
171
+ description?: string;
172
+ repositoryId?: string | null;
173
+ ownerId?: string | null;
174
+ status?: PropertyStatus;
175
+ metadata?: Record<string, unknown>;
176
+ }
177
+
178
+ /**
179
+ * Status of a digital property
180
+ */
181
+ export declare type PropertyStatus = 'active' | 'inactive' | 'pending';
182
+
183
+ export declare const smrtPropertiesSummarizePrompt: PromptDefinition;
184
+
185
+ export declare class Zone extends SmrtHierarchical {
186
+ /**
187
+ * Tenant ID for multi-tenant isolation
188
+ */
189
+ tenantId: string | null;
190
+ /**
191
+ * Parent property ID (required)
192
+ */
193
+ propertyId: string;
194
+ /**
195
+ * Display name
196
+ */
197
+ name: string;
198
+ /**
199
+ * Zone type (descriptive, not structural)
200
+ * Common values: "page", "section", "slot", "container", "widget"
201
+ */
202
+ type: string;
203
+ /**
204
+ * URL path pattern (e.g., "/", "/articles/*")
205
+ */
206
+ path: string;
207
+ /**
208
+ * CSS selector for placement (e.g., "#header", ".sidebar")
209
+ */
210
+ selector: string;
211
+ /**
212
+ * Position hint (e.g., "top", "after-paragraph-3")
213
+ */
214
+ position: string;
215
+ /**
216
+ * Width in pixels (null = fluid)
217
+ */
218
+ width: number | null;
219
+ /**
220
+ * Height in pixels (null = fluid)
221
+ */
222
+ height: number | null;
223
+ /**
224
+ * Allowed content/ad formats
225
+ */
226
+ allowedFormats: string[];
227
+ /**
228
+ * Default content/asset ID for fallback
229
+ */
230
+ defaultContentId: string | null;
231
+ /**
232
+ * Extensible metadata
233
+ */
234
+ metadata: Record<string, unknown>;
235
+ constructor(options?: ZoneOptions);
236
+ /**
237
+ * Check if this is a top-level zone (no parent)
238
+ */
239
+ isTopLevel(): boolean;
240
+ /**
241
+ * Check if this zone has dimensions set
242
+ */
243
+ hasDimensions(): boolean;
244
+ /**
245
+ * Get dimensions as string (e.g., "728x90")
246
+ */
247
+ getDimensionString(): string | null;
248
+ /**
249
+ * Get the parent property
250
+ */
251
+ getProperty(): Promise<Property | null>;
252
+ /**
253
+ * Get the full path from root to this zone
254
+ */
255
+ getFullPath(): Promise<string>;
256
+ /**
257
+ * Get the depth in the hierarchy (0 = top-level)
258
+ */
259
+ getDepth(): Promise<number>;
260
+ /**
261
+ * Create a child zone under this zone
262
+ */
263
+ createChild(options: Omit<ZoneOptions, 'propertyId' | 'parentId'>): Promise<Zone>;
264
+ /**
265
+ * Build tree node for this zone and its children
266
+ */
267
+ toTreeNode(): Promise<ZoneTreeNode<Zone>>;
268
+ /**
269
+ * Check if a format is allowed in this zone
270
+ */
271
+ isFormatAllowed(format: string): boolean;
272
+ }
273
+
274
+ export declare class ZoneCollection extends SmrtCollection<Zone> {
275
+ static readonly _itemClass: typeof Zone;
276
+ /**
277
+ * Find all zones for a property
278
+ *
279
+ * @param propertyId - Property ID
280
+ * @returns Array of zones
281
+ */
282
+ findByProperty(propertyId: string): Promise<Zone[]>;
283
+ /**
284
+ * Find top-level zones for a property (no parent)
285
+ *
286
+ * @param propertyId - Property ID
287
+ * @returns Array of top-level zones
288
+ */
289
+ findTopLevel(propertyId: string): Promise<Zone[]>;
290
+ /**
291
+ * Find direct children of a zone
292
+ *
293
+ * @param parentId - Parent zone ID
294
+ * @returns Array of child zones
295
+ */
296
+ findChildren(parentId: string): Promise<Zone[]>;
297
+ /**
298
+ * Find zones by path pattern
299
+ *
300
+ * @param propertyId - Property ID
301
+ * @param path - URL path to match
302
+ * @returns Array of matching zones
303
+ */
304
+ findByPath(propertyId: string, path: string): Promise<Zone[]>;
305
+ /**
306
+ * Find zones by type
307
+ *
308
+ * @param propertyId - Property ID
309
+ * @param type - Zone type
310
+ * @returns Array of zones of the given type
311
+ */
312
+ findByType(propertyId: string, type: string): Promise<Zone[]>;
313
+ /**
314
+ * Get the complete zone tree for a property
315
+ *
316
+ * @param propertyId - Property ID
317
+ * @returns ZoneTree structure
318
+ */
319
+ getTree(propertyId: string): Promise<ZoneTree<Zone>>;
320
+ /**
321
+ * Get all ancestors of a zone (path to root)
322
+ *
323
+ * @param zoneId - Zone ID
324
+ * @returns Array of ancestors (from root to parent)
325
+ */
326
+ getAncestors(zoneId: string): Promise<Zone[]>;
327
+ /**
328
+ * Get all descendants of a zone recursively
329
+ *
330
+ * @param zoneId - Zone ID
331
+ * @returns Array of all descendant zones
332
+ */
333
+ getDescendants(zoneId: string): Promise<Zone[]>;
334
+ /**
335
+ * Get the depth of the deepest zone in a property
336
+ *
337
+ * @param propertyId - Property ID
338
+ * @returns Maximum depth (0 = only top-level zones)
339
+ */
340
+ getMaxDepth(propertyId: string): Promise<number>;
341
+ /**
342
+ * Find zones with specific dimensions
343
+ *
344
+ * @param propertyId - Property ID
345
+ * @param width - Required width
346
+ * @param height - Required height
347
+ * @returns Array of matching zones
348
+ */
349
+ findByDimensions(propertyId: string, width: number, height: number): Promise<Zone[]>;
350
+ /**
351
+ * Find zones that allow a specific format
352
+ *
353
+ * @param propertyId - Property ID
354
+ * @param format - Format to check
355
+ * @returns Array of zones allowing the format
356
+ */
357
+ findByAllowedFormat(propertyId: string, format: string): Promise<Zone[]>;
358
+ /**
359
+ * Move a zone to a new parent
360
+ *
361
+ * @param zoneId - Zone ID to move
362
+ * @param newParentId - New parent ID (null for top-level)
363
+ * @returns Updated zone
364
+ */
365
+ moveZone(zoneId: string, newParentId: string | null): Promise<Zone | null>;
366
+ /**
367
+ * Delete a zone and optionally its descendants
368
+ *
369
+ * @param zoneId - Zone ID to delete
370
+ * @param cascade - Whether to delete descendants
371
+ * @returns Number of zones deleted
372
+ */
373
+ deleteZone(zoneId: string, cascade?: boolean): Promise<number>;
374
+ /**
375
+ * Find zones belonging to a specific tenant
376
+ *
377
+ * @param tenantId - Tenant ID to filter by
378
+ * @returns Array of zones for the tenant
379
+ */
380
+ findByTenant(tenantId: string): Promise<Zone[]>;
381
+ /**
382
+ * Find global zones (no tenant association)
383
+ *
384
+ * @returns Array of global zones
385
+ */
386
+ findGlobal(): Promise<Zone[]>;
387
+ /**
388
+ * Find zones for a tenant including global zones
389
+ *
390
+ * @param tenantId - Tenant ID to filter by
391
+ * @returns Array of tenant-specific and global zones
392
+ */
393
+ findWithGlobals(tenantId: string): Promise<Zone[]>;
394
+ }
395
+
396
+ /**
397
+ * Options for creating a Zone instance
398
+ */
399
+ export declare interface ZoneOptions extends SmrtObjectOptions {
400
+ tenantId?: string | null;
401
+ propertyId?: string;
402
+ parentId?: string | null;
403
+ name?: string;
404
+ type?: string;
405
+ path?: string;
406
+ selector?: string;
407
+ position?: string;
408
+ width?: number | null;
409
+ height?: number | null;
410
+ allowedFormats?: string[];
411
+ defaultContentId?: string | null;
412
+ metadata?: Record<string, unknown>;
413
+ }
414
+
415
+ /**
416
+ * Complete zone tree for a property
417
+ */
418
+ export declare interface ZoneTree<T = unknown> {
419
+ propertyId: string;
420
+ roots: ZoneTreeNode<T>[];
421
+ }
422
+
423
+ /**
424
+ * A node in the zone tree hierarchy
425
+ */
426
+ export declare interface ZoneTreeNode<T = unknown> {
427
+ zone: T;
428
+ children: ZoneTreeNode<T>[];
429
+ }
430
+
431
+ export { }