Skip to content

RuoYi DynamoDB - Node.js Implementation

Below is a Node.js implementation for the DynamoDB data models and operations previously defined in Go.

Models and Repositories

javascript
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');

// Configure AWS SDK
const dynamoDB = new AWS.DynamoDB.DocumentClient({
  region: 'your-region',
  // Add other configurations as needed
});

const TABLE_NAME = 'AccountManagement';

// ===================== Models =====================

/**
 * Client (Tenant) Entity
 */
class Client {
  constructor(data = {}) {
    this.PK = data.PK || `CLIENT#${data.id || ''}`;
    this.SK = 'METADATA';
    this.id = data.id || uuidv4();
    this.name = data.name || '';
    this.description = data.description || '';
    this.status = data.status || 'active';
    this.created_at = data.created_at || new Date().toISOString();
    this.created_by = data.created_by || '';
    this.updated_at = data.updated_at || new Date().toISOString();
    this.Type = 'CLIENT';
    this.GSI1PK = this.PK;
    this.GSI1SK = this.PK;
    this.GSI4PK = this.PK;
    this.GSI4SK = `CLIENT#${this.created_at}`;
  }

  static fromItem(item) {
    return item ? new Client(item) : null;
  }

  toItem() {
    return {
      ...this,
      PK: `CLIENT#${this.id}`,
      GSI1PK: `CLIENT#${this.id}`,
      GSI1SK: `CLIENT#${this.id}`,
      GSI4PK: `CLIENT#${this.id}`,
      GSI4SK: `CLIENT#${this.created_at}`,
    };
  }
}

/**
 * Project Entity
 */
class Project {
  constructor(data = {}) {
    this.PK = data.PK || `CLIENT#${data.client_id || ''}`;
    this.SK = data.SK || `PROJECT#${data.id || ''}`;
    this.id = data.id || uuidv4();
    this.name = data.name || '';
    this.client_id = data.client_id || '';
    this.description = data.description || '';
    this.status = data.status || 'active';
    this.created_at = data.created_at || new Date().toISOString();
    this.created_by = data.created_by || '';
    this.updated_at = data.updated_at || new Date().toISOString();
    this.Type = 'PROJECT';
    this.GSI1PK = data.GSI1PK || this.PK;
    this.GSI1SK = data.GSI1SK || this.SK;
    this.GSI4PK = data.GSI4PK || this.PK;
    this.GSI4SK = data.GSI4SK || `PROJECT#${this.created_at}`;
  }

  static fromItem(item) {
    return item ? new Project(item) : null;
  }

  toItem() {
    return {
      ...this,
      PK: `CLIENT#${this.client_id}`,
      SK: `PROJECT#${this.id}`,
      GSI1PK: `CLIENT#${this.client_id}`,
      GSI1SK: `PROJECT#${this.id}`,
      GSI4PK: `CLIENT#${this.client_id}`,
      GSI4SK: `PROJECT#${this.created_at}`,
    };
  }
}

/**
 * Building Entity
 */
class Building {
  constructor(data = {}) {
    this.PK = data.PK || `PROJECT#${data.project_id || ''}`;
    this.SK = data.SK || `BUILDING#${data.id || ''}`;
    this.id = data.id || uuidv4();
    this.name = data.name || '';
    this.project_id = data.project_id || '';
    this.client_id = data.client_id || '';
    this.address = data.address || '';
    this.description = data.description || '';
    this.created_at = data.created_at || new Date().toISOString();
    this.created_by = data.created_by || '';
    this.updated_at = data.updated_at || new Date().toISOString();
    this.Type = 'BUILDING';
    this.GSI1PK = data.GSI1PK || `CLIENT#${this.client_id}`;
    this.GSI1SK = data.GSI1SK || this.SK;
    this.GSI4PK = data.GSI4PK || `CLIENT#${this.client_id}`;
    this.GSI4SK = data.GSI4SK || `BUILDING#${this.created_at}`;
  }

