0.1.15 • Published 4 months ago

mango-cms v0.1.15

Weekly downloads
-
License
ISC
Repository
-
Last release
4 months ago

Mango CMS Documentation

Mango is a powerful, code-first CMS built on MongoDB that emphasizes configuration through code. This documentation covers how to configure and use Mango CMS through the config folder.

Table of Contents

  1. Collections
  2. Fields
  3. Permissions
  4. Hooks
  5. Endpoints
  6. Plugins
  7. User Configuration
  8. Fresh Ubuntu Install

Collections

Collections are the foundation of Mango CMS. Each collection represents a MongoDB collection and is defined in the config/collections directory.

Basic Collection Structure

export default {
  // Required: Singular form of collection name
  singular: 'project',

  // Optional: Enable real-time updates via WebSocket
  subscribe: true,

  // Define collection fields
  fields: {
    title: String,
    description: String,
    // ... more fields
  },
}

Collection Options

  • singular: Required. The singular form of your collection name (e.g., 'project' for projects)
  • subscribe: Optional. Enable WebSocket subscriptions for real-time updates
  • formatRequest: Optional. Custom request formatting before processing

Request Formatting

Use formatRequest to modify incoming requests before they're processed:

export default {
    singular: 'project',
    formatRequest(request) {
        // Example 1: Add default search criteria
        if (request.method === 'read') {
            request.search = {
                ...request.search,
                status: 'active'
            }
        }

        // Example 2: Convert create to update for duplicate unique fields
        if (request.method === 'create' && request.document.address) {
            const existing = await readEntry({
                collection: 'project',
                search: { address: request.document.address }
            })
            if (existing) {
                request.method = 'update'
                request.document.id = existing.id
            }
        }

        return request
    }
}

Fields

Mango provides several built-in field types and allows for custom field creation.

Built-in Fields

import fields from '@cms/1. build/fields'
const { Relationship, Timestamp, File, Image } = fields

export default {
  fields: {
    // Basic Types
    title: String,
    count: 'Int',
    isActive: Boolean,

    // Complex Types
    createdAt: Timestamp(),
    thumbnail: Image(),
    attachment: File(),
    author: Relationship({
      collection: 'user',
      single: true,
    }),
    tags: Relationship({
      collection: 'tag',
    }),
  },
}

Field Options

Relationship Fields

Relationship({
  collection: 'user', // Required: Target collection
  single: true, // Optional: Single relationship (default: false)
  required: true, // Optional: Field is required
  default: null, // Optional: Default value
})

Computed Fields

{
    fields: {
        fullName: {
            type: String,
            computed: (document) => {
                return `${document.firstName} ${document.lastName}`
            },
            // Optional: Cache expiration in seconds
            expiresCache: 3600,
            // Optional: Values to provide to the computed function
            provide: ['firstName', 'lastName']
        }
    }
}

Field Translation

Fields can transform data on input and output using translateInput and translateOutput:

{
    fields: {
        price: {
            inputType: String,  // Input comes as string
            type: 'Int',       // Stored as integer
            // Transform input before saving
            translateInput: (value, { request, index, parentValue }) => {
                // Convert string price to number and handle currency
                return parseFloat(value.replace('$', '')) * 100
            },
            // Transform output before sending to client
            translateOutput: (value, { request, document }) => {
                // Convert stored cents to dollars with currency format
                return `$${(value / 100).toFixed(2)}`
            }
        }
    }
}

Custom Fields

Create custom fields in config/fields/ directory:

// config/fields/currency.js
export default {
  type: 'Currency',
  inputType: String,

  translateInput(value, { request }) {
    return parseFloat(value.replace('$', '')) * 100
  },

  translateOutput(value, { request, document }) {
    return `$${(value / 100).toFixed(2)}`
  },

  validate(value, { request }) {
    if (typeof value !== 'number' || isNaN(value)) {
      throw new Error('Invalid currency value')
    }
  },
}

Using custom fields in collections:

import Currency from '../fields/currency'

export default {
  singular: 'product',
  fields: {
    name: String,
    price: Currency(),
    salePrice: Currency(),
  },
}

Permissions

Mango provides flexible permission control at the collection level.

Basic Permissions

export default {
  permissions: {
    // Define permissions for each role in member.roles
    public: ['create'],
    owner: ['read', 'update', 'delete'],
  },
}

Function-based Permissions

export default {
  async permissions(request) {
    // For create requests, always check member permissions
    if (!request.id) {
      return {
        authorized: request.member?.roles.includes('editor'),
      }
    }

    // For existing documents, check ownership or team access
    const document = request.originalDocument
    return {
      authorized:
        // Member is the owner
        document.createdBy === request.member?.id ||
        // Member is part of the document's team
        document.team?.includes(request.member?.id) ||
        // Member has admin role
        request.member?.roles.includes('admin'),
    }
  },
}

Hooks

Hooks allow you to execute code at different stages of the CRUD operations.

