@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/dist/index.js ADDED
@@ -0,0 +1,776 @@
1
+ import { ObjectRegistry, crossPackageRef, smrt, SmrtObject, SmrtCollection, foreignKey, SmrtHierarchical } from "@happyvertical/smrt-core";
2
+ import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
3
+ import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
4
+ ObjectRegistry.registerPackageManifest(
5
+ new URL("./manifest.json", import.meta.url)
6
+ );
7
+ const smrtPropertiesSummarizePrompt = definePrompt({
8
+ key: "smrtProperties.property.summarize",
9
+ template: `Summarize this digital property:
10
+ Name: {propertyName}
11
+ Domain: {propertyDomain}
12
+ Description: {propertyDescription}
13
+ Status: {propertyStatus}
14
+ Total zones: {totalZones}
15
+ Top-level zones: {topLevelZones}`,
16
+ editable: {
17
+ template: true,
18
+ profile: true,
19
+ model: true,
20
+ params: true
21
+ }
22
+ });
23
+ function promptMessageOptions(ai) {
24
+ return {
25
+ ...ai.params || {},
26
+ ...ai.model ? { model: ai.model } : {},
27
+ ...typeof ai.temperature === "number" ? { temperature: ai.temperature } : {},
28
+ ...typeof ai.maxTokens === "number" ? { maxTokens: ai.maxTokens } : {}
29
+ };
30
+ }
31
+ var __defProp$1 = Object.defineProperty;
32
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
33
+ var __decorateClass$1 = (decorators, target, key, kind) => {
34
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
35
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
36
+ if (decorator = decorators[i])
37
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
38
+ if (kind && result) __defProp$1(target, key, result);
39
+ return result;
40
+ };
41
+ let Property = class extends SmrtObject {
42
+ tenantId = null;
43
+ /**
44
+ * Display name of the property
45
+ */
46
+ name = "";
47
+ /**
48
+ * Domain name (e.g., "oakcreeknews.com")
49
+ */
50
+ domain = "";
51
+ /**
52
+ * Full URL (e.g., "https://oakcreeknews.com")
53
+ */
54
+ url = "";
55
+ /**
56
+ * Property description
57
+ */
58
+ description = "";
59
+ repositoryId = null;
60
+ ownerId = null;
61
+ /**
62
+ * Property status
63
+ */
64
+ status = "active";
65
+ /**
66
+ * Extensible metadata (analytics IDs, config, etc.)
67
+ */
68
+ metadata = {};
69
+ constructor(options = {}) {
70
+ super(options);
71
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
72
+ if (options.name !== void 0) this.name = options.name;
73
+ if (options.domain !== void 0) this.domain = options.domain;
74
+ if (options.url !== void 0) this.url = options.url;
75
+ if (options.description !== void 0)
76
+ this.description = options.description;
77
+ if (options.repositoryId !== void 0)
78
+ this.repositoryId = options.repositoryId;
79
+ if (options.ownerId !== void 0) this.ownerId = options.ownerId;
80
+ if (options.status !== void 0) this.status = options.status;
81
+ if (options.metadata !== void 0) this.metadata = options.metadata;
82
+ }
83
+ /**
84
+ * Get all zones for this property
85
+ */
86
+ async getZones() {
87
+ if (!this.id) return [];
88
+ const { ZoneCollection: ZoneCollection2 } = await Promise.resolve().then(() => Zones);
89
+ const collection = await ZoneCollection2.create(this.options);
90
+ return await collection.findByProperty(this.id);
91
+ }
92
+ /**
93
+ * Get zone tree for this property
94
+ */
95
+ async getZoneTree() {
96
+ if (!this.id) return { propertyId: "", roots: [] };
97
+ const { ZoneCollection: ZoneCollection2 } = await Promise.resolve().then(() => Zones);
98
+ const collection = await ZoneCollection2.create(this.options);
99
+ return await collection.getTree(this.id);
100
+ }
101
+ /**
102
+ * Create a zone on this property
103
+ */
104
+ async createZone(options) {
105
+ if (!this.id) {
106
+ throw new Error("Property must be saved before creating zones");
107
+ }
108
+ const { ZoneCollection: ZoneCollection2 } = await Promise.resolve().then(() => Zones);
109
+ const collection = await ZoneCollection2.create(this.options);
110
+ const zone = await collection.create({
111
+ ...options,
112
+ propertyId: this.id
113
+ });
114
+ await zone.save();
115
+ return zone;
116
+ }
117
+ /**
118
+ * Check if property is active
119
+ */
120
+ isActive() {
121
+ return this.status === "active";
122
+ }
123
+ /**
124
+ * AI-powered: Generate a summary of the property and its zones.
125
+ *
126
+ * Uses the `smrtProperties.property.summarize` prompt registered via
127
+ * `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
128
+ * overrides of the template, model, and parameters at runtime.
129
+ *
130
+ * Only non-PII fields (name, domain, description, status) plus aggregate
131
+ * zone information are sent to the AI provider. Internal foreign-key
132
+ * fields and the extensible `metadata` blob are intentionally excluded.
133
+ *
134
+ * @returns Generated summary text
135
+ */
136
+ async summarize() {
137
+ const zones = await this.getZones();
138
+ const topLevelZones = zones.filter((z) => z.parentId === null);
139
+ const db = this.options.db ?? this.options.persistence;
140
+ const resolvedPrompt = await resolvePrompt(
141
+ smrtPropertiesSummarizePrompt.key,
142
+ {
143
+ db,
144
+ tenantId: this.tenantId,
145
+ variables: {
146
+ propertyName: this.name || "",
147
+ propertyDomain: this.domain || "",
148
+ propertyDescription: this.description || "",
149
+ propertyStatus: this.status || "",
150
+ totalZones: String(zones.length),
151
+ topLevelZones: topLevelZones.map((z) => z.name).join(", ")
152
+ }
153
+ }
154
+ );
155
+ const ai = await this.getAiClient();
156
+ const response = await ai.message(
157
+ resolvedPrompt.text,
158
+ promptMessageOptions(resolvedPrompt.ai)
159
+ );
160
+ return response.trim();
161
+ }
162
+ };
163
+ __decorateClass$1([
164
+ tenantId({ nullable: true })
165
+ ], Property.prototype, "tenantId", 2);
166
+ __decorateClass$1([
167
+ crossPackageRef("@happyvertical/smrt-projects:Repository")
168
+ ], Property.prototype, "repositoryId", 2);
169
+ __decorateClass$1([
170
+ crossPackageRef("@happyvertical/smrt-profiles:Profile")
171
+ ], Property.prototype, "ownerId", 2);
172
+ Property = __decorateClass$1([
173
+ TenantScoped({ mode: "optional" }),
174
+ smrt({
175
+ tableStrategy: "sti",
176
+ api: { include: ["list", "get", "create", "update", "delete"] },
177
+ mcp: { include: ["list", "get", "create"] },
178
+ cli: true
179
+ })
180
+ ], Property);
181
+ class PropertyCollection extends SmrtCollection {
182
+ static _itemClass = Property;
183
+ /**
184
+ * Find a property by domain
185
+ *
186
+ * @param domain - Domain name
187
+ * @returns Property or null
188
+ */
189
+ async findByDomain(domain) {
190
+ const results = await this.list({
191
+ where: { domain },
192
+ limit: 1
193
+ });
194
+ return results[0] || null;
195
+ }
196
+ /**
197
+ * Find properties by repository ID
198
+ *
199
+ * @param repositoryId - Repository ID
200
+ * @returns Array of properties
201
+ */
202
+ async findByRepository(repositoryId) {
203
+ return await this.list({
204
+ where: { repositoryId }
205
+ });
206
+ }
207
+ /**
208
+ * Find properties by owner ID
209
+ *
210
+ * @param ownerId - Owner profile ID
211
+ * @returns Array of properties
212
+ */
213
+ async findByOwner(ownerId) {
214
+ return await this.list({
215
+ where: { ownerId }
216
+ });
217
+ }
218
+ /**
219
+ * Find active properties
220
+ *
221
+ * @returns Array of active properties
222
+ */
223
+ async findActive() {
224
+ return await this.list({
225
+ where: { status: "active" }
226
+ });
227
+ }
228
+ /**
229
+ * Find properties by status
230
+ *
231
+ * @param status - Property status
232
+ * @returns Array of properties with the given status
233
+ */
234
+ async findByStatus(status) {
235
+ return await this.list({
236
+ where: { status }
237
+ });
238
+ }
239
+ /**
240
+ * Get or create a property by domain
241
+ *
242
+ * @param domain - Domain name
243
+ * @param defaults - Default values for creation
244
+ * @returns Property (existing or newly created)
245
+ */
246
+ async getOrCreateByDomain(domain, defaults = {}) {
247
+ let property = await this.findByDomain(domain);
248
+ if (!property) {
249
+ property = await this.create({
250
+ domain,
251
+ name: defaults.name || domain,
252
+ url: defaults.url || `https://${domain}`,
253
+ repositoryId: defaults.repositoryId ?? null,
254
+ ownerId: defaults.ownerId ?? null,
255
+ status: "active"
256
+ });
257
+ await property.save();
258
+ }
259
+ return property;
260
+ }
261
+ /**
262
+ * Count properties by status
263
+ *
264
+ * @returns Object with counts per status
265
+ */
266
+ async countByStatus() {
267
+ const all = await this.list({});
268
+ const counts = {
269
+ active: 0,
270
+ inactive: 0,
271
+ pending: 0
272
+ };
273
+ for (const prop of all) {
274
+ counts[prop.status]++;
275
+ }
276
+ return counts;
277
+ }
278
+ // ============================================
279
+ // Tenant Helper Methods
280
+ // ============================================
281
+ /**
282
+ * Find properties belonging to a specific tenant
283
+ *
284
+ * @param tenantId - Tenant ID to filter by
285
+ * @returns Array of properties for the tenant
286
+ */
287
+ async findByTenant(tenantId2) {
288
+ return this.list({ where: { tenantId: tenantId2 } });
289
+ }
290
+ /**
291
+ * Find global properties (no tenant association)
292
+ *
293
+ * @returns Array of global properties
294
+ */
295
+ async findGlobal() {
296
+ return this.list({ where: { tenantId: null } });
297
+ }
298
+ /**
299
+ * Find properties for a tenant including global properties
300
+ *
301
+ * @param tenantId - Tenant ID to filter by
302
+ * @returns Array of tenant-specific and global properties
303
+ */
304
+ async findWithGlobals(tenantId2) {
305
+ return this.query(
306
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
307
+ [tenantId2]
308
+ );
309
+ }
310
+ }
311
+ const Properties = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
312
+ __proto__: null,
313
+ PropertyCollection
314
+ }, Symbol.toStringTag, { value: "Module" }));
315
+ var __defProp = Object.defineProperty;
316
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
317
+ var __decorateClass = (decorators, target, key, kind) => {
318
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
319
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
320
+ if (decorator = decorators[i])
321
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
322
+ if (kind && result) __defProp(target, key, result);
323
+ return result;
324
+ };
325
+ let Zone = class extends SmrtHierarchical {
326
+ tenantId = null;
327
+ propertyId = "";
328
+ // parentId inherited from SmrtHierarchical (null = top-level zone)
329
+ /**
330
+ * Display name
331
+ */
332
+ name = "";
333
+ /**
334
+ * Zone type (descriptive, not structural)
335
+ * Common values: "page", "section", "slot", "container", "widget"
336
+ */
337
+ type = "";
338
+ /**
339
+ * URL path pattern (e.g., "/", "/articles/*")
340
+ */
341
+ path = "";
342
+ /**
343
+ * CSS selector for placement (e.g., "#header", ".sidebar")
344
+ */
345
+ selector = "";
346
+ /**
347
+ * Position hint (e.g., "top", "after-paragraph-3")
348
+ */
349
+ position = "";
350
+ /**
351
+ * Width in pixels (null = fluid)
352
+ */
353
+ width = null;
354
+ /**
355
+ * Height in pixels (null = fluid)
356
+ */
357
+ height = null;
358
+ /**
359
+ * Allowed content/ad formats
360
+ */
361
+ allowedFormats = [];
362
+ /**
363
+ * Default content/asset ID for fallback
364
+ */
365
+ defaultContentId = null;
366
+ /**
367
+ * Extensible metadata
368
+ */
369
+ metadata = {};
370
+ constructor(options = {}) {
371
+ super(options);
372
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
373
+ if (options.propertyId !== void 0) this.propertyId = options.propertyId;
374
+ if (options.parentId !== void 0) this.parentId = options.parentId;
375
+ if (options.name !== void 0) this.name = options.name;
376
+ if (options.type !== void 0) this.type = options.type;
377
+ if (options.path !== void 0) this.path = options.path;
378
+ if (options.selector !== void 0) this.selector = options.selector;
379
+ if (options.position !== void 0) this.position = options.position;
380
+ if (options.width !== void 0) this.width = options.width;
381
+ if (options.height !== void 0) this.height = options.height;
382
+ if (options.allowedFormats !== void 0)
383
+ this.allowedFormats = options.allowedFormats;
384
+ if (options.defaultContentId !== void 0)
385
+ this.defaultContentId = options.defaultContentId;
386
+ if (options.metadata !== void 0) this.metadata = options.metadata;
387
+ }
388
+ /**
389
+ * Check if this is a top-level zone (no parent)
390
+ */
391
+ isTopLevel() {
392
+ return this.parentId === null;
393
+ }
394
+ /**
395
+ * Check if this zone has dimensions set
396
+ */
397
+ hasDimensions() {
398
+ return this.width !== null && this.height !== null;
399
+ }
400
+ /**
401
+ * Get dimensions as string (e.g., "728x90")
402
+ */
403
+ getDimensionString() {
404
+ if (!this.hasDimensions()) return null;
405
+ return `${this.width}x${this.height}`;
406
+ }
407
+ /**
408
+ * Get the parent property
409
+ */
410
+ async getProperty() {
411
+ const { PropertyCollection: PropertyCollection2 } = await Promise.resolve().then(() => Properties);
412
+ const collection = await PropertyCollection2.create(this.options);
413
+ return await collection.get({ id: this.propertyId });
414
+ }
415
+ // Hierarchy traversal (getParent / getChildren / getAncestors /
416
+ // getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical.
417
+ // The collection-level depth cache in `ZoneCollection.getAncestors` /
418
+ // `getDescendants` remains available for callers that go through the
419
+ // collection — the base-class methods do their own batched queries.
420
+ /**
421
+ * Get the full path from root to this zone
422
+ */
423
+ async getFullPath() {
424
+ const ancestors = await this.getAncestors();
425
+ const names = [...ancestors.map((z) => z.name), this.name];
426
+ return names.join(" > ");
427
+ }
428
+ /**
429
+ * Get the depth in the hierarchy (0 = top-level)
430
+ */
431
+ async getDepth() {
432
+ const ancestors = await this.getAncestors();
433
+ return ancestors.length;
434
+ }
435
+ /**
436
+ * Create a child zone under this zone
437
+ */
438
+ async createChild(options) {
439
+ if (!this.id) {
440
+ throw new Error("Zone must be saved before creating children");
441
+ }
442
+ const { ZoneCollection: ZoneCollection2 } = await Promise.resolve().then(() => Zones);
443
+ const collection = await ZoneCollection2.create(this.options);
444
+ const zone = await collection.create({
445
+ ...options,
446
+ propertyId: this.propertyId,
447
+ parentId: this.id
448
+ });
449
+ await zone.save();
450
+ return zone;
451
+ }
452
+ /**
453
+ * Build tree node for this zone and its children
454
+ */
455
+ async toTreeNode() {
456
+ const children = await this.getChildren();
457
+ const childNodes = await Promise.all(children.map((c) => c.toTreeNode()));
458
+ return {
459
+ zone: this,
460
+ children: childNodes
461
+ };
462
+ }
463
+ /**
464
+ * Check if a format is allowed in this zone
465
+ */
466
+ isFormatAllowed(format) {
467
+ if (this.allowedFormats.length === 0) return true;
468
+ return this.allowedFormats.includes(format);
469
+ }
470
+ };
471
+ __decorateClass([
472
+ tenantId({ nullable: true })
473
+ ], Zone.prototype, "tenantId", 2);
474
+ __decorateClass([
475
+ foreignKey("Property")
476
+ ], Zone.prototype, "propertyId", 2);
477
+ Zone = __decorateClass([
478
+ TenantScoped({ mode: "optional" }),
479
+ smrt({
480
+ api: { include: ["list", "get", "create", "update", "delete"] },
481
+ mcp: { include: ["list", "get", "create"] },
482
+ cli: true
483
+ })
484
+ ], Zone);
485
+ class ZoneCollection extends SmrtCollection {
486
+ static _itemClass = Zone;
487
+ /**
488
+ * Find all zones for a property
489
+ *
490
+ * @param propertyId - Property ID
491
+ * @returns Array of zones
492
+ */
493
+ async findByProperty(propertyId) {
494
+ return await this.list({
495
+ where: { propertyId }
496
+ });
497
+ }
498
+ /**
499
+ * Find top-level zones for a property (no parent)
500
+ *
501
+ * @param propertyId - Property ID
502
+ * @returns Array of top-level zones
503
+ */
504
+ async findTopLevel(propertyId) {
505
+ return await this.list({
506
+ where: { propertyId, parentId: null }
507
+ });
508
+ }
509
+ /**
510
+ * Find direct children of a zone
511
+ *
512
+ * @param parentId - Parent zone ID
513
+ * @returns Array of child zones
514
+ */
515
+ async findChildren(parentId) {
516
+ return await this.list({
517
+ where: { parentId }
518
+ });
519
+ }
520
+ /**
521
+ * Find zones by path pattern
522
+ *
523
+ * @param propertyId - Property ID
524
+ * @param path - URL path to match
525
+ * @returns Array of matching zones
526
+ */
527
+ async findByPath(propertyId, path) {
528
+ return await this.list({
529
+ where: { propertyId, path }
530
+ });
531
+ }
532
+ /**
533
+ * Find zones by type
534
+ *
535
+ * @param propertyId - Property ID
536
+ * @param type - Zone type
537
+ * @returns Array of zones of the given type
538
+ */
539
+ async findByType(propertyId, type) {
540
+ return await this.list({
541
+ where: { propertyId, type }
542
+ });
543
+ }
544
+ /**
545
+ * Get the complete zone tree for a property
546
+ *
547
+ * @param propertyId - Property ID
548
+ * @returns ZoneTree structure
549
+ */
550
+ async getTree(propertyId) {
551
+ const allZones = await this.findByProperty(propertyId);
552
+ const zoneMap = /* @__PURE__ */ new Map();
553
+ const childrenMap = /* @__PURE__ */ new Map();
554
+ for (const zone of allZones) {
555
+ if (zone.id) {
556
+ zoneMap.set(zone.id, zone);
557
+ }
558
+ if (!childrenMap.has(zone.parentId || "")) {
559
+ childrenMap.set(zone.parentId || "", []);
560
+ }
561
+ childrenMap.get(zone.parentId || "")?.push(zone);
562
+ }
563
+ const buildNode = (zone) => {
564
+ const children = (zone.id ? childrenMap.get(zone.id) : []) || [];
565
+ return {
566
+ zone,
567
+ children: children.map(buildNode)
568
+ };
569
+ };
570
+ const roots = childrenMap.get("") || [];
571
+ return {
572
+ propertyId,
573
+ roots: roots.map(buildNode)
574
+ };
575
+ }
576
+ /**
577
+ * Get all ancestors of a zone (path to root)
578
+ *
579
+ * @param zoneId - Zone ID
580
+ * @returns Array of ancestors (from root to parent)
581
+ */
582
+ async getAncestors(zoneId) {
583
+ const ancestors = [];
584
+ const startZone = await this.get({ id: zoneId });
585
+ if (!startZone) return ancestors;
586
+ let currentId = startZone.parentId;
587
+ while (currentId) {
588
+ const parent = await this.get({ id: currentId });
589
+ if (!parent) break;
590
+ ancestors.unshift(parent);
591
+ currentId = parent.parentId;
592
+ }
593
+ return ancestors;
594
+ }
595
+ /**
596
+ * Get all descendants of a zone recursively
597
+ *
598
+ * @param zoneId - Zone ID
599
+ * @returns Array of all descendant zones
600
+ */
601
+ async getDescendants(zoneId) {
602
+ const descendants = [];
603
+ const queue = [zoneId];
604
+ while (queue.length > 0) {
605
+ const currentId = queue.shift();
606
+ if (!currentId) continue;
607
+ const children = await this.findChildren(currentId);
608
+ for (const child of children) {
609
+ descendants.push(child);
610
+ if (child.id) {
611
+ queue.push(child.id);
612
+ }
613
+ }
614
+ }
615
+ return descendants;
616
+ }
617
+ /**
618
+ * Get the depth of the deepest zone in a property
619
+ *
620
+ * @param propertyId - Property ID
621
+ * @returns Maximum depth (0 = only top-level zones)
622
+ */
623
+ async getMaxDepth(propertyId) {
624
+ const allZones = await this.findByProperty(propertyId);
625
+ const zonesById = /* @__PURE__ */ new Map();
626
+ for (const zone of allZones) {
627
+ if (zone.id) {
628
+ zonesById.set(zone.id, zone);
629
+ }
630
+ }
631
+ const depthCache = /* @__PURE__ */ new Map();
632
+ const getDepth = (zone) => {
633
+ if (!zone.id) return 0;
634
+ const cached = depthCache.get(zone.id);
635
+ if (cached !== void 0) return cached;
636
+ let depth = 0;
637
+ let current = zone;
638
+ const visited = /* @__PURE__ */ new Set();
639
+ while (current?.id) {
640
+ const parentId = current.parentId;
641
+ if (!parentId) break;
642
+ if (visited.has(parentId)) break;
643
+ visited.add(parentId);
644
+ const parent = zonesById.get(parentId);
645
+ if (!parent) break;
646
+ depth += 1;
647
+ current = parent;
648
+ }
649
+ depthCache.set(zone.id, depth);
650
+ return depth;
651
+ };
652
+ let maxDepth = 0;
653
+ for (const zone of allZones) {
654
+ maxDepth = Math.max(maxDepth, getDepth(zone));
655
+ }
656
+ return maxDepth;
657
+ }
658
+ /**
659
+ * Find zones with specific dimensions
660
+ *
661
+ * @param propertyId - Property ID
662
+ * @param width - Required width
663
+ * @param height - Required height
664
+ * @returns Array of matching zones
665
+ */
666
+ async findByDimensions(propertyId, width, height) {
667
+ return await this.list({
668
+ where: { propertyId, width, height }
669
+ });
670
+ }
671
+ /**
672
+ * Find zones that allow a specific format
673
+ *
674
+ * @param propertyId - Property ID
675
+ * @param format - Format to check
676
+ * @returns Array of zones allowing the format
677
+ */
678
+ async findByAllowedFormat(propertyId, format) {
679
+ const allZones = await this.findByProperty(propertyId);
680
+ return allZones.filter((zone) => zone.isFormatAllowed(format));
681
+ }
682
+ /**
683
+ * Move a zone to a new parent
684
+ *
685
+ * @param zoneId - Zone ID to move
686
+ * @param newParentId - New parent ID (null for top-level)
687
+ * @returns Updated zone
688
+ */
689
+ async moveZone(zoneId, newParentId) {
690
+ const zone = await this.get({ id: zoneId });
691
+ if (!zone) return null;
692
+ if (newParentId) {
693
+ const descendants = await this.getDescendants(zoneId);
694
+ const descendantIds = descendants.map((d) => d.id);
695
+ if (descendantIds.includes(newParentId)) {
696
+ throw new Error("Cannot move zone into one of its descendants");
697
+ }
698
+ }
699
+ zone.parentId = newParentId;
700
+ await zone.save();
701
+ return zone;
702
+ }
703
+ /**
704
+ * Delete a zone and optionally its descendants
705
+ *
706
+ * @param zoneId - Zone ID to delete
707
+ * @param cascade - Whether to delete descendants
708
+ * @returns Number of zones deleted
709
+ */
710
+ async deleteZone(zoneId, cascade = false) {
711
+ const zone = await this.get({ id: zoneId });
712
+ if (!zone) return 0;
713
+ let deleted = 0;
714
+ if (cascade) {
715
+ const descendants = await this.getDescendants(zoneId);
716
+ for (const desc of descendants) {
717
+ await desc.delete();
718
+ deleted++;
719
+ }
720
+ } else {
721
+ const children = await this.findChildren(zoneId);
722
+ for (const child of children) {
723
+ child.parentId = zone.parentId;
724
+ await child.save();
725
+ }
726
+ }
727
+ await zone.delete();
728
+ deleted++;
729
+ return deleted;
730
+ }
731
+ // ============================================
732
+ // Tenant Helper Methods
733
+ // ============================================
734
+ /**
735
+ * Find zones belonging to a specific tenant
736
+ *
737
+ * @param tenantId - Tenant ID to filter by
738
+ * @returns Array of zones for the tenant
739
+ */
740
+ async findByTenant(tenantId2) {
741
+ return this.list({ where: { tenantId: tenantId2 } });
742
+ }
743
+ /**
744
+ * Find global zones (no tenant association)
745
+ *
746
+ * @returns Array of global zones
747
+ */
748
+ async findGlobal() {
749
+ return this.list({ where: { tenantId: null } });
750
+ }
751
+ /**
752
+ * Find zones for a tenant including global zones
753
+ *
754
+ * @param tenantId - Tenant ID to filter by
755
+ * @returns Array of tenant-specific and global zones
756
+ */
757
+ async findWithGlobals(tenantId2) {
758
+ return this.query(
759
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
760
+ [tenantId2]
761
+ );
762
+ }
763
+ }
764
+ const Zones = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
765
+ __proto__: null,
766
+ ZoneCollection
767
+ }, Symbol.toStringTag, { value: "Module" }));
768
+ export {
769
+ Property,
770
+ PropertyCollection,
771
+ Zone,
772
+ ZoneCollection,
773
+ promptMessageOptions,
774
+ smrtPropertiesSummarizePrompt
775
+ };
776
+ //# sourceMappingURL=index.js.map