  static fromItem(item) {
    return item ? new Building(item) : null;
  }

  toItem() {
    return {
      ...this,
      PK: `PROJECT#${this.project_id}`,
      SK: `BUILDING#${this.id}`,
      GSI1PK: `CLIENT#${this.client_id}`,
      GSI1SK: `BUILDING#${this.id}`,
      GSI4PK: `CLIENT#${this.client_id}`,
      GSI4SK: `BUILDING#${this.created_at}`,
    };
  }
}

/**
 * Role Entity
 */
class Role {
  constructor(data = {}) {
    this.PK = data.PK || `CLIENT#${data.client_id || ''}`;
    this.SK = data.SK || `ROLE#${data.id || ''}`;
    this.id = data.id || uuidv4();
    this.name = data.name || '';
    this.is_system = data.is_system || 'false';
    this.description = data.description || '';
    this.permissions = data.permissions || '[]';
    this.created_at = data.created_at || new Date().toISOString();
    this.created_by = data.created_by || '';
    this.updated_at = data.updated_at || new Date().toISOString();
    this.Type = 'ROLE';
    this.GSI1PK = data.GSI1PK || this.PK;
    this.GSI1SK = data.GSI1SK || this.SK;
    this.GSI4PK = data.GSI4PK || this.PK;
    this.GSI4SK = data.GSI4SK || `ROLE#${this.created_at}`;
  }

  static fromItem(item) {
    return item ? new Role(item) : null;
  }

  toItem() {
    return {
      ...this,
      PK: `CLIENT#${this.client_id}`,
      SK: `ROLE#${this.id}`,
      GSI1PK: `CLIENT#${this.client_id}`,
      GSI1SK: `ROLE#${this.id}`,
      GSI4PK: `CLIENT#${this.client_id}`,
      GSI4SK: `ROLE#${this.created_at}`,
    };
  }

  // Parse stored JSON permissions string to array
  getPermissionsArray() {
    try {
      return JSON.parse(this.permissions || '[]');
    } catch (error) {
      console.error('Failed to parse permissions:', error);
      return [];
    }
  }
}

/**
 * Menu Entity
 */
class Menu {
  constructor(data = {}) {
    this.PK = data.PK || `CLIENT#${data.client_id || ''}`;
    this.SK = data.SK || `MENU#${data.menu_id || ''}`;
    this.Type = 'MENU';
    this.menu_id = data.menu_id || uuidv4();
    this.menu_name = data.menu_name || '';
    this.parent_id = data.parent_id || '0';
    this.order_num = data.order_num || 0;
    this.path = data.path || '';
    this.component = data.component || '';
    this.query_param = data.query_param || '';
    this.is_frame = data.is_frame || 0;
    this.is_cache = data.is_cache || 0;
    this.menu_type = data.menu_type || 'M';
    this.visible = data.visible || '0';
    this.status = data.status || '0';
    this.permissions = data.permissions || '';
    this.perms = data.perms || '';
    this.icon = data.icon || '';
    this.create_dept = data.create_dept || '';
    this.create_by = data.create_by || '';
    this.create_time = data.create_time || new Date().toISOString();
    this.update_by = data.update_by || '';
    this.update_time = data.update_time || new Date().toISOString();
    this.remark = data.remark || '';
    this.GSI1PK = data.GSI1PK || this.PK;
    this.GSI1SK = data.GSI1SK || this.SK;
    this.GSI2PK = data.GSI2PK || `MENU#${this.parent_id}`;
    this.GSI2SK = data.GSI2SK || `MENU#${this.menu_id}`;
  }

  static fromItem(item) {
    return item ? new Menu(item) : null;
  }

