Skip to content

Cesivi Server - Plugin Development Guide

HomeDocumentationUsage → Plugins

Complete guide for creating, testing, and deploying custom plugins for Cesivi Server.

Table of Contents

  1. Overview
  2. Quick Start
  3. Hook System
  4. Plugin Configuration
  5. Creating Plugins
  6. Testing Plugins
  7. Plugin Examples
  8. Troubleshooting

Overview

What are Plugins?

Cesivi Server plugins are HTTP endpoints that receive notifications when SharePoint objects are accessed or modified. Plugins enable you to extend the server with custom logic without modifying core code.

Plugin Capabilities

  • Auditing - Log all SharePoint operations
  • Validation - Enforce custom business rules
  • Synchronization - Replicate changes to external systems
  • Monitoring - Track usage patterns and performance
  • Transformation - Modify data before/after operations

How Plugins Work

Client Request → Cesivi Server
                      ↓
                 BEFORE Hook → Your Plugin (HTTP endpoint)
                      ↓
                 Process Request
                      ↓
                 AFTER Hook → Your Plugin (HTTP endpoint)
                      ↓
                 Response to Client

Event Types

  • BEFORE - Fired before operation (can modify or abort)
  • AFTER - Fired after operation (audit/notification only)

Quick Start

5-Minute Plugin

Step 1: Create Plugin Endpoint (Node.js)

Create hello-plugin.js:

const http = require('http');

const server = http.createServer((req, res) => {
    console.log(`Plugin called: ${req.method} ${req.url}`);

    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
        if (body) {
            const data = JSON.parse(body);
            console.log(`Operation: ${data.Operation} on ${data.ObjectType}`);
        }

        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(JSON.stringify({
            Success: true,
            Message: 'Hello from plugin!'
        }));
    });
});

server.listen(5001, () => {
    console.log('Plugin listening on port 5001');
});

Step 2: Start Plugin

node hello-plugin.js

Step 3: Configure in Cesivi

Edit appsettings.Plugins.json:

{
  "Cesivi": {
    "Plugins": [
      {
        "Id": "hello-world",
        "ObjectType": "Microsoft.SharePoint.SPList",
        "EventType": "After",
        "EndpointUrl": "http://localhost:5001",
        "TimeoutMs": 5000,
        "ContinueOnError": true,
        "Enabled": true
      }
    ]
  }
}

Step 4: Restart Cesivi & Test

# Start Mock Server
dotnet run --project Cesivi.Server

# In another terminal, create a list to trigger plugin
Connect-PnPOnline -Url "http://localhost:5000" -UseWebLogin
New-PnPList -Title "TestList" -Template GenericList

Your plugin console should show:

Plugin called: POST /
Operation: Save on Microsoft.SharePoint.SPList


Hook System

BEFORE Hooks

Fired before the operation is executed. Can: - Inspect incoming data - Modify data before processing - Abort the operation

Request Format (GET or POST):

{
  "CorrelationId": "guid",
  "ObjectType": "Microsoft.SharePoint.SPList",
  "Operation": "Add",
  "Method": "POST",
  "Endpoint": "/api/web/lists",
  "Context": {
    "WebAppName": "Default",
    "SiteName": "RootSite"
  },
  "Data": {
    "Title": "My List"
  }
}

Response Format:

{
  "Success": true,
  "Abort": false,
  "Message": "Validation passed",
  "ModifiedData": null
}

Abort Operation:

{
  "Success": false,
  "Abort": true,
  "Message": "List name not allowed"
}

AFTER Hooks

Fired after the operation completes. Can: - Inspect completed operations - Trigger side effects (logging, notifications) - Cannot modify the result

Request Format (POST):

{
  "CorrelationId": "guid",
  "ObjectType": "Microsoft.SharePoint.SPList",
  "Operation": "Add",
  "StatusCode": 201,
  "Duration": 150,
  "Context": {
    "WebAppName": "Default",
    "SiteName": "RootSite",
    "ListName": "Documents"
  },
  "Data": {
    "Title": "My List"
  }
}

Response Format:

{
  "Success": true,
  "Message": "Logged successfully"
}


Plugin Configuration

Configuration Properties

Property Type Description Required
Id string Unique plugin identifier Yes
ObjectType string SharePoint object type Yes
EventType string "Before" or "After" Yes
EndpointUrl string HTTP endpoint to call Yes
Headers object Additional headers No
TimeoutMs number Request timeout (ms) No (default: 5000)
ContinueOnError boolean Continue if plugin fails No (default: true)
Enabled boolean Whether plugin is active No (default: true)

Supported Object Types

