import LocationRepo, { LocationByTagResult, LocationElementQueryResult, LocationQueryResult } from "../repositories/location.repository.js"; import System from "../System.js"; import {getUserEncryptionKey} from "../keyManager.js"; export interface SubElement { id: string; name: string; description: string; } export interface Element { id: string; name: string; description: string; subElements: SubElement[]; } export interface LocationProps { id: string; name: string; elements: Element[]; } export interface SyncedLocation { id: string; name: string; lastUpdate: number; elements: SyncedLocationElement[]; } export interface SyncedLocationElement { id: string; name: string; lastUpdate: number; subElements: SyncedLocationSubElement[]; } export interface SyncedLocationSubElement { id: string; name: string; lastUpdate: number; } export default class Location { /** * Retrieves all locations for a given user and book. * @param userId - The user's unique identifier. * @param bookId - The book's unique identifier. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @returns An array of location properties with their elements and sub-elements. */ static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationProps[] { const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang); if (!locationRecords || locationRecords.length === 0) return []; const userKey: string = getUserEncryptionKey(userId); const locationArray: LocationProps[] = []; for (const record of locationRecords) { let location = locationArray.find(loc => loc.id === record.loc_id); if (!location) { const decryptedName: string = System.decryptDataWithUserKey(record.loc_name, userKey); location = { id: record.loc_id, name: decryptedName, elements: [] }; locationArray.push(location); } if (record.element_id) { let element = location.elements.find(elem => elem.id === record.element_id); if (!element) { const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey); const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : ''; element = { id: record.element_id, name: decryptedName, description: decryptedDescription, subElements: [] }; location.elements.push(element); } if (record.sub_element_id) { const subElementExists = element.subElements.some(sub => sub.id === record.sub_element_id); if (!subElementExists) { const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey); const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : ''; element.subElements.push({ id: record.sub_element_id, name: decryptedName, description: decryptedDescription }); } } } } return locationArray; } /** * Adds a new location section for a book. * @param userId - The user's unique identifier. * @param locationName - The name of the location to create. * @param bookId - The book's unique identifier. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @param existingLocationId - Optional existing location ID to use instead of generating a new one. * @returns The ID of the created location. */ static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string): string { const userKey: string = getUserEncryptionKey(userId); const hashedName: string = System.hashElement(locationName); const encryptedName: string = System.encryptDataWithUserKey(locationName, userKey); const locationId: string = existingLocationId || System.createUniqueId(); return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang); } /** * Adds a new element to a location. * @param userId - The user's unique identifier. * @param locationId - The parent location's unique identifier. * @param elementName - The name of the element to create. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @param existingElementId - Optional existing element ID to use instead of generating a new one. * @returns The result of the insert operation. */ static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string { const userKey: string = getUserEncryptionKey(userId); const hashedName: string = System.hashElement(elementName); const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey); const elementId: string = existingElementId || System.createUniqueId(); return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, hashedName, lang) } /** * Adds a new sub-element to a location element. * @param userId - The user's unique identifier. * @param elementId - The parent element's unique identifier. * @param subElementName - The name of the sub-element to create. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @param existingSubElementId - Optional existing sub-element ID to use instead of generating a new one. * @returns The result of the insert operation. */ static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string): string { const userKey: string = getUserEncryptionKey(userId); const hashedName: string = System.hashElement(subElementName); const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey); const subElementId: string = existingSubElementId || System.createUniqueId(); return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, hashedName, lang) } /** * Updates multiple location sections along with their elements and sub-elements. * @param userId - The user's unique identifier. * @param locations - Array of location properties to update. * @param lang - The language for response messages ('fr' or 'en'). Defaults to 'fr'. * @returns An object indicating success and a localized message. */ static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } { const userKey: string = getUserEncryptionKey(userId); for (const location of locations) { const hashedLocationName: string = System.hashElement(location.name); const encryptedLocationName: string = System.encryptDataWithUserKey(location.name, userKey); LocationRepo.updateLocationSection(userId, location.id, encryptedLocationName, hashedLocationName, System.timeStampInSeconds(), lang) for (const element of location.elements) { const hashedElementName: string = System.hashElement(element.name); const encryptedElementName: string = System.encryptDataWithUserKey(element.name, userKey); const encryptedElementDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : ''; LocationRepo.updateLocationElement(userId, element.id, encryptedElementName, hashedElementName, encryptedElementDescription, System.timeStampInSeconds(), lang) for (const subElement of element.subElements) { const hashedSubElementName: string = System.hashElement(subElement.name); const encryptedSubElementName: string = System.encryptDataWithUserKey(subElement.name, userKey); const encryptedSubElementDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : ''; LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedSubElementName, hashedSubElementName, encryptedSubElementDescription, System.timeStampInSeconds(), lang) } } } return { valid: true, message: lang === 'fr' ? 'Les sections ont été mis à jour.' : 'Sections have been updated.' } } /** * Deletes a location section and all its associated elements and sub-elements. * @param userId - The user's unique identifier. * @param locationId - The location's unique identifier to delete. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @returns The result of the delete operation. */ static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } { return LocationRepo.deleteLocationSection(userId, locationId, lang); } /** * Deletes a location element and all its associated sub-elements. * @param userId - The user's unique identifier. * @param elementId - The element's unique identifier to delete. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @returns The result of the delete operation. */ static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } { return LocationRepo.deleteLocationElement(userId, elementId, lang); } /** * Deletes a location sub-element. * @param userId - The user's unique identifier. * @param subElementId - The sub-element's unique identifier to delete. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @returns The result of the delete operation. */ static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } { return LocationRepo.deleteLocationSubElement(userId, subElementId, lang); } /** * Retrieves location tags (elements or sub-elements) for tagging purposes. * Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself. * @param userId - The user's unique identifier. * @param bookId - The book's unique identifier. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @returns An array of sub-elements suitable for tagging. */ static getLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SubElement[] { const tagRecords: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang); if (!tagRecords || tagRecords.length === 0) return []; const userKey: string = getUserEncryptionKey(userId); const elementCounts = new Map(); tagRecords.forEach((record: LocationElementQueryResult): void => { elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1); }); const subElements: SubElement[] = []; const processedIds = new Set(); for (const record of tagRecords) { const elementCount: number = elementCounts.get(record.element_id) || 0; if (elementCount > 1 && record.sub_element_id) { if (processedIds.has(record.sub_element_id)) continue; subElements.push({ id: record.sub_element_id, name: System.decryptDataWithUserKey(record.sub_elem_name, userKey), description: record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '' }); processedIds.add(record.sub_element_id); } else if (elementCount === 1 && !record.sub_element_id) { if (processedIds.has(record.element_id)) continue; subElements.push({ id: record.element_id, name: System.decryptDataWithUserKey(record.element_name, userKey), description: record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '' }); processedIds.add(record.element_id); } } return subElements; } /** * Retrieves location elements filtered by specific tag IDs. * @param userId - The user's unique identifier. * @param locations - Array of location tag IDs to filter by. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. * @returns An array of elements with their associated sub-elements. */ static getLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): Element[] { const locationTagRecords: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang); if (!locationTagRecords || locationTagRecords.length === 0) return []; const userKey: string = getUserEncryptionKey(userId); const locationElements: Element[] = []; for (const record of locationTagRecords) { let element: Element | undefined = locationElements.find((elem: Element): boolean => elem.name === record.element_name); if (!element) { const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey); const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : ''; element = { id: '', name: decryptedName, description: decryptedDescription, subElements: [] }; locationElements.push(element); } if (record.sub_elem_name) { const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name); if (!subElementExists) { const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey); const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : ''; element.subElements.push({ id: '', name: decryptedName, description: decryptedDescription }); } } } return locationElements; } /** * Generates a formatted description string from an array of location elements. * @param locations - Array of location elements to describe. * @returns A formatted string with location names and descriptions. */ static locationsDescription(locations: Element[]): string { return locations.map((location: Element): string => { const descriptionFields: string[] = []; if (location.name) descriptionFields.push(`Nom : ${location.name}`); if (location.description) descriptionFields.push(`Description : ${location.description}`); return descriptionFields.join('\n'); }).join('\n\n'); } }