  toItem() {
    return {
      ...this,
      PK: `CLIENT#${this.client_id}`,
      SK: `MENU#${this.menu_id}`,
      GSI1PK: `CLIENT#${this.client_id}`,
      GSI1SK: `MENU#${this.menu_id}`,
      GSI2PK: `MENU#${this.parent_id}`,
      GSI2SK: `MENU#${this.menu_id}`,
    };
  }
}

// ===================== Repositories =====================

/**
 * Client Repository
 */
class ClientRepository {
  /**
   * Get client by ID
   * @param {string} clientId - The client ID
   * @returns {Promise<Client|null>} The client or null
   */
  async getById(clientId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: 'METADATA'
      }
    };

    try {
      const result = await dynamoDB.get(params).promise();
      return Client.fromItem(result.Item);
    } catch (error) {
      console.error('Error getting client by ID:', error);
      throw error;
    }
  }

  /**
   * Get all clients
   * @returns {Promise<Client[]>} Array of clients
   */
  async getAll() {
    const params = {
      TableName: TABLE_NAME,
      FilterExpression: 'begins_with(PK, :pk) AND SK = :sk',
      ExpressionAttributeValues: {
        ':pk': 'CLIENT#',
        ':sk': 'METADATA'
      }
    };

    try {
      const result = await dynamoDB.scan(params).promise();
      return result.Items.map(item => Client.fromItem(item));
    } catch (error) {
      console.error('Error getting all clients:', error);
      throw error;
    }
  }

  /**
   * Get clients by status
   * @param {string} status - The status to filter by
   * @returns {Promise<Client[]>} Array of clients with the given status
   */
  async getByStatus(status) {
    const params = {
      TableName: TABLE_NAME,
      FilterExpression: 'begins_with(PK, :pk) AND SK = :sk AND #status = :status',
      ExpressionAttributeNames: {
        '#status': 'status'
      },
      ExpressionAttributeValues: {
        ':pk': 'CLIENT#',
        ':sk': 'METADATA',
        ':status': status
      }
    };

    try {
      const result = await dynamoDB.scan(params).promise();
      return result.Items.map(item => Client.fromItem(item));
    } catch (error) {
      console.error(`Error getting clients with status ${status}:`, error);
      throw error;
    }
  }

  /**
   * Save client
   * @param {Client} client - The client to save
   * @returns {Promise<Client>} The saved client
   */
  async save(client) {
    if (!client.id) {
      client.id = uuidv4();
    }

    const now = new Date().toISOString();
    if (!client.created_at) {
      client.created_at = now;
    }
    client.updated_at = now;

    const item = client.toItem();
    
    const params = {
      TableName: TABLE_NAME,
      Item: item
    };

    try {
      await dynamoDB.put(params).promise();
      return client;
    } catch (error) {
      console.error('Error saving client:', error);
      throw error;
    }
  }

  /**
   * Delete client (soft delete)
   * @param {string} clientId - The client ID to delete
   * @returns {Promise<void>}
   */
  async delete(clientId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: 'METADATA'
      },
      UpdateExpression: 'SET #status = :status, updated_at = :updatedAt',
      ExpressionAttributeNames: {
        '#status': 'status'
      },
      ExpressionAttributeValues: {
        ':status': 'deleted',
        ':updatedAt': new Date().toISOString()
      }
    };

    try {
      await dynamoDB.update(params).promise();
    } catch (error) {
      console.error('Error deleting client:', error);
      throw error;
    }
  }
}

/**
 * Project Repository
 */