Common object types: - Microsoft.SharePoint.SPList - Microsoft.SharePoint.SPListItem - Microsoft.SharePoint.SPFile - Microsoft.SharePoint.SPFolder - Microsoft.SharePoint.SPWeb - Microsoft.SharePoint.SPSite - Microsoft.SharePoint.SPUser - Microsoft.SharePoint.SPGroup

Use wildcards: - Microsoft.SharePoint.* - All SharePoint objects - Microsoft.SharePoint.SP* - All SharePoint objects

Environment Variables in Headers

Headers support environment variable expansion:

{
  "Headers": {
    "Authorization": "Bearer {AUTH_TOKEN}",
    "X-API-Key": "${API_KEY}"
  }
}

Set environment variables before starting server:

# Windows
set AUTH_TOKEN=your-token
set API_KEY=your-key

# Linux/Mac
export AUTH_TOKEN=your-token
export API_KEY=your-key

Creating Plugins

Example 1: Audit Logger (Python/Flask)

Purpose: Log all SharePoint list operations

Create audit-plugin.py:

from flask import Flask, request, jsonify
from datetime import datetime
import json

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def audit():
    timestamp = datetime.now().isoformat()
    data = request.get_json() or {}

    # Extract details
    object_type = data.get('ObjectType', 'Unknown')
    operation = data.get('Operation', 'Unknown')
    context = data.get('Context', {})

    # Log to console
    print(f"[{timestamp}] {operation} on {object_type}")
    print(f"  Context: {json.dumps(context)}")

    # Log to file
    with open('audit.log', 'a') as f:
        f.write(f"{timestamp} | {operation} | {object_type} | {json.dumps(context)}\n")

    return jsonify({
        'Success': True,
        'Message': f'Logged {operation} operation'
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5002)

Configuration:

{
  "Id": "audit-logger",
  "ObjectType": "Microsoft.SharePoint.*",
  "EventType": "After",
  "EndpointUrl": "http://localhost:5002",
  "TimeoutMs": 5000,
  "ContinueOnError": true,
  "Enabled": true
}

Run:

pip install flask
python audit-plugin.py

Example 2: Validation Plugin (.NET/C#)

Purpose: Prevent list creation without description

Create ValidationPlugin/Program.cs:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/", async (HttpContext context) =>
{
    using var reader = new StreamReader(context.Request.Body);
    var body = await reader.ReadToEndAsync();
    var data = JsonDocument.Parse(body);

    var operation = data.RootElement.GetProperty("Operation").GetString();
    var objectType = data.RootElement.GetProperty("ObjectType").GetString();

    // Validate list creation
    if (operation == "Save" && objectType == "Microsoft.SharePoint.SPList")
    {
        if (data.RootElement.TryGetProperty("Data", out var listData))
        {
            var description = listData.TryGetProperty("Description", out var desc)
                ? desc.GetString() : "";

            // Require description
            if (string.IsNullOrWhiteSpace(description))
            {
                return Results.Ok(new
                {
                    Success = false,
                    Abort = true,
                    Message = "Lists must have a description"
                });
            }
        }
    }

    return Results.Ok(new { Success = true });
});

app.MapGet("/health", () => Results.Ok(new { Status = "Healthy" }));

app.Run("http://localhost:5003");

Configuration:

{
  "Id": "list-validator",
  "ObjectType": "Microsoft.SharePoint.SPList",
  "EventType": "Before",
  "EndpointUrl": "http://localhost:5003",
  "TimeoutMs": 3000,
  "ContinueOnError": false,
  "Enabled": true
}

Run:

dotnet run

Example 3: Sync to Database (Node.js)

Purpose: Replicate list items to external database

Create sync-plugin.js:

const express = require('express');
const { MongoClient } = require('mongodb');

const app = express();
app.use(express.json());

const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017';
const dbName = 'sharepoint_sync';

app.post('/', async (req, res) => {
    const { ObjectType, Operation, Data, Context } = req.body;

    // Only sync list items
    if (ObjectType !== 'Microsoft.SharePoint.SPListItem') {
        return res.json({ Success: true });
    }

    try {
        const client = await MongoClient.connect(mongoUrl);
        const db = client.db(dbName);
        const collection = db.collection(Context.ListName || 'items');

        if (Operation === 'Add' || Operation === 'Update') {
            await collection.updateOne(
                { Id: Data.Id },
                { $set: Data },
                { upsert: true }
            );
        } else if (Operation === 'Delete') {
            await collection.deleteOne({ Id: Data.Id });
        }

        await client.close();

        res.json({
            Success: true,
            Message: `Synced ${Operation} to database`
        });
    } catch (error) {
        console.error('Sync error:', error);
        res.json({
            Success: false,
            Message: error.message
        });
    }
});

app.listen(5004, () => {
    console.log('Sync plugin on port 5004');
});

Configuration:

{
  "Id": "db-sync",
  "ObjectType": "Microsoft.SharePoint.SPListItem",
  "EventType": "After",
  "EndpointUrl": "http://localhost:5004",
  "Headers": {
    "X-DB-Connection": "{MONGO_URL}"
  },
  "TimeoutMs": 10000,
  "ContinueOnError": true,
  "Enabled": true
}


Testing Plugins

Manual Testing with curl

Test AFTER Hook:

curl -X POST http://localhost:5002 \
  -H "Content-Type: application/json" \
  -d '{
    "ObjectType": "Microsoft.SharePoint.SPList",
    "Operation": "Save",
    "Context": {
      "WebAppName": "Default",
      "SiteName": "RootSite",
      "ListName": "Documents"
    },
    "Data": {
      "Title": "Test List"
    }
  }'

