Cesivi Server - Plugin Development Guide¶
Home → Documentation → Usage → Plugins
Complete guide for creating, testing, and deploying custom plugins for Cesivi Server.
Table of Contents¶
- Overview
- Quick Start
- Hook System
- Plugin Configuration
- Creating Plugins
- Testing Plugins
- Plugin Examples
- 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¶
- Health Check Endpoint - Add
/healthendpoint to plugins - Graceful Degradation - Tests should be Inconclusive when plugin unavailable
- Test Both Hooks - Test BEFORE and AFTER independently
- Error Handling - Test plugin failure scenarios
- 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
}
Related Documentation¶
- Basic Operations - Working with SharePoint data
- Export/Import Guide - Backup and restore
- API Reference - API documentation
- Plugin Testing Guide - Automated testing
Navigation: - ← Usage Guide - Documentation Home
Last Updated: 2025-11-15