class ProjectRepository {
  /**
   * Get project by ID
   * @param {string} clientId - The client ID
   * @param {string} projectId - The project ID
   * @returns {Promise<Project|null>} The project or null
   */
  async getById(clientId, projectId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: `PROJECT#${projectId}`
      }
    };

    try {
      const result = await dynamoDB.get(params).promise();
      return Project.fromItem(result.Item);
    } catch (error) {
      console.error('Error getting project by ID:', error);
      throw error;
    }
  }

  /**
   * Get all projects for a client
   * @param {string} clientId - The client ID
   * @returns {Promise<Project[]>} Array of projects
   */
  async getByClientId(clientId) {
    const params = {
      TableName: TABLE_NAME,
      KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
      ExpressionAttributeValues: {
        ':pk': `CLIENT#${clientId}`,
        ':sk': 'PROJECT#'
      }
    };

    try {
      const result = await dynamoDB.query(params).promise();
      return result.Items.map(item => Project.fromItem(item));
    } catch (error) {
      console.error(`Error getting projects for client ${clientId}:`, error);
      throw error;
    }
  }

  /**
   * Save project
   * @param {Project} project - The project to save
   * @returns {Promise<Project>} The saved project
   */
  async save(project) {
    if (!project.id) {
      project.id = uuidv4();
    }

    const now = new Date().toISOString();
    if (!project.created_at) {
      project.created_at = now;
    }
    project.updated_at = now;

    const item = project.toItem();
    
    const params = {
      TableName: TABLE_NAME,
      Item: item
    };

    try {
      await dynamoDB.put(params).promise();
      return project;
    } catch (error) {
      console.error('Error saving project:', error);
      throw error;
    }
  }

  /**
   * Delete project (soft delete)
   * @param {string} clientId - The client ID
   * @param {string} projectId - The project ID to delete
   * @returns {Promise<void>}
   */
  async delete(clientId, projectId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: `PROJECT#${projectId}`
      },
      UpdateExpression: 'SET #status = :status, updated_at = :updatedAt',
      ExpressionAttributeNames: {
        '#status': 'status'
      },
      ExpressionAttributeValues: {
        ':status': 'deleted',
        ':updatedAt': new Date().toISOString()
      }
    };

    try {
      await dynamoDB.update(params).promise();
    } catch (error) {
      console.error('Error deleting project:', error);
      throw error;
    }
  }
}

/**
 * Role Repository
 */
class RoleRepository {
  /**
   * Get role by ID
   * @param {string} clientId - The client ID
   * @param {string} roleId - The role ID
   * @returns {Promise<Role|null>} The role or null
   */
  async getById(clientId, roleId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: `ROLE#${roleId}`
      }
    };

    try {
      const result = await dynamoDB.get(params).promise();
      return Role.fromItem(result.Item);
    } catch (error) {
      console.error('Error getting role by ID:', error);
      throw error;
    }
  }

  /**
   * Get all roles for a client
   * @param {string} clientId - The client ID
   * @returns {Promise<Role[]>} Array of roles
   */
  async getByClientId(clientId) {
    const params = {
      TableName: TABLE_NAME,
      KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
      ExpressionAttributeValues: {
        ':pk': `CLIENT#${clientId}`,
        ':sk': 'ROLE#'
      }
    };

    try {
      const result = await dynamoDB.query(params).promise();
      return result.Items.map(item => Role.fromItem(item));
    } catch (error) {
      console.error(`Error getting roles for client ${clientId}:`, error);
      throw error;
    }
  }

  /**
   * Get system roles
   * @param {string} clientId - The client ID
   * @returns {Promise<Role[]>} Array of system roles
   */
  async getSystemRoles(clientId) {
    const params = {
      TableName: TABLE_NAME,
      KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
      FilterExpression: 'is_system = :isSystem',
      ExpressionAttributeValues: {
        ':pk': `CLIENT#${clientId}`,
        ':sk': 'ROLE#',
        ':isSystem': 'true'
      }
    };

    try {
      const result = await dynamoDB.query(params).promise();
      return result.Items.map(item => Role.fromItem(item));
    } catch (error) {
      console.error(`Error getting system roles for client ${clientId}:`, error);
      throw error;
    }
  }

  /**
   * Save role
   * @param {Role} role - The role to save
   * @returns {Promise<Role>} The saved role
   */
  async save(role) {
    if (!role.id) {
      role.id = uuidv4();
    }

    const now = new Date().toISOString();
    if (!role.created_at) {
      role.created_at = now;
    }
    role.updated_at = now;

    // Ensure permissions is a string (JSON)
    if (typeof role.permissions === 'object') {
      role.permissions = JSON.stringify(role.permissions);
    }

    const item = role.toItem();
    
    const params = {
      TableName: TABLE_NAME,
      Item: item
    };

    try {
      await dynamoDB.put(params).promise();
      return role;
    } catch (error) {
      console.error('Error saving role:', error);
      throw error;
    }
  }

  /**
   * Delete role (soft delete)
   * @param {string} clientId - The client ID
   * @param {string} roleId - The role ID to delete
   * @returns {Promise<void>}
   */
  async delete(clientId, roleId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: `ROLE#${roleId}`
      },
      UpdateExpression: 'SET #status = :status, updated_at = :updatedAt',
      ExpressionAttributeNames: {
        '#status': 'status'
      },
      ExpressionAttributeValues: {
        ':status': 'deleted',
        ':updatedAt': new Date().toISOString()
      }
    };

    try {
      await dynamoDB.update(params).promise();
    } catch (error) {
      console.error('Error deleting role:', error);
      throw error;
    }
  }
}

