import { SmrtCollection } from '@happyvertical/smrt-core'; import { SmrtHierarchical } from '@happyvertical/smrt-core'; import { SmrtObject } from '@happyvertical/smrt-core'; import { SmrtObjectOptions } from '@happyvertical/smrt-core'; /** * Calculate hierarchy level * * Determines the level (depth) of a tag based on its parent. * Root tags have level 0, their children have level 1, etc. * * @param parentSlug - The parent tag slug (null for root) * @param tagCollection - TagCollection instance for queries * @returns The calculated level */ export declare function calculateLevel(parentSlug: string | null, tagCollection: TagCollection): Promise; /** * Generate a unique slug from a name * * Creates a slug and ensures uniqueness by appending a number if needed. * * @param name - The name to convert to slug * @param context - The context for uniqueness checking * @param tagCollection - TagCollection instance for queries * @returns Unique slug */ export declare function generateUniqueSlug(name: string, context: string, tagCollection: TagCollection): Promise; /** * Validate hierarchy for circular references * * Checks if setting a parent would create a circular reference * (e.g., making a tag its own ancestor). The actual move call in * `TagCollection.moveTag` also runs `SmrtHierarchical.moveTo`'s * descendant-cycle check; this helper remains exported for callers that * want to pre-validate a candidate parent without attempting the move. * * Tags are identified by `(slug, context)`. If `context` is omitted, * slug-only lookups are used — fine when slugs are unique across all * contexts, but the walk can traverse the wrong chain when the same * slug exists in multiple contexts. Pass the candidate parent's * `context` for accurate cross-context-safe validation. * * @param slug - The tag being moved * @param parentSlug - The proposed new parent * @param tagCollection - TagCollection instance for queries * @param context - Optional context to scope every slug lookup to * @returns True if circular reference detected */ export declare function hasCircularReference(slug: string, parentSlug: string, tagCollection: TagCollection, context?: string): Promise; /** * Sanitize slug input * * Converts to lowercase, replaces spaces with hyphens, * removes invalid characters, and ensures proper format. * * @param input - The input string to sanitize * @returns Sanitized slug */ export declare function sanitizeSlug(input: string): string; export declare class Tag extends SmrtHierarchical { protected _slug: string; protected _context: string; get slug(): string; set slug(value: string); get context(): string; set context(value: string); name: string; level: number; description: string; metadata: string; tenantId: string | null; createdAt: Date; updatedAt: Date; constructor(options?: TagOptions); /** * Get metadata as parsed object * * @returns Parsed metadata object or empty object if no metadata */ getMetadata(): TagMetadata; /** * Set metadata from object * * @param data - Metadata object to store */ setMetadata(data: TagMetadata): void; /** * Update metadata by merging with existing values * * @param updates - Partial metadata to merge */ updateMetadata(updates: Partial): void; /** * Convenience method for slug-based lookup * * @param slug - The slug to search for * @param context - Optional context filter * @returns Tag instance or null if not found */ static getBySlug(_slug: string, _context?: string): Promise; /** * Get root tags (no parent) for a context * * @param context - The context to filter by * @returns Array of root tags */ static getRootTags(_context?: string): Promise; } export declare class TagAlias extends SmrtObject { tagSlug: string; alias: string; language: string; protected _context: string; get context(): string; set context(value: string); tenantId: string | null; createdAt: Date; constructor(options?: TagAliasOptions); /** * Get the tag this alias belongs to * * @returns Tag instance or null if not found */ getTag(): Promise; /** * Search tags by alias * * @param alias - The alias to search for * @param language - Optional language filter * @returns Array of matching tags */ static searchByAlias(_alias: string, _language?: string): Promise; /** * Get all aliases for a tag * * @param tagSlug - The tag slug to get aliases for * @returns Array of TagAlias instances */ static getAliasesForTag(_tagSlug: string): Promise; } export declare class TagAliasCollection extends SmrtCollection { static readonly _itemClass: typeof TagAlias; /** * Add an alias to a tag (get or create) * * @param tagSlug - The tag slug * @param alias - The alias text * @param language - Optional language code * @param context - Optional context * @returns TagAlias instance */ addAlias(tagSlug: string, alias: string, language?: string, context?: string): Promise; /** * Search tags by alias * * @param alias - The alias to search for * @param language - Optional language filter * @returns Array of matching tags */ searchByAlias(alias: string, language?: string): Promise; /** * Get all aliases for a tag * * @param tagSlug - The tag slug * @param language - Optional language filter * @returns Array of TagAlias instances */ getAliasesForTag(tagSlug: string, language?: string): Promise; /** * Remove an alias by ID * * @param aliasId - The alias UUID */ removeAlias(aliasId: string): Promise; /** * Bulk add aliases to a tag * * @param tagSlug - The tag slug * @param aliases - Array of alias configurations * @returns Array of created TagAlias instances */ bulkAddAliases(tagSlug: string, aliases: Array<{ alias: string; language?: string; context?: string; }>): Promise; /** * Get aliases grouped by language * * @param tagSlug - The tag slug * @returns Map of language code to array of aliases */ getAliasesByLanguage(tagSlug: string): Promise>; /** * Find matching aliases (case-insensitive partial match) * * Note: This is a simple implementation. For production use, * consider using full-text search or fuzzy matching. * * @param query - The search query * @param language - Optional language filter * @returns Array of matching TagAlias instances */ findMatchingAliases(query: string, language?: string): Promise; /** * Find all tag aliases belonging to a specific tenant * * @param tenantId - The tenant ID to filter by * @returns Array of tag aliases for the specified tenant */ findByTenant(tenantId: string): Promise; /** * Find all global (tenant-less) tag aliases. * * Routes through the shared tenant-global helper so it does not throw under * an active tenant context (an explicit `tenant_id IS NULL` filter would be * flagged as an isolation violation). (#1600) * * @returns Array of global tag aliases with null tenantId */ findGlobal(): Promise; /** * Find tag aliases for a tenant including global aliases. * * Fails closed if an active tenant context requests a different tenant's * rows; the admin/system path keeps the cross-tenant capability. (#1600) * * @param tenantId - The tenant ID to filter by * @returns Array of tag aliases for the tenant plus all global aliases */ findWithGlobals(tenantId: string): Promise; } /** * Options for creating a TagAlias instance */ export declare interface TagAliasOptions extends SmrtObjectOptions { tagSlug?: string; alias?: string; language?: string; context?: string; tenantId?: string | null; } export declare class TagCollection extends SmrtCollection { static readonly _itemClass: typeof Tag; /** * Get or create a tag with context * * @param slug - Tag slug * @param context - Tag context (default: 'global') * @returns Tag instance */ getOrCreate(slug: string, context?: string): Promise; /** * Resolve a tag by slug, optionally scoped to a context. * * Tags are identified by `(slug, context)`. When `context` is omitted * and the slug exists in more than one context, this throws a clear * ambiguity error rather than silently picking the first matching row. * Callers that know their context should pass it; callers that work in * a single-context world can leave it off. * * @returns The matching Tag, or `null` if nothing matches. * @throws Error if `context` is omitted and the slug is ambiguous. */ private resolveBySlug; /** * List tags by context with optional parent filtering by slug. * * @param context - The context to filter by * @param parentSlug - Optional parent slug to filter children. Pass an * empty string or `null` to find root tags; pass a slug to find that * tag's immediate children. Typed as `string | null` so TypeScript * callers can pass `null` without a cast — the `null` and `''` paths * are both treated as "roots only". * @returns Array of matching tags */ listByContext(context: string, parentSlug?: string | null): Promise; /** * Get root tags (no parent) for a context * * @param context - The context to filter by (default: 'global') * @returns Array of root tags */ getRootTags(context?: string): Promise; /** * Get immediate children of a parent tag, looked up by slug. * * @param parentSlug - The parent tag slug * @param context - Optional context for the parent lookup. When omitted, * the parent slug must be unambiguous across contexts (throws if not). * @returns Array of child tags, or `[]` if the parent slug doesn't * resolve. Children are filtered to the resolved parent's context so * cross-context children don't leak in. */ getChildren(parentSlug: string, context?: string): Promise; /** * Get tag hierarchy (all ancestors and descendants) * * @param slug - The tag slug * @param context - Optional context for the slug lookup. When omitted, * the slug must be unambiguous across contexts. * @returns Object with ancestors, current tag, and descendants */ getHierarchy(slug: string, context?: string): Promise; /** * Move a tag to a new parent. Slug-based API; UUIDs resolved internally. * * Cycle detection is inlined here (mirroring `SmrtHierarchical.moveTo`'s * self-loop + descendant checks) so that both `parentId` and the * denormalised `level` field can be persisted in a single `save()`. * Delegating to `moveTo` would write `parentId` first and `level` in a * second save — if the second save failed, the tag would be left with * the new parent but a stale level, breaking the depth cache. * * After the moved tag persists, descendant levels are recalculated * recursively via `updateDescendantLevels`. * * @param slug - The tag to move * @param newParentSlug - The new parent slug (null for root) * @param context - Optional context. When provided, both source and new * parent are resolved within it. When omitted, both slugs must be * unambiguous across contexts; the resolver throws otherwise. * @throws Error if either slug fails to resolve, if either slug is * ambiguous across contexts (no context provided), if source and new * parent live in different contexts, or if the move would create a * cycle. */ moveTag(slug: string, newParentSlug: string | null, context?: string): Promise; /** * Merge one tag into another (updates all references) * * Reparents `fromTag`'s direct children onto `toTag` and recalculates * their `level` field plus the level of every descendant — without * this, children moved from a different depth would carry stale * levels relative to their new parent. `TagAlias.tagSlug` references * are also rewritten, then `fromTag` is deleted. * * Note: Consuming packages are responsible for updating their own * join tables (e.g. `asset_tags`). * * @param fromSlug - The tag to merge from * @param toSlug - The tag to merge into * @param context - Optional context. When provided, both tags are * resolved within it. When omitted, both slugs must be unambiguous * across contexts. * @throws Error if either slug fails to resolve, if either slug is * ambiguous, or if the two tags live in different contexts. */ mergeTag(fromSlug: string, toSlug: string, context?: string): Promise; /** * Remove tags with no references (cleanup unused tags) * * Note: This requires consuming packages to provide usage information. * By default, only removes tags with no children and no aliases. * * @param context - Optional context to filter cleanup */ cleanupUnused(context?: string): Promise; /** * Calculate hierarchy level for a tag, looking the parent up by slug. * * @param parentSlug - The parent tag slug (null/empty for root) * @param context - Optional context for the parent lookup. When * omitted, the parent slug must be unambiguous across contexts. * @returns The calculated level (root parent → 1, missing parent → 0) */ calculateLevel(parentSlug: string | null, context?: string): Promise; /** * Update levels for all descendants after moving a tag * * @param tag - The tag that was moved */ private updateDescendantLevels; /** * Find all tags belonging to a specific tenant * * @param tenantId - The tenant ID to filter by * @returns Array of tags for the specified tenant */ findByTenant(tenantId: string): Promise; /** * Find all global (tenant-less) tags. * * Routes through the shared tenant-global helper so it does not throw under * an active tenant context (an explicit `tenant_id IS NULL` filter would be * flagged as an isolation violation). (#1600) * * @returns Array of global tags with null tenantId */ findGlobal(): Promise; /** * Find tags for a tenant including global tags. * * Fails closed if an active tenant context requests a different tenant's * rows; the admin/system path keeps the cross-tenant capability. (#1600) * * @param tenantId - The tenant ID to filter by * @returns Array of tags for the tenant plus all global tags */ findWithGlobals(tenantId: string): Promise; } /** * Tag hierarchy result structure */ export declare interface TagHierarchy { ancestors: Tag[]; current: Tag; descendants: Tag[]; } /** * Tag metadata structure (flexible, application-specific) */ export declare interface TagMetadata { color?: string; backgroundColor?: string; icon?: string; emoji?: string; usageCount?: number; lastUsed?: string; trending?: boolean; featured?: boolean; sortOrder?: number; showInNav?: boolean; displayFormat?: string; aiGenerated?: boolean; confidence?: number; source?: string; reviewStatus?: string; [key: string]: unknown; } /** * Options for creating a Tag instance */ export declare interface TagOptions extends SmrtObjectOptions { slug?: string; name?: string; context?: string; parentId?: string | null; level?: number; description?: string; metadata?: string | Record; tenantId?: string | null; } /** * Validate slug format (lowercase, alphanumeric + hyphens) * * @param slug - The slug to validate * @returns True if slug is valid */ export declare function validateSlug(slug: string): boolean; export { }