@debito/hippo-lib
Version:
Double-entry accounting library for CouchDB
169 lines (147 loc) • 5.34 kB
JavaScript
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;