@happyvertical/smrt-tags 0.33.1 → 0.34.1

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.d.ts CHANGED
@@ -216,13 +216,20 @@ export declare class TagAliasCollection extends SmrtCollection<TagAlias> {
216
216
  */
217
217
  findByTenant(tenantId: string): Promise<TagAlias[]>;
218
218
  /**
219
- * Find all global (tenant-less) tag aliases
219
+ * Find all global (tenant-less) tag aliases.
220
+ *
221
+ * Routes through the shared tenant-global helper so it does not throw under
222
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
223
+ * flagged as an isolation violation). (#1600)
220
224
  *
221
225
  * @returns Array of global tag aliases with null tenantId
222
226
  */
223
227
  findGlobal(): Promise<TagAlias[]>;
224
228
  /**
225
- * Find tag aliases for a tenant including global aliases
229
+ * Find tag aliases for a tenant including global aliases.
230
+ *
231
+ * Fails closed if an active tenant context requests a different tenant's
232
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
226
233
  *
227
234
  * @param tenantId - The tenant ID to filter by
228
235
  * @returns Array of tag aliases for the tenant plus all global aliases
@@ -380,13 +387,20 @@ export declare class TagCollection extends SmrtCollection<Tag> {
380
387
  */
381
388
  findByTenant(tenantId: string): Promise<Tag[]>;
382
389
  /**
383
- * Find all global (tenant-less) tags
390
+ * Find all global (tenant-less) tags.
391
+ *
392
+ * Routes through the shared tenant-global helper so it does not throw under
393
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
394
+ * flagged as an isolation violation). (#1600)
384
395
  *
385
396
  * @returns Array of global tags with null tenantId
386
397
  */
387
398
  findGlobal(): Promise<Tag[]>;
388
399
  /**
389
- * Find tags for a tenant including global tags
400
+ * Find tags for a tenant including global tags.
401
+ *
402
+ * Fails closed if an active tenant context requests a different tenant's
403
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
390
404
  *
391
405
  * @param tenantId - The tenant ID to filter by
392
406
  * @returns Array of tags for the tenant plus all global tags
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ObjectRegistry, field, smrt, SmrtHierarchical, SmrtObject, SmrtCollection } from "@happyvertical/smrt-core";
2
- import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
2
+ import { tenantId, TenantScoped, queryGlobal, queryWithGlobals } from "@happyvertical/smrt-tenancy";
3
3
  import { calculateLevel, generateUniqueSlug, hasCircularReference, sanitizeSlug, validateSlug } from "./utils.js";
4
4
  ObjectRegistry.registerPackageManifest(
5
5
  new URL("./manifest.json", import.meta.url)
@@ -359,23 +359,31 @@ class TagAliasCollection extends SmrtCollection {
359
359
  return this.list({ where: { tenantId: tenantId2 } });
360
360
  }
361
361
  /**
362
- * Find all global (tenant-less) tag aliases
362
+ * Find all global (tenant-less) tag aliases.
363
+ *
364
+ * Routes through the shared tenant-global helper so it does not throw under
365
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
366
+ * flagged as an isolation violation). (#1600)
363
367
  *
364
368
  * @returns Array of global tag aliases with null tenantId
365
369
  */
366
370
  async findGlobal() {
367
- return this.list({ where: { tenantId: null } });
371
+ return queryGlobal(this);
368
372
  }
369
373
  /**
370
- * Find tag aliases for a tenant including global aliases
374
+ * Find tag aliases for a tenant including global aliases.
375
+ *
376
+ * Fails closed if an active tenant context requests a different tenant's
377
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
371
378
  *
372
379
  * @param tenantId - The tenant ID to filter by
373
380
  * @returns Array of tag aliases for the tenant plus all global aliases
374
381
  */
375
382
  async findWithGlobals(tenantId2) {
376
- return this.query(
377
- `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
378
- [tenantId2]
383
+ return queryWithGlobals(
384
+ this,
385
+ tenantId2,
386
+ "TagAlias.findWithGlobals"
379
387
  );
380
388
  }
381
389
  }
@@ -701,24 +709,28 @@ class TagCollection extends SmrtCollection {
701
709
  return this.list({ where: { tenantId: tenantId2 } });
702
710
  }
703
711
  /**
704
- * Find all global (tenant-less) tags
712
+ * Find all global (tenant-less) tags.
713
+ *
714
+ * Routes through the shared tenant-global helper so it does not throw under
715
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
716
+ * flagged as an isolation violation). (#1600)
705
717
  *
706
718
  * @returns Array of global tags with null tenantId
707
719
  */
708
720
  async findGlobal() {
709
- return this.list({ where: { tenantId: null } });
721
+ return queryGlobal(this);
710
722
  }
711
723
  /**
712
- * Find tags for a tenant including global tags
724
+ * Find tags for a tenant including global tags.
725
+ *
726
+ * Fails closed if an active tenant context requests a different tenant's
727
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
713
728
  *
714
729
  * @param tenantId - The tenant ID to filter by
715
730
  * @returns Array of tags for the tenant plus all global tags
716
731
  */
717
732
  async findWithGlobals(tenantId2) {
718
- return this.query(
719
- `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
720
- [tenantId2]
721
- );
733
+ return queryWithGlobals(this, tenantId2, "Tag.findWithGlobals");
722
734
  }
723
735
  }
