OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / lib / services / notebook-service.js

127 lines · javascript ref:
/**
 * NotebookService — Business logic for notebook operations.
 *
 * Extracts filtering, visibility, and side-effect logic from route handlers.
 * Routes call this service; this service calls NoteManager.
 */

export class NotebookService {
  constructor ({ noteManager, notebookCrypto, authManager, sitePublisher }) {
    this.noteManager = noteManager
    this.notebookCrypto = notebookCrypto
    this.authManager = authManager
    this.sitePublisher = sitePublisher
  }

  /**
   * List notebooks with visibility/ownership filtering.
   *
   * @param {Object|null} auth - Auth result from authenticate() (null = anonymous)
   * @param {Object} filters
   * @param {boolean} [filters.mine] - Only current user's notebooks
   * @param {string} [filters.owner] - Filter by owner username
   * @returns {Promise<Array>} Filtered notebook list
   */
  async listFiltered (auth, filters = {}) {
    let notebooks = await this.noteManager.listNotebooks()

    const isAdmin = auth && auth.permissions && auth.permissions.admin

    // Non-admins cannot see private or encrypted notebooks
    if (!isAdmin) {
      notebooks = notebooks.filter(nb => nb.visibility !== 'private' && !nb.encrypted)
    }

    // Filter to current user's notebooks
    if (filters.mine) {
      if (auth && auth.userId) {
        notebooks = notebooks.filter(nb => {
          return nb.ownerId === auth.userId || (!nb.ownerId && isAdmin)
        })
      } else {
        return []
      }
    }

    // Filter by owner username
    if (filters.owner) {
      const ownerUser = await this.authManager.identityRegistry.getUserByUsername(filters.owner)
      if (!ownerUser) return []

      const userPerms = ownerUser.globalPermissions || []
      const ownerIsAdmin = userPerms.includes('admin')
      notebooks = notebooks.filter(nb => {
        if (nb.ownerId === ownerUser.userId) return true
        if (ownerIsAdmin && !nb.ownerId) return true
        return false
      })
    }

    return notebooks
  }

  /**
   * Create a notebook with optional encryption and site publishing.
   *
   * @param {string} slug
   * @param {Object} options - { name, description, visibility, encrypted, password }
   * @param {string} ownerId
   * @returns {Promise<Object>} Created notebook
   */
  async create (slug, options, ownerId) {
    const encrypted = !!(options.encrypted && options.password)

    if (encrypted && !this.notebookCrypto) {
      throw new Error('Encryption not configured on server')
    }

    const notebook = await this.noteManager.createNotebook(slug, {
      name: options.name,
      description: options.description,
      encrypted,
      visibility: options.visibility || 'public',
      ownerId
    })

    if (encrypted) {
      const key = await this.notebookCrypto.enableEncryption(slug, options.password)
      this.noteManager.cacheKey(slug, key)
    }

    return notebook
  }

  /**
   * Get a single notebook by slug.
   */
  async get (slug) {
    return this.noteManager.getNotebook(slug)
  }

  /**
   * Update notebook metadata.
   */
  async update (slug, updates) {
    const allowed = {}
    if (updates.name !== undefined) allowed.name = updates.name
    if (updates.description !== undefined) allowed.description = updates.description
    if (updates.encrypted !== undefined) allowed.encrypted = updates.encrypted
    if (updates.visibility !== undefined) allowed.visibility = updates.visibility
    return this.noteManager.updateNotebook(slug, allowed)
  }

  /**
   * Delete a notebook.
   */
  async delete (slug) {
    return this.noteManager.deleteNotebook(slug)
  }

  /**
   * Rename a notebook.
   */
  async rename (oldSlug, newSlug) {
    return this.noteManager.renameNotebook(oldSlug, newSlug)
  }
}