/**
 * Menu Repository
 */
class MenuRepository {
  /**
   * Get menu by ID
   * @param {string} clientId - The client ID
   * @param {string} menuId - The menu ID
   * @returns {Promise<Menu|null>} The menu or null
   */
  async getById(clientId, menuId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: `MENU#${menuId}`
      }
    };

    try {
      const result = await dynamoDB.get(params).promise();
      return Menu.fromItem(result.Item);
    } catch (error) {
      console.error('Error getting menu by ID:', error);
      throw error;
    }
  }

  /**
   * Get all menus for a client
   * @param {string} clientId - The client ID
   * @returns {Promise<Menu[]>} Array of menus
   */
  async getByClientId(clientId) {
    const params = {
      TableName: TABLE_NAME,
      KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
      ExpressionAttributeValues: {
        ':pk': `CLIENT#${clientId}`,
        ':sk': 'MENU#'
      }
    };

    try {
      const result = await dynamoDB.query(params).promise();
      return result.Items.map(item => Menu.fromItem(item));
    } catch (error) {
      console.error(`Error getting menus for client ${clientId}:`, error);
      throw error;
    }
  }

  /**
   * Get menus by parent ID using GSI2
   * @param {string} clientId - The client ID
   * @param {string} parentId - The parent menu ID
   * @returns {Promise<Menu[]>} Array of child menus
   */
  async getByParentId(parentId) {
    const params = {
      TableName: TABLE_NAME,
      IndexName: 'GSI2',
      KeyConditionExpression: 'GSI2PK = :pk',
      ExpressionAttributeValues: {
        ':pk': `MENU#${parentId}`
      }
    };

    try {
      const result = await dynamoDB.query(params).promise();
      return result.Items.map(item => Menu.fromItem(item));
    } catch (error) {
      console.error(`Error getting menus with parent ID ${parentId}:`, error);
      throw error;
    }
  }

  /**
   * Get menus by type
   * @param {string} clientId - The client ID
   * @param {string} menuType - The menu type (M, C, F)
   * @returns {Promise<Menu[]>} Array of menus of the specified type
   */
  async getByType(clientId, menuType) {
    const params = {
      TableName: TABLE_NAME,
      KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
      FilterExpression: 'menu_type = :menuType',
      ExpressionAttributeValues: {
        ':pk': `CLIENT#${clientId}`,
        ':sk': 'MENU#',
        ':menuType': menuType
      }
    };

    try {
      const result = await dynamoDB.query(params).promise();
      return result.Items.map(item => Menu.fromItem(item));
    } catch (error) {
      console.error(`Error getting menus of type ${menuType}:`, error);
      throw error;
    }
  }

  /**
   * Save menu
   * @param {Menu} menu - The menu to save
   * @returns {Promise<Menu>} The saved menu
   */
  async save(menu) {
    if (!menu.menu_id) {
      menu.menu_id = uuidv4();
    }

    const now = new Date().toISOString();
    if (!menu.create_time) {
      menu.create_time = now;
    }
    menu.update_time = now;

    const item = menu.toItem();
    
    const params = {
      TableName: TABLE_NAME,
      Item: item
    };

    try {
      await dynamoDB.put(params).promise();
      return menu;
    } catch (error) {
      console.error('Error saving menu:', error);
      throw error;
    }
  }

  /**
   * Delete menu (soft delete)
   * @param {string} clientId - The client ID
   * @param {string} menuId - The menu ID to delete
   * @returns {Promise<void>}
   */
  async delete(clientId, menuId) {
    const params = {
      TableName: TABLE_NAME,
      Key: {
        PK: `CLIENT#${clientId}`,
        SK: `MENU#${menuId}`
      },
      UpdateExpression: 'SET #status = :status, update_time = :updateTime',
      ExpressionAttributeNames: {
        '#status': 'status'
      },
      ExpressionAttributeValues: {
        ':status': '1', // 1 = disabled in RuoYi
        ':updateTime': new Date().toISOString()
      }
    };

    try {
      await dynamoDB.update(params).promise();
    } catch (error) {
      console.error('Error deleting menu:', error);
      throw error;
    }
  }
}