724
736
  const tags = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/__smrt-register__.ts","../src/tag.ts","../src/tag-alias.ts","../src/tag-aliases.ts","../src/tags.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// `new URL('./manifest.json', import.meta.url)` resolves at runtime to the\n// manifest sitting next to this module's compiled output. Vite warns at build\n// time that it cannot pre-resolve the URL; that is the intended behavior —\n// the URL must resolve to dist/manifest.json at runtime, not be inlined.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Tag model - Core entity for tagging with hierarchy and context support\n *\n * Central table for tag definitions with optional parent-child relationships\n * for taxonomies and category trees.\n */\n\nimport { field, SmrtHierarchical, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { TagMetadata, TagOptions } from './types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get', 'create', 'update'] },\n cli: true,\n})\nexport class Tag extends SmrtHierarchical {\n // id: UUID (auto-generated by SmrtObject)\n protected _slug = ''; // Unique identifier\n protected _context = 'global'; // Namespace/grouping\n\n // Override SmrtObject accessors\n override get slug(): string {\n return this._slug;\n }\n override set slug(value: string) {\n this._slug = value;\n }\n\n override get context(): string {\n return this._context;\n }\n override set context(value: string) {\n this._context = value;\n }\n\n @field({ required: true })\n name: string = ''; // Display name\n\n // parentId inherited from SmrtHierarchical (UUID, nullable). Stores the\n // parent Tag's id (not its slug) — Tag's slug+context natural key remains\n // the public identifier on TagCollection's API, but the FK is now UUID\n // for consistency with Place / Event / Account / Zone.\n level: number = 0; // Hierarchy depth (0 = root)\n description: string = ''; // Optional description\n metadata: string = ''; // JSON metadata stored as text\n\n // Tenancy\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n // Timestamps\n createdAt: Date = new Date();\n updatedAt: Date = new Date();\n\n constructor(options: TagOptions = {}) {\n super(options);\n if (options.name) this.name = options.name;\n if (options.parentId !== undefined)\n this.parentId = options.parentId ?? null;\n if (options.slug) this._slug = options.slug;\n if (options.context !== undefined) this._context = options.context;\n if (options.level !== undefined) this.level = options.level;\n if (options.description !== undefined)\n this.description = options.description;\n\n // Handle metadata - can be object or JSON string\n if (options.metadata !== undefined) {\n if (typeof options.metadata === 'string') {\n this.metadata = options.metadata;\n } else {\n this.metadata = JSON.stringify(options.metadata);\n }\n }\n }\n\n /**\n * Get metadata as parsed object\n *\n * @returns Parsed metadata object or empty object if no metadata\n */\n getMetadata(): TagMetadata {\n const metadataValue = String(this.metadata || '');\n if (!metadataValue) return {};\n try {\n return JSON.parse(metadataValue);\n } catch {\n return {};\n }\n }\n\n /**\n * Set metadata from object\n *\n * @param data - Metadata object to store\n */\n setMetadata(data: TagMetadata): void {\n this.metadata = JSON.stringify(data);\n }\n\n /**\n * Update metadata by merging with existing values\n *\n * @param updates - Partial metadata to merge\n */\n updateMetadata(updates: Partial<TagMetadata>): void {\n const current = this.getMetadata();\n this.setMetadata({ ...current, ...updates });\n }\n\n // Hierarchy traversal (getParent / getChildren / getAncestors /\n // getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical\n // against the inherited UUID `parentId`. TagCollection wraps these in\n // slug-friendly methods (moveTag / mergeTag / getChildren etc.) so the\n // existing public API surface keeps working for callers that prefer\n // slug references.\n\n /**\n * Convenience method for slug-based lookup\n *\n * @param slug - The slug to search for\n * @param context - Optional context filter\n * @returns Tag instance or null if not found\n */\n static async getBySlug(\n _slug: string,\n _context?: string,\n ): Promise<Tag | null> {\n // Will be auto-implemented by SMRT\n return null;\n }\n\n /**\n * Get root tags (no parent) for a context\n *\n * @param context - The context to filter by\n * @returns Array of root tags\n */\n static async getRootTags(_context: string = 'global'): Promise<Tag[]> {\n // Will be auto-implemented by SMRT\n return [];\n }\n}\n","/**\n * TagAlias model - Alternative names and translations for tags\n *\n * Stores aliases, variations, and multi-language translations for tags.\n * Supports language-neutral aliases and context-scoped variations.\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { Tag } from './tag';\nimport type { TagAliasOptions } from './types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: true,\n})\nexport class TagAlias extends SmrtObject {\n // id: UUID (auto-generated by SmrtObject)\n tagSlug: string = ''; // FK to Tag.slug\n\n @field({ required: true })\n alias: string = ''; // Alternative name or translation\n\n language: string = ''; // ISO 639-1 language code (nullable)\n protected _context = ''; // Optional context scoping (nullable)\n\n // Override SmrtObject accessor\n override get context(): string {\n return this._context;\n }\n override set context(value: string) {\n this._context = value;\n }\n\n // Tenancy\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n // Timestamps\n createdAt: Date = new Date();\n\n constructor(options: TagAliasOptions = {}) {\n super(options);\n if (options.tagSlug !== undefined) this.tagSlug = options.tagSlug;\n if (options.alias) this.alias = options.alias;\n if (options.language !== undefined) this.language = options.language;\n if (options.context !== undefined) this._context = options.context;\n }\n\n /**\n * Get the tag this alias belongs to\n *\n * @returns Tag instance or null if not found\n */\n async getTag(): Promise<Tag | null> {\n const { TagCollection } = await import('./tags');\n const collection = await (TagCollection as any).create(this.options);\n\n return await collection.get({ slug: this.tagSlug });\n }\n\n /**\n * Search tags by alias\n *\n * @param alias - The alias to search for\n * @param language - Optional language filter\n * @returns Array of matching tags\n */\n static async searchByAlias(\n _alias: string,\n _language?: string,\n ): Promise<Tag[]> {\n // Will be auto-implemented by SMRT\n return [];\n }\n\n /**\n * Get all aliases for a tag\n *\n * @param tagSlug - The tag slug to get aliases for\n * @returns Array of TagAlias instances\n */\n static async getAliasesForTag(_tagSlug: string): Promise<TagAlias[]> {\n // Will be auto-implemented by SMRT\n return [];\n }\n}\n","/**\n * TagAliasCollection - Collection manager for TagAlias objects\n *\n * Provides alias management, multi-language search, and bulk operations.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport type { Tag } from './tag';\nimport { TagAlias } from './tag-alias';\n\nexport class TagAliasCollection extends SmrtCollection<TagAlias> {\n static readonly _itemClass = TagAlias;\n\n /**\n * Add an alias to a tag (get or create)\n *\n * @param tagSlug - The tag slug\n * @param alias - The alias text\n * @param language - Optional language code\n * @param context - Optional context\n * @returns TagAlias instance\n */\n async addAlias(\n tagSlug: string,\n alias: string,\n language?: string,\n context?: string,\n ): Promise<TagAlias> {\n // Check if alias already exists\n const where: any = { tagSlug, alias };\n if (language) where.language = language;\n if (context) where.context = context;\n\n const existing = await this.list({ where, limit: 1 });\n if (existing.length > 0) {\n return existing[0];\n }\n\n // Create new alias\n return await this.create({\n tagSlug,\n alias,\n language,\n context,\n });\n }\n\n /**\n * Search tags by alias\n *\n * @param alias - The alias to search for\n * @param language - Optional language filter\n * @returns Array of matching tags\n */\n async searchByAlias(alias: string, language?: string): Promise<Tag[]> {\n const where: any = { alias };\n if (language) where.language = language;\n\n const aliases = await this.list({ where });\n const tagSlugs = [...new Set(aliases.map((a) => a.tagSlug))];\n\n const { TagCollection } = await import('./tags');\n const tagCollection = await (TagCollection as any).create(this.options);\n\n const tags: Tag[] = [];\n for (const slug of tagSlugs) {\n const tag = await tagCollection.get({ slug });\n if (tag) tags.push(tag);\n }\n\n return tags;\n }\n\n /**\n * Get all aliases for a tag\n *\n * @param tagSlug - The tag slug\n * @param language - Optional language filter\n * @returns Array of TagAlias instances\n */\n async getAliasesForTag(\n tagSlug: string,\n language?: string,\n ): Promise<TagAlias[]> {\n const where: any = { tagSlug };\n if (language) where.language = language;\n\n return await this.list({ where });\n }\n\n /**\n * Remove an alias by ID\n *\n * @param aliasId - The alias UUID\n */\n async removeAlias(aliasId: string): Promise<void> {\n const alias = await this.get({ id: aliasId });\n if (alias) {\n await alias.delete();\n }\n }\n\n /**\n * Bulk add aliases to a tag\n *\n * @param tagSlug - The tag slug\n * @param aliases - Array of alias configurations\n * @returns Array of created TagAlias instances\n */\n async bulkAddAliases(\n tagSlug: string,\n aliases: Array<{\n alias: string;\n language?: string;\n context?: string;\n }>,\n ): Promise<TagAlias[]> {\n const created: TagAlias[] = [];\n\n for (const aliasData of aliases) {\n const tagAlias = await this.addAlias(\n tagSlug,\n aliasData.alias,\n aliasData.language,\n aliasData.context,\n );\n created.push(tagAlias);\n }\n\n return created;\n }\n\n /**\n * Get aliases grouped by language\n *\n * @param tagSlug - The tag slug\n * @returns Map of language code to array of aliases\n */\n async getAliasesByLanguage(tagSlug: string): Promise<Map<string, string[]>> {\n const aliases = await this.getAliasesForTag(tagSlug);\n const grouped = new Map<string, string[]>();\n\n for (const alias of aliases) {\n const lang = String(alias.language || 'default');\n if (!grouped.has(lang)) {\n grouped.set(lang, []);\n }\n grouped.get(lang)?.push(String(alias.alias));\n }\n\n return grouped;\n }\n\n /**\n * Find matching aliases (case-insensitive partial match)\n *\n * Note: This is a simple implementation. For production use,\n * consider using full-text search or fuzzy matching.\n *\n * @param query - The search query\n * @param language - Optional language filter\n * @returns Array of matching TagAlias instances\n */\n async findMatchingAliases(\n query: string,\n language?: string,\n ): Promise<TagAlias[]> {\n const where: any = {};\n if (language) where.language = language;\n\n const all = await this.list({ where });\n const queryLower = query.toLowerCase();\n\n return all.filter((alias) =>\n String(alias.alias).toLowerCase().includes(queryLower),\n );\n }\n\n // =========================================================================\n // Tenant Helper Methods\n // =========================================================================\n\n /**\n * Find all tag aliases belonging to a specific tenant\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tag aliases for the specified tenant\n */\n async findByTenant(tenantId: string): Promise<TagAlias[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global (tenant-less) tag aliases\n *\n * @returns Array of global tag aliases with null tenantId\n */\n async findGlobal(): Promise<TagAlias[]> {\n return this.list({ where: { tenantId: null } });\n }\n\n /**\n * Find tag aliases for a tenant including global aliases\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tag aliases for the tenant plus all global aliases\n */\n async findWithGlobals(tenantId: string): Promise<TagAlias[]> {\n return this.query(\n `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,\n [tenantId],\n );\n }\n}\n","/**\n * TagCollection - Collection manager for Tag objects\n *\n * Public methods continue to accept slug strings for ergonomic call sites\n * (declarative tag-tree seeds, CLI tools, etc.), but the underlying FK is\n * now `Tag.parentId` (UUID, inherited from `SmrtHierarchical`). Each public\n * method resolves slugs to ids internally before mutating storage.\n *\n * Slug resolution is context-aware: Tag's natural key is `(slug, context)`,\n * not slug alone. Every slug-resolving method accepts an optional `context`\n * parameter. When omitted, the resolver fails fast if the slug is\n * ambiguous across contexts rather than silently picking one row.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Tag } from './tag';\nimport type { TagHierarchy } from './types';\n\nexport class TagCollection extends SmrtCollection<Tag> {\n static readonly _itemClass = Tag;\n\n /**\n * Get or create a tag with context\n *\n * @param slug - Tag slug\n * @param context - Tag context (default: 'global')\n * @returns Tag instance\n */\n async getOrCreate(slug: string, context: string = 'global'): Promise<Tag> {\n // First try to find existing tag with this slug and context\n const existing = await this.list({\n where: { slug, context },\n limit: 1,\n });\n\n if (existing.length > 0) {\n return existing[0];\n }\n\n // Create new tag\n return await this.create({\n slug,\n name: slug.replace(/-/g, ' ').replace(/\\b\\w/g, (l) => l.toUpperCase()),\n context,\n level: 0,\n });\n }\n\n /**\n * Resolve a tag by slug, optionally scoped to a context.\n *\n * Tags are identified by `(slug, context)`. When `context` is omitted\n * and the slug exists in more than one context, this throws a clear\n * ambiguity error rather than silently picking the first matching row.\n * Callers that know their context should pass it; callers that work in\n * a single-context world can leave it off.\n *\n * @returns The matching Tag, or `null` if nothing matches.\n * @throws Error if `context` is omitted and the slug is ambiguous.\n */\n private async resolveBySlug(\n slug: string,\n context?: string,\n ): Promise<Tag | null> {\n const where: { slug: string; context?: string } = { slug };\n if (context !== undefined) where.context = context;\n const matches = await this.list({ where, limit: 2 });\n if (matches.length === 0) return null;\n if (matches.length > 1) {\n throw new Error(\n `Tag slug '${slug}' is ambiguous: resolves to ${matches.length}+ rows across contexts. Pass an explicit \\`context\\` argument.`,\n );\n }\n return matches[0];\n }\n\n /**\n * List tags by context with optional parent filtering by slug.\n *\n * @param context - The context to filter by\n * @param parentSlug - Optional parent slug to filter children. Pass an\n * empty string or `null` to find root tags; pass a slug to find that\n * tag's immediate children. Typed as `string | null` so TypeScript\n * callers can pass `null` without a cast — the `null` and `''` paths\n * are both treated as \"roots only\".\n * @returns Array of matching tags\n */\n async listByContext(\n context: string,\n parentSlug?: string | null,\n ): Promise<Tag[]> {\n const where: any = { context };\n if (parentSlug === '' || parentSlug === null) {\n where.parentId = null;\n } else if (parentSlug !== undefined) {\n const parent = await this.get({ slug: parentSlug, context });\n if (!parent?.id) return [];\n where.parentId = parent.id;\n }\n return await this.list({ where });\n }\n\n /**\n * Get root tags (no parent) for a context\n *\n * @param context - The context to filter by (default: 'global')\n * @returns Array of root tags\n */\n async getRootTags(context: string = 'global'): Promise<Tag[]> {\n return await this.list({\n where: { context, parentId: null },\n });\n }\n\n /**\n * Get immediate children of a parent tag, looked up by slug.\n *\n * @param parentSlug - The parent tag slug\n * @param context - Optional context for the parent lookup. When omitted,\n * the parent slug must be unambiguous across contexts (throws if not).\n * @returns Array of child tags, or `[]` if the parent slug doesn't\n * resolve. Children are filtered to the resolved parent's context so\n * cross-context children don't leak in.\n */\n async getChildren(parentSlug: string, context?: string): Promise<Tag[]> {\n const parent = await this.resolveBySlug(parentSlug, context);\n if (!parent?.id) return [];\n return await this.list({\n where: { parentId: parent.id, context: parent.context },\n });\n }\n\n /**\n * Get tag hierarchy (all ancestors and descendants)\n *\n * @param slug - The tag slug\n * @param context - Optional context for the slug lookup. When omitted,\n * the slug must be unambiguous across contexts.\n * @returns Object with ancestors, current tag, and descendants\n */\n async getHierarchy(slug: string, context?: string): Promise<TagHierarchy> {\n const tag = await this.resolveBySlug(slug, context);\n if (!tag) throw new Error(`Tag '${slug}' not found`);\n\n const [ancestors, descendants] = await Promise.all([\n tag.getAncestors() as Promise<Tag[]>,\n tag.getDescendants() as Promise<Tag[]>,\n ]);\n\n return { ancestors, current: tag, descendants };\n }\n\n /**\n * Move a tag to a new parent. Slug-based API; UUIDs resolved internally.\n *\n * Cycle detection is inlined here (mirroring `SmrtHierarchical.moveTo`'s\n * self-loop + descendant checks) so that both `parentId` and the\n * denormalised `level` field can be persisted in a single `save()`.\n * Delegating to `moveTo` would write `parentId` first and `level` in a\n * second save — if the second save failed, the tag would be left with\n * the new parent but a stale level, breaking the depth cache.\n *\n * After the moved tag persists, descendant levels are recalculated\n * recursively via `updateDescendantLevels`.\n *\n * @param slug - The tag to move\n * @param newParentSlug - The new parent slug (null for root)\n * @param context - Optional context. When provided, both source and new\n * parent are resolved within it. When omitted, both slugs must be\n * unambiguous across contexts; the resolver throws otherwise.\n * @throws Error if either slug fails to resolve, if either slug is\n * ambiguous across contexts (no context provided), if source and new\n * parent live in different contexts, or if the move would create a\n * cycle.\n */\n async moveTag(\n slug: string,\n newParentSlug: string | null,\n context?: string,\n ): Promise<void> {\n const tag = await this.resolveBySlug(slug, context);\n if (!tag) throw new Error(`Tag '${slug}' not found`);\n\n let newParent: Tag | null = null;\n if (newParentSlug) {\n // Resolve the new parent in the SAME context as the tag we're\n // moving. If the caller passed a context, use it; otherwise pin\n // to the source tag's context so we don't drift across contexts\n // on the second lookup.\n newParent = await this.resolveBySlug(\n newParentSlug,\n context ?? tag.context,\n );\n if (!newParent) {\n throw new Error(`Tag '${newParentSlug}' not found`);\n }\n if (newParent.context !== tag.context) {\n throw new Error(\n `Cannot move Tag '${slug}' (context '${tag.context}') under '${newParentSlug}' (context '${newParent.context}') — contexts must match.`,\n );\n }\n }\n const newParentId = newParent?.id ?? null;\n\n if (newParentId !== null && newParentId === tag.id) {\n throw new Error(`Cannot move Tag ${tag.id} to itself.`);\n }\n if (newParentId !== null) {\n const descendants = (await tag.getDescendants()) as Tag[];\n if (descendants.some((d) => d.id === newParentId)) {\n throw new Error(\n `Cannot move Tag ${tag.id} under one of its own descendants (${newParentId}) — would create a cycle.`,\n );\n }\n }\n\n tag.parentId = newParentId;\n tag.level = newParent ? newParent.level + 1 : 0;\n await tag.save();\n await this.updateDescendantLevels(tag);\n }\n\n /**\n * Merge one tag into another (updates all references)\n *\n * Reparents `fromTag`'s direct children onto `toTag` and recalculates\n * their `level` field plus the level of every descendant — without\n * this, children moved from a different depth would carry stale\n * levels relative to their new parent. `TagAlias.tagSlug` references\n * are also rewritten, then `fromTag` is deleted.\n *\n * Note: Consuming packages are responsible for updating their own\n * join tables (e.g. `asset_tags`).\n *\n * @param fromSlug - The tag to merge from\n * @param toSlug - The tag to merge into\n * @param context - Optional context. When provided, both tags are\n * resolved within it. When omitted, both slugs must be unambiguous\n * across contexts.\n * @throws Error if either slug fails to resolve, if either slug is\n * ambiguous, or if the two tags live in different contexts.\n */\n async mergeTag(\n fromSlug: string,\n toSlug: string,\n context?: string,\n ): Promise<void> {\n const fromTag = await this.resolveBySlug(fromSlug, context);\n const toTag = await this.resolveBySlug(toSlug, context ?? fromTag?.context);\n\n if (!fromTag) throw new Error(`Source tag '${fromSlug}' not found`);\n if (!toTag) throw new Error(`Target tag '${toSlug}' not found`);\n if (!fromTag.id) throw new Error(`Source tag '${fromSlug}' has no id`);\n if (!toTag.id) throw new Error(`Target tag '${toSlug}' has no id`);\n if (fromTag.context !== toTag.context) {\n throw new Error(\n `Cannot merge '${fromSlug}' (context '${fromTag.context}') into '${toSlug}' (context '${toTag.context}') — contexts must match.`,\n );\n }\n if (fromTag.id === toTag.id) {\n throw new Error(`Cannot merge tag '${fromSlug}' into itself.`);\n }\n\n // Codex round-4 finding: refuse to merge into a descendant. Without\n // this guard the children-reparenting loop below could attach\n // `toTag` (or its ancestor) under itself, producing a cycle; the\n // recursive `updateDescendantLevels` walk has no visited set and\n // would stack-overflow before the corruption is observable.\n const fromDescendants = (await fromTag.getDescendants()) as Tag[];\n if (fromDescendants.some((d) => d.id === toTag.id)) {\n throw new Error(\n `Cannot merge '${fromSlug}' into '${toSlug}' — target is a descendant of source (would create a cycle).`,\n );\n }\n\n // Move all direct children of fromTag to toTag. Each child needs its\n // own `level` recomputed because toTag's depth may differ from\n // fromTag's depth (the children were positioned relative to fromTag's\n // level; after reparenting they hang off toTag instead). Then walk\n // each child's own subtree to propagate the level shift down.\n const children = await this.list({\n where: { parentId: fromTag.id },\n });\n const newChildLevel = toTag.level + 1;\n for (const child of children) {\n child.parentId = toTag.id;\n child.level = newChildLevel;\n await child.save();\n await this.updateDescendantLevels(child);\n }\n\n // Copy aliases from fromTag to toTag.\n // R3-B follow-up (codex caught this across multiple rounds): scope\n // the alias rewrite to the merge's resolved context, not the bare\n // slug. Otherwise `mergeTag('foo', 'bar', 'blog')` rewrites\n // `foo`'s aliases in EVERY context (forum, etc.), corrupting tag\n // data outside the requested merge scope.\n //\n // Round-8 codex follow-up: `Tag._context` defaults to `'global'`\n // but `TagAlias._context` defaults to `''` (and `addAlias()`\n // doesn't backfill the default), so a strict equality filter\n // would orphan aliases that were created without an explicit\n // context whenever the merge happens at the default 'global'\n // scope.\n //\n // Round-8 fix (initial): widen to `[fromTag.context, '']` —\n // BUT codex + copilot caught that this overcorrected. For a\n // non-default merge like `mergeTag('foo','bar','blog')`, the\n // unscoped (`''`) aliases for slug `foo` belong to a DIFFERENT\n // tag entirely (the `global/foo` row that the blog merge isn't\n // touching). Widening unconditionally re-introduced the\n // cross-context corruption round-7 was guarding against.\n //\n // Round-8 fix (final): only include `''` when the merge is at\n // the default context — that's the case where the\n // addAlias/Tag default-mismatch would orphan rows. For any\n // other context, the strict equality from round-7 is correct.\n const { TagAliasCollection } = await import('./tag-aliases');\n const aliasCollection = await (TagAliasCollection as any).create(\n this.options,\n );\n\n const aliasContexts: string[] = [fromTag.context];\n if (fromTag.context === 'global') {\n aliasContexts.push('');\n }\n const aliases = await aliasCollection.list({\n where: { tagSlug: fromSlug, context: aliasContexts },\n });\n for (const alias of aliases) {\n alias.tagSlug = toSlug;\n await alias.save();\n }\n\n // Delete the fromTag\n await fromTag.delete();\n }\n\n /**\n * Remove tags with no references (cleanup unused tags)\n *\n * Note: This requires consuming packages to provide usage information.\n * By default, only removes tags with no children and no aliases.\n *\n * @param context - Optional context to filter cleanup\n */\n async cleanupUnused(context?: string): Promise<number> {\n const where: any = {};\n if (context) where.context = context;\n\n const tags = await this.list({ where });\n const { TagAliasCollection } = await import('./tag-aliases');\n const aliasCollection = await (TagAliasCollection as any).create(\n this.options,\n );\n\n let deletedCount = 0;\n\n for (const tag of tags) {\n if (!tag.id) continue;\n\n // Check if tag has children (by parentId — UUID).\n const children = await this.list({\n where: { parentId: tag.id },\n limit: 1,\n });\n if (children.length > 0) continue;\n\n // Check if tag has aliases (still slug-keyed on TagAlias.tagSlug).\n const aliases = await aliasCollection.list({\n where: { tagSlug: tag.slug },\n limit: 1,\n });\n if (aliases.length > 0) continue;\n\n // No children, no aliases - safe to delete\n await tag.delete();\n deletedCount++;\n }\n\n return deletedCount;\n }\n\n /**\n * Calculate hierarchy level for a tag, looking the parent up by slug.\n *\n * @param parentSlug - The parent tag slug (null/empty for root)\n * @param context - Optional context for the parent lookup. When\n * omitted, the parent slug must be unambiguous across contexts.\n * @returns The calculated level (root parent → 1, missing parent → 0)\n */\n async calculateLevel(\n parentSlug: string | null,\n context?: string,\n ): Promise<number> {\n if (!parentSlug) return 0;\n\n const parent = await this.resolveBySlug(parentSlug, context);\n if (!parent) return 0;\n\n return parent.level + 1;\n }\n\n /**\n * Update levels for all descendants after moving a tag\n *\n * @param tag - The tag that was moved\n */\n private async updateDescendantLevels(tag: Tag): Promise<void> {\n if (!tag.id) return;\n const children = await this.list({\n where: { parentId: tag.id },\n });\n\n for (const child of children) {\n child.level = tag.level + 1;\n await child.save();\n await this.updateDescendantLevels(child); // Recursive\n }\n }\n\n // =========================================================================\n // Tenant Helper Methods\n // =========================================================================\n\n /**\n * Find all tags belonging to a specific tenant\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tags for the specified tenant\n */\n async findByTenant(tenantId: string): Promise<Tag[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global (tenant-less) tags\n *\n * @returns Array of global tags with null tenantId\n */\n async findGlobal(): Promise<Tag[]> {\n return this.list({ where: { tenantId: null } });\n }\n\n /**\n * Find tags for a tenant including global tags\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tags for the tenant plus all global tags\n */\n async findWithGlobals(tenantId: string): Promise<Tag[]> {\n return this.query(\n `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,\n [tenantId],\n );\n }\n}\n"],"names":["__decorateClass","TagCollection","tags","tenantId","TagAliasCollection"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/__smrt-register__.ts","../src/tag.ts","../src/tag-alias.ts","../src/tag-aliases.ts","../src/tags.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// `new URL('./manifest.json', import.meta.url)` resolves at runtime to the\n// manifest sitting next to this module's compiled output. Vite warns at build\n// time that it cannot pre-resolve the URL; that is the intended behavior —\n// the URL must resolve to dist/manifest.json at runtime, not be inlined.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Tag model - Core entity for tagging with hierarchy and context support\n *\n * Central table for tag definitions with optional parent-child relationships\n * for taxonomies and category trees.\n */\n\nimport { field, SmrtHierarchical, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { TagMetadata, TagOptions } from './types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get', 'create', 'update'] },\n cli: true,\n})\nexport class Tag extends SmrtHierarchical {\n // id: UUID (auto-generated by SmrtObject)\n protected _slug = ''; // Unique identifier\n protected _context = 'global'; // Namespace/grouping\n\n // Override SmrtObject accessors\n override get slug(): string {\n return this._slug;\n }\n override set slug(value: string) {\n this._slug = value;\n }\n\n override get context(): string {\n return this._context;\n }\n override set context(value: string) {\n this._context = value;\n }\n\n @field({ required: true })\n name: string = ''; // Display name\n\n // parentId inherited from SmrtHierarchical (UUID, nullable). Stores the\n // parent Tag's id (not its slug) — Tag's slug+context natural key remains\n // the public identifier on TagCollection's API, but the FK is now UUID\n // for consistency with Place / Event / Account / Zone.\n level: number = 0; // Hierarchy depth (0 = root)\n description: string = ''; // Optional description\n metadata: string = ''; // JSON metadata stored as text\n\n // Tenancy\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n // Timestamps\n createdAt: Date = new Date();\n updatedAt: Date = new Date();\n\n constructor(options: TagOptions = {}) {\n super(options);\n if (options.name) this.name = options.name;\n if (options.parentId !== undefined)\n this.parentId = options.parentId ?? null;\n if (options.slug) this._slug = options.slug;\n if (options.context !== undefined) this._context = options.context;\n if (options.level !== undefined) this.level = options.level;\n if (options.description !== undefined)\n this.description = options.description;\n\n // Handle metadata - can be object or JSON string\n if (options.metadata !== undefined) {\n if (typeof options.metadata === 'string') {\n this.metadata = options.metadata;\n } else {\n this.metadata = JSON.stringify(options.metadata);\n }\n }\n }\n\n /**\n * Get metadata as parsed object\n *\n * @returns Parsed metadata object or empty object if no metadata\n */\n getMetadata(): TagMetadata {\n const metadataValue = String(this.metadata || '');\n if (!metadataValue) return {};\n try {\n return JSON.parse(metadataValue);\n } catch {\n return {};\n }\n }\n\n /**\n * Set metadata from object\n *\n * @param data - Metadata object to store\n */\n setMetadata(data: TagMetadata): void {\n this.metadata = JSON.stringify(data);\n }\n\n /**\n * Update metadata by merging with existing values\n *\n * @param updates - Partial metadata to merge\n */\n updateMetadata(updates: Partial<TagMetadata>): void {\n const current = this.getMetadata();\n this.setMetadata({ ...current, ...updates });\n }\n\n // Hierarchy traversal (getParent / getChildren / getAncestors /\n // getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical\n // against the inherited UUID `parentId`. TagCollection wraps these in\n // slug-friendly methods (moveTag / mergeTag / getChildren etc.) so the\n // existing public API surface keeps working for callers that prefer\n // slug references.\n\n /**\n * Convenience method for slug-based lookup\n *\n * @param slug - The slug to search for\n * @param context - Optional context filter\n * @returns Tag instance or null if not found\n */\n static async getBySlug(\n _slug: string,\n _context?: string,\n ): Promise<Tag | null> {\n // Will be auto-implemented by SMRT\n return null;\n }\n\n /**\n * Get root tags (no parent) for a context\n *\n * @param context - The context to filter by\n * @returns Array of root tags\n */\n static async getRootTags(_context: string = 'global'): Promise<Tag[]> {\n // Will be auto-implemented by SMRT\n return [];\n }\n}\n","/**\n * TagAlias model - Alternative names and translations for tags\n *\n * Stores aliases, variations, and multi-language translations for tags.\n * Supports language-neutral aliases and context-scoped variations.\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { Tag } from './tag';\nimport type { TagAliasOptions } from './types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: true,\n})\nexport class TagAlias extends SmrtObject {\n // id: UUID (auto-generated by SmrtObject)\n tagSlug: string = ''; // FK to Tag.slug\n\n @field({ required: true })\n alias: string = ''; // Alternative name or translation\n\n language: string = ''; // ISO 639-1 language code (nullable)\n protected _context = ''; // Optional context scoping (nullable)\n\n // Override SmrtObject accessor\n override get context(): string {\n return this._context;\n }\n override set context(value: string) {\n this._context = value;\n }\n\n // Tenancy\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n // Timestamps\n createdAt: Date = new Date();\n\n constructor(options: TagAliasOptions = {}) {\n super(options);\n if (options.tagSlug !== undefined) this.tagSlug = options.tagSlug;\n if (options.alias) this.alias = options.alias;\n if (options.language !== undefined) this.language = options.language;\n if (options.context !== undefined) this._context = options.context;\n }\n\n /**\n * Get the tag this alias belongs to\n *\n * @returns Tag instance or null if not found\n */\n async getTag(): Promise<Tag | null> {\n const { TagCollection } = await import('./tags');\n const collection = await (TagCollection as any).create(this.options);\n\n return await collection.get({ slug: this.tagSlug });\n }\n\n /**\n * Search tags by alias\n *\n * @param alias - The alias to search for\n * @param language - Optional language filter\n * @returns Array of matching tags\n */\n static async searchByAlias(\n _alias: string,\n _language?: string,\n ): Promise<Tag[]> {\n // Will be auto-implemented by SMRT\n return [];\n }\n\n /**\n * Get all aliases for a tag\n *\n * @param tagSlug - The tag slug to get aliases for\n * @returns Array of TagAlias instances\n */\n static async getAliasesForTag(_tagSlug: string): Promise<TagAlias[]> {\n // Will be auto-implemented by SMRT\n return [];\n }\n}\n","/**\n * TagAliasCollection - Collection manager for TagAlias objects\n *\n * Provides alias management, multi-language search, and bulk operations.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport type { Tag } from './tag';\nimport { TagAlias } from './tag-alias';\n\nexport class TagAliasCollection extends SmrtCollection<TagAlias> {\n static readonly _itemClass = TagAlias;\n\n /**\n * Add an alias to a tag (get or create)\n *\n * @param tagSlug - The tag slug\n * @param alias - The alias text\n * @param language - Optional language code\n * @param context - Optional context\n * @returns TagAlias instance\n */\n async addAlias(\n tagSlug: string,\n alias: string,\n language?: string,\n context?: string,\n ): Promise<TagAlias> {\n // Check if alias already exists\n const where: any = { tagSlug, alias };\n if (language) where.language = language;\n if (context) where.context = context;\n\n const existing = await this.list({ where, limit: 1 });\n if (existing.length > 0) {\n return existing[0];\n }\n\n // Create new alias\n return await this.create({\n tagSlug,\n alias,\n language,\n context,\n });\n }\n\n /**\n * Search tags by alias\n *\n * @param alias - The alias to search for\n * @param language - Optional language filter\n * @returns Array of matching tags\n */\n async searchByAlias(alias: string, language?: string): Promise<Tag[]> {\n const where: any = { alias };\n if (language) where.language = language;\n\n const aliases = await this.list({ where });\n const tagSlugs = [...new Set(aliases.map((a) => a.tagSlug))];\n\n const { TagCollection } = await import('./tags');\n const tagCollection = await (TagCollection as any).create(this.options);\n\n const tags: Tag[] = [];\n for (const slug of tagSlugs) {\n const tag = await tagCollection.get({ slug });\n if (tag) tags.push(tag);\n }\n\n return tags;\n }\n\n /**\n * Get all aliases for a tag\n *\n * @param tagSlug - The tag slug\n * @param language - Optional language filter\n * @returns Array of TagAlias instances\n */\n async getAliasesForTag(\n tagSlug: string,\n language?: string,\n ): Promise<TagAlias[]> {\n const where: any = { tagSlug };\n if (language) where.language = language;\n\n return await this.list({ where });\n }\n\n /**\n * Remove an alias by ID\n *\n * @param aliasId - The alias UUID\n */\n async removeAlias(aliasId: string): Promise<void> {\n const alias = await this.get({ id: aliasId });\n if (alias) {\n await alias.delete();\n }\n }\n\n /**\n * Bulk add aliases to a tag\n *\n * @param tagSlug - The tag slug\n * @param aliases - Array of alias configurations\n * @returns Array of created TagAlias instances\n */\n async bulkAddAliases(\n tagSlug: string,\n aliases: Array<{\n alias: string;\n language?: string;\n context?: string;\n }>,\n ): Promise<TagAlias[]> {\n const created: TagAlias[] = [];\n\n for (const aliasData of aliases) {\n const tagAlias = await this.addAlias(\n tagSlug,\n aliasData.alias,\n aliasData.language,\n aliasData.context,\n );\n created.push(tagAlias);\n }\n\n return created;\n }\n\n /**\n * Get aliases grouped by language\n *\n * @param tagSlug - The tag slug\n * @returns Map of language code to array of aliases\n */\n async getAliasesByLanguage(tagSlug: string): Promise<Map<string, string[]>> {\n const aliases = await this.getAliasesForTag(tagSlug);\n const grouped = new Map<string, string[]>();\n\n for (const alias of aliases) {\n const lang = String(alias.language || 'default');\n if (!grouped.has(lang)) {\n grouped.set(lang, []);\n }\n grouped.get(lang)?.push(String(alias.alias));\n }\n\n return grouped;\n }\n\n /**\n * Find matching aliases (case-insensitive partial match)\n *\n * Note: This is a simple implementation. For production use,\n * consider using full-text search or fuzzy matching.\n *\n * @param query - The search query\n * @param language - Optional language filter\n * @returns Array of matching TagAlias instances\n */\n async findMatchingAliases(\n query: string,\n language?: string,\n ): Promise<TagAlias[]> {\n const where: any = {};\n if (language) where.language = language;\n\n const all = await this.list({ where });\n const queryLower = query.toLowerCase();\n\n return all.filter((alias) =>\n String(alias.alias).toLowerCase().includes(queryLower),\n );\n }\n\n // =========================================================================\n // Tenant Helper Methods\n // =========================================================================\n\n /**\n * Find all tag aliases belonging to a specific tenant\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tag aliases for the specified tenant\n */\n async findByTenant(tenantId: string): Promise<TagAlias[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global (tenant-less) tag aliases.\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Array of global tag aliases with null tenantId\n */\n async findGlobal(): Promise<TagAlias[]> {\n return queryGlobal<TagAlias>(this);\n }\n\n /**\n * Find tag aliases for a tenant including global aliases.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tag aliases for the tenant plus all global aliases\n */\n async findWithGlobals(tenantId: string): Promise<TagAlias[]> {\n return queryWithGlobals<TagAlias>(\n this,\n tenantId,\n 'TagAlias.findWithGlobals',\n );\n }\n}\n","/**\n * TagCollection - Collection manager for Tag objects\n *\n * Public methods continue to accept slug strings for ergonomic call sites\n * (declarative tag-tree seeds, CLI tools, etc.), but the underlying FK is\n * now `Tag.parentId` (UUID, inherited from `SmrtHierarchical`). Each public\n * method resolves slugs to ids internally before mutating storage.\n *\n * Slug resolution is context-aware: Tag's natural key is `(slug, context)`,\n * not slug alone. Every slug-resolving method accepts an optional `context`\n * parameter. When omitted, the resolver fails fast if the slug is\n * ambiguous across contexts rather than silently picking one row.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport { Tag } from './tag';\nimport type { TagHierarchy } from './types';\n\nexport class TagCollection extends SmrtCollection<Tag> {\n static readonly _itemClass = Tag;\n\n /**\n * Get or create a tag with context\n *\n * @param slug - Tag slug\n * @param context - Tag context (default: 'global')\n * @returns Tag instance\n */\n async getOrCreate(slug: string, context: string = 'global'): Promise<Tag> {\n // First try to find existing tag with this slug and context\n const existing = await this.list({\n where: { slug, context },\n limit: 1,\n });\n\n if (existing.length > 0) {\n return existing[0];\n }\n\n // Create new tag\n return await this.create({\n slug,\n name: slug.replace(/-/g, ' ').replace(/\\b\\w/g, (l) => l.toUpperCase()),\n context,\n level: 0,\n });\n }\n\n /**\n * Resolve a tag by slug, optionally scoped to a context.\n *\n * Tags are identified by `(slug, context)`. When `context` is omitted\n * and the slug exists in more than one context, this throws a clear\n * ambiguity error rather than silently picking the first matching row.\n * Callers that know their context should pass it; callers that work in\n * a single-context world can leave it off.\n *\n * @returns The matching Tag, or `null` if nothing matches.\n * @throws Error if `context` is omitted and the slug is ambiguous.\n */\n private async resolveBySlug(\n slug: string,\n context?: string,\n ): Promise<Tag | null> {\n const where: { slug: string; context?: string } = { slug };\n if (context !== undefined) where.context = context;\n const matches = await this.list({ where, limit: 2 });\n if (matches.length === 0) return null;\n if (matches.length > 1) {\n throw new Error(\n `Tag slug '${slug}' is ambiguous: resolves to ${matches.length}+ rows across contexts. Pass an explicit \\`context\\` argument.`,\n );\n }\n return matches[0];\n }\n\n /**\n * List tags by context with optional parent filtering by slug.\n *\n * @param context - The context to filter by\n * @param parentSlug - Optional parent slug to filter children. Pass an\n * empty string or `null` to find root tags; pass a slug to find that\n * tag's immediate children. Typed as `string | null` so TypeScript\n * callers can pass `null` without a cast — the `null` and `''` paths\n * are both treated as \"roots only\".\n * @returns Array of matching tags\n */\n async listByContext(\n context: string,\n parentSlug?: string | null,\n ): Promise<Tag[]> {\n const where: any = { context };\n if (parentSlug === '' || parentSlug === null) {\n where.parentId = null;\n } else if (parentSlug !== undefined) {\n const parent = await this.get({ slug: parentSlug, context });\n if (!parent?.id) return [];\n where.parentId = parent.id;\n }\n return await this.list({ where });\n }\n\n /**\n * Get root tags (no parent) for a context\n *\n * @param context - The context to filter by (default: 'global')\n * @returns Array of root tags\n */\n async getRootTags(context: string = 'global'): Promise<Tag[]> {\n return await this.list({\n where: { context, parentId: null },\n });\n }\n\n /**\n * Get immediate children of a parent tag, looked up by slug.\n *\n * @param parentSlug - The parent tag slug\n * @param context - Optional context for the parent lookup. When omitted,\n * the parent slug must be unambiguous across contexts (throws if not).\n * @returns Array of child tags, or `[]` if the parent slug doesn't\n * resolve. Children are filtered to the resolved parent's context so\n * cross-context children don't leak in.\n */\n async getChildren(parentSlug: string, context?: string): Promise<Tag[]> {\n const parent = await this.resolveBySlug(parentSlug, context);\n if (!parent?.id) return [];\n return await this.list({\n where: { parentId: parent.id, context: parent.context },\n });\n }\n\n /**\n * Get tag hierarchy (all ancestors and descendants)\n *\n * @param slug - The tag slug\n * @param context - Optional context for the slug lookup. When omitted,\n * the slug must be unambiguous across contexts.\n * @returns Object with ancestors, current tag, and descendants\n */\n async getHierarchy(slug: string, context?: string): Promise<TagHierarchy> {\n const tag = await this.resolveBySlug(slug, context);\n if (!tag) throw new Error(`Tag '${slug}' not found`);\n\n const [ancestors, descendants] = await Promise.all([\n tag.getAncestors() as Promise<Tag[]>,\n tag.getDescendants() as Promise<Tag[]>,\n ]);\n\n return { ancestors, current: tag, descendants };\n }\n\n /**\n * Move a tag to a new parent. Slug-based API; UUIDs resolved internally.\n *\n * Cycle detection is inlined here (mirroring `SmrtHierarchical.moveTo`'s\n * self-loop + descendant checks) so that both `parentId` and the\n * denormalised `level` field can be persisted in a single `save()`.\n * Delegating to `moveTo` would write `parentId` first and `level` in a\n * second save — if the second save failed, the tag would be left with\n * the new parent but a stale level, breaking the depth cache.\n *\n * After the moved tag persists, descendant levels are recalculated\n * recursively via `updateDescendantLevels`.\n *\n * @param slug - The tag to move\n * @param newParentSlug - The new parent slug (null for root)\n * @param context - Optional context. When provided, both source and new\n * parent are resolved within it. When omitted, both slugs must be\n * unambiguous across contexts; the resolver throws otherwise.\n * @throws Error if either slug fails to resolve, if either slug is\n * ambiguous across contexts (no context provided), if source and new\n * parent live in different contexts, or if the move would create a\n * cycle.\n */\n async moveTag(\n slug: string,\n newParentSlug: string | null,\n context?: string,\n ): Promise<void> {\n const tag = await this.resolveBySlug(slug, context);\n if (!tag) throw new Error(`Tag '${slug}' not found`);\n\n let newParent: Tag | null = null;\n if (newParentSlug) {\n // Resolve the new parent in the SAME context as the tag we're\n // moving. If the caller passed a context, use it; otherwise pin\n // to the source tag's context so we don't drift across contexts\n // on the second lookup.\n newParent = await this.resolveBySlug(\n newParentSlug,\n context ?? tag.context,\n );\n if (!newParent) {\n throw new Error(`Tag '${newParentSlug}' not found`);\n }\n if (newParent.context !== tag.context) {\n throw new Error(\n `Cannot move Tag '${slug}' (context '${tag.context}') under '${newParentSlug}' (context '${newParent.context}') — contexts must match.`,\n );\n }\n }\n const newParentId = newParent?.id ?? null;\n\n if (newParentId !== null && newParentId === tag.id) {\n throw new Error(`Cannot move Tag ${tag.id} to itself.`);\n }\n if (newParentId !== null) {\n const descendants = (await tag.getDescendants()) as Tag[];\n if (descendants.some((d) => d.id === newParentId)) {\n throw new Error(\n `Cannot move Tag ${tag.id} under one of its own descendants (${newParentId}) — would create a cycle.`,\n );\n }\n }\n\n tag.parentId = newParentId;\n tag.level = newParent ? newParent.level + 1 : 0;\n await tag.save();\n await this.updateDescendantLevels(tag);\n }\n\n /**\n * Merge one tag into another (updates all references)\n *\n * Reparents `fromTag`'s direct children onto `toTag` and recalculates\n * their `level` field plus the level of every descendant — without\n * this, children moved from a different depth would carry stale\n * levels relative to their new parent. `TagAlias.tagSlug` references\n * are also rewritten, then `fromTag` is deleted.\n *\n * Note: Consuming packages are responsible for updating their own\n * join tables (e.g. `asset_tags`).\n *\n * @param fromSlug - The tag to merge from\n * @param toSlug - The tag to merge into\n * @param context - Optional context. When provided, both tags are\n * resolved within it. When omitted, both slugs must be unambiguous\n * across contexts.\n * @throws Error if either slug fails to resolve, if either slug is\n * ambiguous, or if the two tags live in different contexts.\n */\n async mergeTag(\n fromSlug: string,\n toSlug: string,\n context?: string,\n ): Promise<void> {\n const fromTag = await this.resolveBySlug(fromSlug, context);\n const toTag = await this.resolveBySlug(toSlug, context ?? fromTag?.context);\n\n if (!fromTag) throw new Error(`Source tag '${fromSlug}' not found`);\n if (!toTag) throw new Error(`Target tag '${toSlug}' not found`);\n if (!fromTag.id) throw new Error(`Source tag '${fromSlug}' has no id`);\n if (!toTag.id) throw new Error(`Target tag '${toSlug}' has no id`);\n if (fromTag.context !== toTag.context) {\n throw new Error(\n `Cannot merge '${fromSlug}' (context '${fromTag.context}') into '${toSlug}' (context '${toTag.context}') — contexts must match.`,\n );\n }\n if (fromTag.id === toTag.id) {\n throw new Error(`Cannot merge tag '${fromSlug}' into itself.`);\n }\n\n // Codex round-4 finding: refuse to merge into a descendant. Without\n // this guard the children-reparenting loop below could attach\n // `toTag` (or its ancestor) under itself, producing a cycle; the\n // recursive `updateDescendantLevels` walk has no visited set and\n // would stack-overflow before the corruption is observable.\n const fromDescendants = (await fromTag.getDescendants()) as Tag[];\n if (fromDescendants.some((d) => d.id === toTag.id)) {\n throw new Error(\n `Cannot merge '${fromSlug}' into '${toSlug}' — target is a descendant of source (would create a cycle).`,\n );\n }\n\n // Move all direct children of fromTag to toTag. Each child needs its\n // own `level` recomputed because toTag's depth may differ from\n // fromTag's depth (the children were positioned relative to fromTag's\n // level; after reparenting they hang off toTag instead). Then walk\n // each child's own subtree to propagate the level shift down.\n const children = await this.list({\n where: { parentId: fromTag.id },\n });\n const newChildLevel = toTag.level + 1;\n for (const child of children) {\n child.parentId = toTag.id;\n child.level = newChildLevel;\n await child.save();\n await this.updateDescendantLevels(child);\n }\n\n // Copy aliases from fromTag to toTag.\n // R3-B follow-up (codex caught this across multiple rounds): scope\n // the alias rewrite to the merge's resolved context, not the bare\n // slug. Otherwise `mergeTag('foo', 'bar', 'blog')` rewrites\n // `foo`'s aliases in EVERY context (forum, etc.), corrupting tag\n // data outside the requested merge scope.\n //\n // Round-8 codex follow-up: `Tag._context` defaults to `'global'`\n // but `TagAlias._context` defaults to `''` (and `addAlias()`\n // doesn't backfill the default), so a strict equality filter\n // would orphan aliases that were created without an explicit\n // context whenever the merge happens at the default 'global'\n // scope.\n //\n // Round-8 fix (initial): widen to `[fromTag.context, '']` —\n // BUT codex + copilot caught that this overcorrected. For a\n // non-default merge like `mergeTag('foo','bar','blog')`, the\n // unscoped (`''`) aliases for slug `foo` belong to a DIFFERENT\n // tag entirely (the `global/foo` row that the blog merge isn't\n // touching). Widening unconditionally re-introduced the\n // cross-context corruption round-7 was guarding against.\n //\n // Round-8 fix (final): only include `''` when the merge is at\n // the default context — that's the case where the\n // addAlias/Tag default-mismatch would orphan rows. For any\n // other context, the strict equality from round-7 is correct.\n const { TagAliasCollection } = await import('./tag-aliases');\n const aliasCollection = await (TagAliasCollection as any).create(\n this.options,\n );\n\n const aliasContexts: string[] = [fromTag.context];\n if (fromTag.context === 'global') {\n aliasContexts.push('');\n }\n const aliases = await aliasCollection.list({\n where: { tagSlug: fromSlug, context: aliasContexts },\n });\n for (const alias of aliases) {\n alias.tagSlug = toSlug;\n await alias.save();\n }\n\n // Delete the fromTag\n await fromTag.delete();\n }\n\n /**\n * Remove tags with no references (cleanup unused tags)\n *\n * Note: This requires consuming packages to provide usage information.\n * By default, only removes tags with no children and no aliases.\n *\n * @param context - Optional context to filter cleanup\n */\n async cleanupUnused(context?: string): Promise<number> {\n const where: any = {};\n if (context) where.context = context;\n\n const tags = await this.list({ where });\n const { TagAliasCollection } = await import('./tag-aliases');\n const aliasCollection = await (TagAliasCollection as any).create(\n this.options,\n );\n\n let deletedCount = 0;\n\n for (const tag of tags) {\n if (!tag.id) continue;\n\n // Check if tag has children (by parentId — UUID).\n const children = await this.list({\n where: { parentId: tag.id },\n limit: 1,\n });\n if (children.length > 0) continue;\n\n // Check if tag has aliases (still slug-keyed on TagAlias.tagSlug).\n const aliases = await aliasCollection.list({\n where: { tagSlug: tag.slug },\n limit: 1,\n });\n if (aliases.length > 0) continue;\n\n // No children, no aliases - safe to delete\n await tag.delete();\n deletedCount++;\n }\n\n return deletedCount;\n }\n\n /**\n * Calculate hierarchy level for a tag, looking the parent up by slug.\n *\n * @param parentSlug - The parent tag slug (null/empty for root)\n * @param context - Optional context for the parent lookup. When\n * omitted, the parent slug must be unambiguous across contexts.\n * @returns The calculated level (root parent → 1, missing parent → 0)\n */\n async calculateLevel(\n parentSlug: string | null,\n context?: string,\n ): Promise<number> {\n if (!parentSlug) return 0;\n\n const parent = await this.resolveBySlug(parentSlug, context);\n if (!parent) return 0;\n\n return parent.level + 1;\n }\n\n /**\n * Update levels for all descendants after moving a tag\n *\n * @param tag - The tag that was moved\n */\n private async updateDescendantLevels(tag: Tag): Promise<void> {\n if (!tag.id) return;\n const children = await this.list({\n where: { parentId: tag.id },\n });\n\n for (const child of children) {\n child.level = tag.level + 1;\n await child.save();\n await this.updateDescendantLevels(child); // Recursive\n }\n }\n\n // =========================================================================\n // Tenant Helper Methods\n // =========================================================================\n\n /**\n * Find all tags belonging to a specific tenant\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tags for the specified tenant\n */\n async findByTenant(tenantId: string): Promise<Tag[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global (tenant-less) tags.\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Array of global tags with null tenantId\n */\n async findGlobal(): Promise<Tag[]> {\n return queryGlobal<Tag>(this);\n }\n\n /**\n * Find tags for a tenant including global tags.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Array of tags for the tenant plus all global tags\n */\n async findWithGlobals(tenantId: string): Promise<Tag[]> {\n return queryWithGlobals<Tag>(this, tenantId, 'Tag.findWithGlobals');\n }\n}\n"],"names":["__decorateClass","TagCollection","tags","tenantId","TagAliasCollection"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1782238474639,