Expected Response:

{
  "Success": true,
  "Message": "Logged Save operation"
}

Automated Testing Framework

Cesivi includes a plugin testing framework. See Plugin Testing Guide for details.

Example Test:

[TestClass]
public class MyPluginTests : PluginTestBase
{
    protected override string? PluginBaseUrl => "http://localhost:5002";

    [TestMethod]
    public async Task AfterHook_LogsOperation()
    {
        if (!await IsPluginAvailableAsync())
        {
            Assert.Inconclusive("Plugin not available");
            return;
        }

        var hookRequest = CreateHookRequest(
            Guid.NewGuid().ToString(),
            objectType: "List",
            operation: "Add"
        );

        var response = await SendToPluginAsync("/", hookRequest);

        AssertHookResponse(response, shouldSucceed: true);
    }
}

Testing Best Practices

  1. Health Check Endpoint - Add /health endpoint to plugins
  2. Graceful Degradation - Tests should be Inconclusive when plugin unavailable
  3. Test Both Hooks - Test BEFORE and AFTER independently
  4. Error Handling - Test plugin failure scenarios
  5. Performance - Ensure plugins respond quickly (<100ms)

Plugin Examples

Example 1: File Size Validator

Prevent uploads over 50MB:

app.get('/validate', (req, res) => {
    const { ObjectType, Data } = req.query;

    if (ObjectType === 'Microsoft.SharePoint.SPFile') {
        const fileSize = Data.Length || 0;
        const maxSize = 50 * 1024 * 1024; // 50MB

        if (fileSize > maxSize) {
            return res.json({
                Success: false,
                Abort: true,
                Message: `File size ${fileSize} exceeds limit ${maxSize}`
            });
        }
    }

    res.json({ Success: true });
});

Example 2: Naming Convention Enforcer

Require lists to start with uppercase:

@app.route('/validate', methods=['POST'])
def validate_name():
    data = request.get_json()

    if data.get('ObjectType') == 'Microsoft.SharePoint.SPList':
        title = data.get('Data', {}).get('Title', '')

        if not title[0].isupper():
            return jsonify({
                'Success': False,
                'Abort': True,
                'Message': 'List names must start with uppercase'
            })

    return jsonify({'Success': True})

Example 3: Slack Notification

Post to Slack on list deletion:

const axios = require('axios');

app.post('/notify', async (req, res) => {
    const { ObjectType, Operation, Context } = req.body;

    if (ObjectType === 'Microsoft.SharePoint.SPList' && Operation === 'Delete') {
        await axios.post(process.env.SLACK_WEBHOOK, {
            text: `List deleted: ${Context.ListName} in ${Context.SiteName}`
        });
    }

    res.json({ Success: true });
});

Troubleshooting

Plugin Not Called

Check: 1. Is Enabled: true? 2. Does ObjectType match? 3. Is endpoint accessible? (curl http://localhost:PORT/health) 4. Check Mock Server logs for plugin registration

Timeout Errors

[WARN] Plugin timed out after 5000ms

Solutions: - Increase TimeoutMs in configuration - Optimize plugin response time - Use async processing for slow operations

Authentication Failures

[WARN] Plugin returned 401: Unauthorized

Solutions: - Verify environment variables are set before server start - Check header configuration syntax - Test endpoint manually with curl

Data Not Modified (BEFORE hooks)

Ensure you return ModifiedData in response:

{
  "Success": true,
  "ModifiedData": {
    "Title": "Modified Title"
  }
}

Plugin Errors Break Server

Use ContinueOnError: true for non-critical plugins:

{
  "ContinueOnError": true
}

For validation plugins that must succeed:

{
  "EventType": "Before",
  "ContinueOnError": false
}


Navigation: - ← Usage Guide - Documentation Home

Last Updated: 2025-11-15