// ===================== Utility Functions =====================

/**
 * Build menu tree from flat menu array
 * @param {Menu[]} menus - Array of menus
 * @param {string} parentId - Parent ID to start from (usually '0')
 * @returns {Object[]} Tree of menu nodes
 */
function buildMenuTree(menus, parentId = '0') {
  const nodes = [];
  
  for (const menu of menus) {
    if (menu.parent_id === parentId) {
      const node = {
        menu,
        children: buildMenuTree(menus, menu.menu_id)
      };
      nodes.push(node);
    }
  }
  
  // Sort by order_num
  return nodes.sort((a, b) => a.menu.order_num - b.menu.order_num);
}

/**
 * Get user roles and permissions
 * @param {string} userId - The user ID
 * @param {string} clientId - The client ID
 * @returns {Promise<Object>} Object containing roles and permissions
 */
async function getUserRolesAndPermissions(userId, clientId) {
  // This function assumes you have a UserRoleRepository and RoleRepository
  const userRoleRepo = new UserRoleRepository();
  const roleRepo = new RoleRepository();
  
  try {
    // Get user roles
    const userRoles = await userRoleRepo.getByUserId(userId);
    
    // Get role details and extract permissions
    const roles = [];
    const allPermissions = new Set();
    
    for (const userRole of userRoles) {
      const role = await roleRepo.getById(clientId, userRole.role_id);
      if (role) {
        roles.push(role);
        
        // Extract permissions from role
        const permissions = role.getPermissionsArray();
        for (const perm of permissions) {
          if (perm.action && perm.resource) {
            allPermissions.add(`${perm.action}:${perm.resource}`);
          }
        }
      }
    }
    
    return {
      roles,
      permissions: Array.from(allPermissions)
    };
  } catch (error) {
    console.error('Error getting user roles and permissions:', error);
    throw error;
  }
}

/**
 * Check if user has a specific permission
 * @param {string[]} permissions - User's permission array
 * @param {string} requiredPermission - Permission to check
 * @returns {boolean} True if user has permission
 */
function hasPermission(permissions, requiredPermission) {
  // Check for exact match or wildcard permissions
  if (permissions.includes(requiredPermission) || permissions.includes('*:*')) {
    return true;
  }
  
  // Check for wildcard permissions (e.g., "system:*")
  for (const perm of permissions) {
    if (perm.includes('*')) {
      const pattern = perm.replace(/\*/g, '.*');
      const regex = new RegExp(`^${pattern}$`);
      if (regex.test(requiredPermission)) {
        return true;
      }
    }
  }
  
  return false;
}