3
+ "timestamp": 1782248720156,
4
4
  "packageName": "@happyvertical/smrt-tags",
5
- "packageVersion": "0.33.1",
5
+ "packageVersion": "0.34.1",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-tags:TagAlias": {
8
8
  "name": "tagalias",
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-06-23T18:14:38.417Z",
3
+ "generatedAt": "2026-06-23T21:05:23.730Z",
4
4
  "packageName": "@happyvertical/smrt-tags",
5
- "packageVersion": "0.33.1",
5
+ "packageVersion": "0.34.1",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "9738b93d9468213c33d0d871b37915acb11596c467684e3ac6b40cd81506610c",
10
- "packageJson": "f7f2f805312c05ba52af89421fdae7ad1bf6a86722c9c8327b1710300fc2bf36",
9
+ "manifest": "758ea10163fd5ec8c6c79cd4337e7193dc25765ba677e8cb41bfbfcdc1a78e56",
10
+ "packageJson": "f38e2d42290700d978aa12b01b81eb88f9a414f764b90388f5e023f560cb036b",
11
11
  "agents": "900516b49407d5a521c2f0f2bdb2d4ab0c97e620808115718779f7ecf3ace222"
12
12
  },
13
13
  "exports": [
package/dist/utils.d.ts CHANGED
@@ -249,13 +249,20 @@ declare class TagCollection extends SmrtCollection<Tag> {
249
249
  */
250
250
  findByTenant(tenantId: string): Promise<Tag[]>;
251
251
  /**
252
- * Find all global (tenant-less) tags
252
+ * Find all global (tenant-less) tags.
253
+ *
254
+ * Routes through the shared tenant-global helper so it does not throw under
255
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
256
+ * flagged as an isolation violation). (#1600)
253
257
  *
254
258
  * @returns Array of global tags with null tenantId
255
259
  */
256
260
  findGlobal(): Promise<Tag[]>;
257
261
  /**
258
- * Find tags for a tenant including global tags
262
+ * Find tags for a tenant including global tags.
263
+ *
264
+ * Fails closed if an active tenant context requests a different tenant's
265
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
259
266
  *
260
267
  * @param tenantId - The tenant ID to filter by
261
268
  * @returns Array of tags for the tenant plus all global tags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-tags",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "description": "Reusable tagging system with hierarchies, contexts, and multi-language support for SMRT framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,8 +28,8 @@
28
28
  "@happyvertical/logger": "^0.74.7",
29
29
  "@happyvertical/sql": "^0.74.7",
30
30
  "@happyvertical/utils": "^0.74.7",
31
- "@happyvertical/smrt-tenancy": "0.33.1",
32
- "@happyvertical/smrt-core": "0.33.1"
31
+ "@happyvertical/smrt-core": "0.34.1",
32
+ "@happyvertical/smrt-tenancy": "0.34.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "25.0.9",