export default {
  hooks: {
    created: async ({ request, response, document }) => {
      // Handle post-creation tasks
      console.log('New document created:', document.id)
    },
    read: async ({ request, response, document }) => {
      // Handle during read operation
      console.log('Document accessed:', document.id)
    },
    updated: async ({ request, response, document, originalDocument }) => {
      // Handle post-update tasks with access to previous version
      console.log('Document updated from:', originalDocument, 'to:', document)
    },
    deleted: async ({ request, response, document, originalDocument }) => {
      // Handle post-deletion tasks with access to deleted document
      console.log('Document deleted:', document.id)
      console.log('Original state:', originalDocument)
    },
  },
}

Endpoints

Create custom endpoints by exporting an object structure in config/endpoints/index.js:

import { sendEmail } from '../services/email'

export default {
  contact: {
    // POST /endpoints/contact
    async post(req) {
      const { name, email, message } = req.body

      // Validate required fields
      if (!name || !email || !message) {
        return {
          success: false,
          error: 'Missing required fields',
        }
      }

      // Send email
      await sendEmail({
        to: 'support@example.com',
        subject: `Contact Form: ${name}`,
        text: `From: ${name} (${email})\n\n${message}`,
      })

      return {
        success: true,
        message: 'Thank you for your message',
      }
    },

    support: {
      // POST /endpoints/contact/support
      async post(req) {
        const { ticketId, message } = req.body

        // Create support ticket
        const ticket = await createSupportTicket({
          id: ticketId,
          message,
          member: req.member.id,
        })

        return {
          success: true,
          ticket,
        }
      },
    },
  },
}

Plugins

Plugins extend Mango's functionality through a structured folder in config/plugins:

config/plugins/
  my-plugin/
    collections/
      widget.js
      gadget.js
    fields/
      customField.js
    endpoints/
      index.js

Example plugin structure:

// config/plugins/my-plugin/collections/widget.js
export default {
    singular: 'widget',
    fields: {
        name: String,
        configuration: {...}
    }
}

// config/plugins/my-plugin/fields/customField.js
export default {
    type: 'CustomField',
    // field implementation
}

// config/plugins/my-plugin/endpoints/index.js
export default {
    widgets: {
        async get(req) {
            return { success: true }
        }
    }
}

User Configuration

Extend the built-in member collection by creating config/config/users.js:

import fields from '@cms/1. build/fields'
const { Relationship, Image } = fields

export default {
  // Extend the member collection with custom fields
  fields: {
    // Built-in fields: email, password, roles

    // Add custom fields
    displayName: String,
    avatar: Image(),
    timezone: {
      type: String,
      default: 'UTC',
    },
    teams: Relationship({
      collection: 'team',
    }),
    preferences: {
      fields: {
        theme: String,
        notifications: Boolean,
      },
      default: {
        theme: 'light',
        notifications: true,
      },
    },
  },

  // Add member-specific hooks
  hooks: {
    created: async ({ document }) => {
      // Send welcome email
      await sendWelcomeEmail(document.email)
    },
    updated: async ({ document, originalDocument }) => {
      // Handle email change verification
      if (document.email !== originalDocument.email) {
        await sendEmailVerification(document.email)
      }
    },
  },

  // Custom member permissions
  permissions: {
    owner: ['read', 'update'],
    admin: ['create', 'read', 'update', 'delete'],
  },
}

The user configuration allows you to:

  • Add custom fields to the member collection
  • Define member-specific hooks
  • Set up member permissions
  • Handle user-related business logic

Member documents will automatically include these custom fields alongside the built-in authentication fields.

Best Practices

  1. Collection Names: Use singular form in collection definition
  2. Field Organization: Group related fields together
  3. Permissions: Start restrictive and open up as needed
  4. Validation: Implement thorough field validation
  5. Error Handling: Use try-catch blocks in hooks and custom logic

Fresh Ubuntu 24 Install

# Install nginx
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx

# Install Node
sudo apt update
sudo apt install -y nodejs
sudo apt install -y npm
sudo npm install -g n
sudo n stable
export PATH="/usr/local/bin:$PATH"
source ~/.bashrc  # or source ~/.zshrc
sudo apt remove -y nodejs
node -v

# Install MongoDB
sudo apt-get install gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo systemctl start mongod
sudo systemctl enable mongod

# Install PM2
sudo npm install -g pm2
pm2 startup

# Run ssh-keygen and add the public key to your keys in GitHub
# Clone your repo and build
# Add your dist folder to the nginx config at /etc/nginx/sites-available/default

# Configure Permissions
sudo chown -R www-data:www-data /root/Sites/{{repo}}/front/dist
sudo chmod -R 755 /root/Sites/{{repo}}/front/dist
sudo chmod +x /root
sudo chmod +x /root/Sites
sudo chmod +x /root/Sites/{{repo}}
sudo chmod +x /root/Sites/{{repo}}/front

# Ensure the site is working by going to the IP address in your browser
# Add your site DNS to CloudFlare and point it to the IP address

# Install Certbot
apt install certbot -y
sudo apt install python3-certbot-nginx
certbot

# Install redis
sudo apt install redis-server -y
sudo systemctl enable redis-server