// ===================== Example Usage =====================

/**
 * Example: Create a new client
 */
async function createClient() {
  const clientRepo = new ClientRepository();
  
  const client = new Client({
    name: 'Test Client',
    description: 'This is a test client',
    created_by: 'admin'
  });
  
  try {
    const savedClient = await clientRepo.save(client);
    console.log('Client created:', savedClient);
    return savedClient;
  } catch (error) {
    console.error('Failed to create client:', error);
    throw error;
  }
}

/**
 * Example: Create a project for a client
 */
async function createProject(clientId) {
  const projectRepo = new ProjectRepository();
  
  const project = new Project({
    name: 'Test Project',
    client_id: clientId,
    description: 'This is a test project',
    created_by: 'admin'
  });
  
  try {
    const savedProject = await projectRepo.save(project);
    console.log('Project created:', savedProject);
    return savedProject;
  } catch (error) {
    console.error('Failed to create project:', error);
    throw error;
  }
}

/**
 * Example: Get client menu tree
 */
async function getClientMenuTree(clientId) {
  const menuRepo = new MenuRepository();
  
  try {
    const menus = await menuRepo.getByClientId(clientId);
    const menuTree = buildMenuTree(menus);
    console.log('Menu tree:', JSON.stringify(menuTree, null, 2));
    return menuTree;
  } catch (error) {
    console.error('Failed to get menu tree:', error);
    throw error;
  }
}

module.exports = {
  // Models
  Client,
  Project,
  Building,
  Role,
  Menu,
  
  // Repositories
  ClientRepository,
  ProjectRepository,
  RoleRepository,
  MenuRepository,
  
  // Utility functions
  buildMenuTree,
  getUserRolesAndPermissions,
  hasPermission,
  
  // Example functions
  createClient,
  createProject,
  getClientMenuTree
};

Key Differences from Go Implementation

  1. Dynamic Types: JavaScript's dynamic typing eliminates the need for explicit type declarations and conversions.

  2. Object Manipulation: JavaScript's object spread syntax (...obj) and JSON handling make object manipulation simpler.

  3. Promise-based Async: Uses JavaScript's Promise-based async/await pattern instead of Go's context-based approach.

  4. Class-based Models: Models are implemented as classes with methods like toItem() and static fromItem().

  5. Built-in JSON Handling: No need for explicit JSON marshaling/unmarshaling since DynamoDB DocumentClient handles this.

  6. Regular Expressions: JavaScript's built-in regex handling is more concise for permission checking.

Benefits of Node.js Implementation

  1. Less Boilerplate: Significantly less code compared to Go implementation.

  2. Natural JSON Handling: DynamoDB stores items as JSON, which aligns perfectly with JavaScript objects.

  3. Async Operations: JavaScript's async/await makes handling asynchronous DynamoDB operations more intuitive.

  4. AWS SDK Integration: AWS SDK for JavaScript has excellent DynamoDB support with DocumentClient.

  5. Server-side & Lambda Compatibility: Code works in both Node.js server environments and AWS Lambda functions.

Usage in AWS Lambda

This code can be directly used in AWS Lambda functions with minimal modifications:

javascript
// Lambda handler example
exports.handler = async (event) => {
  const clientId = event.pathParameters.clientId;
  const clientRepo = new ClientRepository();
  
  try {
    const client = await clientRepo.getById(clientId);
    if (!client) {
      return {
        statusCode: 404,
        body: JSON.stringify({ message: 'Client not found' })
      };
    }
    
    return {
      statusCode: 200,
      body: JSON.stringify(client)
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Internal server error' })
    };
  }
};

This Node.js implementation provides a complete solution for working with RuoYi data models in DynamoDB, with significant improvements in code readability and maintainability compared to the Go version.