UNPKG

@debito/hippo-lib

Version:

Double-entry accounting library for CouchDB

169 lines (147 loc) 5.34 kB
import { getMetaDB } from '../config.js'; import { v4 as uuidv4 } from 'uuid'; import Book from './Book.js'; /** * User class for managing users and their books */ class User { userId; /** * Creates a User instance * @param {string} userId - User identifier */ constructor(userId) { if (!userId) { throw new Error('userId is required'); } this.userId = userId; } /** * Create a new book for this user (with ownership) * @param {string} name - Book display name * @param {Object} options - Book creation options * @param {string} [options.template='base'] - COA template to use * @param {string} [options.description=''] - Book description * @param {string} [options.currency='USD'] - Default currency * @param {string} [options.fiscalYearStart='01-01'] - Fiscal year start (MM-DD) * @param {string} [options.organizationId=null] - Organization identifier * @returns {Promise<Book>} Created book instance */ async createBook(name, options = {}) { // Create the book (ownerless) const book = await Book.create(name, options); // Set this user as owner await book.update({ owner: this.userId }); // Create user-book relationship for the creator const metaDB = getMetaDB(); const userBookUuid = uuidv4(); const userBookId = `userbook-${userBookUuid}`; const userBookDoc = { _id: userBookId, type: 'user_book', userId: this.userId, bookId: book._id, role: 'owner', permissions: ['read', 'write', 'admin'], joinedAt: new Date().toISOString() }; await metaDB.insert(userBookDoc, userBookId); return book; } /** * List all books for this user * @returns {Promise<Book[]>} Array of book instances */ async listBooks() { const metaDB = getMetaDB(); // Query user-book relationships const result = await metaDB.find({ selector: { type: 'user_book', userId: this.userId }, sort: [{ 'joinedAt': 'desc' }], limit: 999999 }); const books = []; for (const userBook of result.docs) { try { const bookDoc = await metaDB.get(userBook.bookId); const book = new Book(bookDoc); // Add role info from user-book relationship book.role = userBook.role; book.permissions = userBook.permissions; books.push(book); } catch (error) { console.warn(`Warning: Could not load book ${userBook.bookId}:`, error.message); } } return books; } /** * List books for this user scoped to an organization * Fetches org books and user_book docs separately, then cross-references * @param {string} organizationId - Organization identifier * @returns {Promise<Book[]>} Array of book instances */ async listOrgBooks(organizationId) { const metaDB = getMetaDB(); // Fetch all books belonging to the org const orgBooks = await Book.listOrgBooks(organizationId); // Fetch all user_book docs for this user const userBooksResult = await metaDB.find({ selector: { type: 'user_book', userId: this.userId }, limit: 999999 }); // Build a map of bookId → user_book for quick lookup const userBookMap = {}; for (const ub of userBooksResult.docs) { userBookMap[ub.bookId] = ub; } // Return only org books where the user has a user_book relationship return orgBooks .filter(book => userBookMap[book._id]) .map(book => { book.role = userBookMap[book._id].role; book.permissions = userBookMap[book._id].permissions; return book; }); } /** * Get user's role in a specific book * @param {string} bookId - Book identifier * @returns {Promise<Object>} User-book relationship */ async getBookRole(bookId) { const metaDB = getMetaDB(); try { // Query for user-book relationship by userId and bookId const result = await metaDB.find({ selector: { type: 'user_book', userId: this.userId, bookId: bookId }, limit: 1 }); if (result.docs.length === 0) { throw new Error(`User ${this.userId} does not have access to book ${bookId}`); } const userBook = result.docs[0]; return { role: userBook.role, permissions: userBook.permissions, joinedAt: userBook.joinedAt }; } catch (error) { if (error.message.includes('does not have access')) { throw error; } throw new Error(`Failed to get book role: ${error.message}`); } } } export default User;