# MIDCON.Backend.SDK.Server

SDK for developing backend REST-API plugins for MIDCON Backend Server. Provides base classes and interfaces for creating custom API controller plugins that integrate with Odoo auth_api_key routes.

## What is this SDK?

This SDK allows developers to create plugin controllers that:
- Integrate seamlessly with the MIDCON Backend Server plugin system
- Automatically connect to Odoo authentication via API keys
- Access authenticated Odoo user context in controller actions
- Declare required Odoo module dependencies

## Quick Start

### 1. Create a Plugin Project

```bash
dotnet new classlib -n MyCompany.Plugin.CustomModule
cd MyCompany.Plugin.CustomModule
```

### 2. Add the SDK Package

```bash
dotnet add package MIDCON.Backend.SDK.Server --source MidConsulting
```

### 3. Create a Plugin Controller

```csharp
using MIDCON.Backend.SDK.Server;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Odoo.Net.Core;

namespace MyCompany.Plugin.CustomModule;

[Route("api/custom")]
[Authorize(Policy = "OdooApiPolicy")]
public class CustomController : OdooPluginControllerBase
{
    public override OdooPluginMetadata Metadata => new(
        Name: "custom_module",
        DisplayName: "My Custom Module",
        Version: "1.0.0",
        RequiredOdooModules: new[] { "base", "custom_module" }
    );

    private readonly IOdooJson2Client _odoo;

    public CustomController(IOdooUserService odooService, IOdooJson2Client odoo)
        : base(odooService)
    {
        _odoo = odoo;
    }

    [HttpGet("data")]
    public async Task<IActionResult> GetData()
    {
        var apiKey = GetUserApiKey();
        await _odoo.WithApiKeyAsync(apiKey);

        var partners = await _odoo.SearchReadAsync<dynamic>(
            "res.partner",
            fields: ["name", "email"],
            limit: 10);

        return Ok(partners);
    }
}
```

### 4. Build Your Plugin

```bash
dotnet build
```

### 5. Deploy to MIDCON Backend Server

Copy your plugin DLL to the MIDCON Backend Server plugins directory and restart the server.

## Host Setup (midcon-backend-server)

The server must call `AddMidconPluginHost()` once in `Program.cs`. This is the only
place Odoo clients need to be registered — plugins receive them via DI automatically.

```csharp
// Program.cs
builder.Services.AddMidconPluginHost(options =>
{
    options.BaseUrl = builder.Configuration["Odoo:BaseUrl"]!;  // shared by all clients

    // JSON-2 client: Odoo 19+, Bearer auth
    options.OdooJson2 = cfg =>
    {
        cfg.Database = builder.Configuration["Odoo:Database"]!;
    };

    // RPC client: all Odoo versions, OCA module support
    options.OdooRpc = cfg =>
    {
        cfg.Database = builder.Configuration["OdooRpc:Database"]!;
        cfg.Username = builder.Configuration["OdooRpc:Username"]!;
        cfg.Password = builder.Configuration["OdooRpc:Password"]!;
    };

    // IOdooRouteClient is registered automatically when BaseUrl is set.
});
```

`OdooJson2` and `OdooRpc` are optional — omit to skip registration. `IOdooRouteClient` is always registered when `BaseUrl` is set.

## Injectable Services for Plugin Developers

After the host calls `AddMidconPluginHost()`, plugins can inject:

| Interface | Client | Use case |
|-----------|--------|----------|
| `IOdooJson2Client` | JSON-2 | Odoo 19+, standard ORM (Bearer auth) |
| `IOdooRpcClient` | RPC | All versions, OCA modules (session auth) |
| `IOdooRouteClient` | Route | Custom OCA `auth_api_key` routes, per-call API-KEY header |
| `IOdooClient` | either | Generic — resolves to whichever was registered |
| `IOdooUserService` | — | Current request's Odoo API key |
| `IOdooAddonService` | JSON-2 | Install/list MIDCON plugin addons via `api.installer` |
| `IOdooRouteInfoService` | JSON-2 | Discover registered Odoo routes via `api.route.info` |

```csharp
// Inject the specific interface you need
public class MyController(IOdooUserService odooService, IOdooRouteClient routeClient)
    : OdooPluginControllerBase(odooService)
{
    // IOdooRouteClient: per-call API key passed directly to CallAsync()
    // IOdooJson2Client: per-request API key via WithApiKeyAsync()
    // IOdooRpcClient:   uses credentials from appsettings (session-based)
}
```

### Client Selection Guide: `auth.api.key` and OCA ir.rules

Choosing the wrong client for `auth.api.key` operations is a common source of `Access Denied` errors.

| Operation | Correct Client | Reason |
|-----------|---------------|--------|
| Create API key for **another** user (`user_id != own uid`) | `IOdooJson2Client` (Admin-Bearer) | Admin-Bearer bypasses OCA ir.rule `[('user_id', '=', uid)]` |
| Create API key for the **authenticated** user itself | `IOdooRpcClient` | Session auth respects ir.rules — allowed for own records |
| Read/write **own** records | `IOdooRpcClient` | Session auth enforces ir.rules correctly |

**`IOdooJson2Client` — Admin operations (`@api.model_create_multi`)**

Use with an Admin-Bearer token to create records for arbitrary users. Supports
`@api.model_create_multi` models (e.g. `auth.api.key`) correctly — the request
body sends `vals_list` as kwargs (fixed in Odoo.Net ≥ 2.1.1):

```csharp
// Admin creates an auth.api.key for another user (user_id = 5)
// → IOdooJson2Client with Admin-Bearer bypasses OCA ir.rule
var apiKeyProvider = HttpContext.RequestServices.GetRequiredService<IOdooApiKeyProvider>();
await apiKeyProvider.WithApiKeyAsync(adminBearerToken, async () =>
{
    var ids = await _json2.CreateManyAsync("auth.api.key", new[]
    {
        new { name = req.Name, key = generatedKey, user_id = req.UserId }
    });
});
```

**`IOdooRpcClient` — Session-based operations (own user)**

Use for operations where OCA ir.rules should be enforced (users only access their own records):

```csharp
// User reads their own partners — ir.rule enforced, correct behaviour
var partners = await _rpc.SearchReadAsync<Partner>("res.partner");

// ⚠️ Do NOT use IOdooRpcClient to create auth.api.key for a different user_id:
// var ids = await _rpc.CreateManyAsync("auth.api.key", new[]
// {
//     new { name = req.Name, user_id = otherUserId }  // → Access Denied!
// });
```

## API Reference

### MidconSdkExtensions

| Method | Description |
|--------|-------------|
| `AddMidconPluginHost(options)` | Registers all plugin-injectable Odoo services |

### MidconPluginHostOptions

| Property | Type | Description |
|----------|------|-------------|
| `BaseUrl` | `string` | Odoo instance URL, shared by all clients. Setting this automatically registers `IOdooRouteClient` |
| `OdooRouteTimeoutSeconds` | `int` | Timeout for `IOdooRouteClient` in seconds. Default: 30 |
| `OdooJson2` | `Action<OdooConfig>?` | Configures `IOdooJson2Client`. `null` = not registered |
| `OdooRpc` | `Action<OdooRpcConfig>?` | Configures `IOdooRpcClient`. `null` = not registered |

### OdooPluginControllerBase

Abstract base class for all plugin controllers.

**Properties:**
- `Metadata` (abstract): Plugin metadata including name, version, and dependencies
- `OdooService`: Service for accessing Odoo user context

**Methods:**
- `GetUserApiKey()`: Retrieves the API key for the currently authenticated Odoo user

### OdooPluginMetadata

Record containing plugin metadata.

**Parameters:**
- `Name`: Internal plugin name (used for routing)
- `DisplayName`: Human-readable display name
- `Version`: Plugin version (SemVer recommended)
- `RequiredOdooModules`: Array of required Odoo module technical names

### IOdooUserService

Interface for accessing authenticated Odoo user information.

**Methods:**
- `GetCurrentUserApiKey()`: Gets the current user's API key

### IOdooAddonService

Service for managing MIDCON plugin addons in Odoo via `api.installer`.
Requires `OdooJson2` to be configured in `AddMidconPluginHost()`.

**Methods:**
- `InstallAsync(name, models, ct)`: Installs an addon and returns `AddonInstallResult`
- `ListAsync(ct)`: Returns all known addons as `AddonInfo[]`

### AddonInstallResult

Returned by `IOdooAddonService.InstallAsync()` after a successful installation.

| Property | Type | Description |
|----------|------|-------------|
| `ModuleName` | `string` | Technical Odoo module name created (e.g. `"midcon_plugin_notes"`) |
| `Models` | `string[]` | Technical model names created by this installation |
| `RestartRequired` | `bool` | `true` when Odoo signals a server reload is needed (`reload` field is non-empty) |

```csharp
var result = await _addonService.InstallAsync("midcon_plugin_notes", new[]
{
    new AddonModelDef("midcon.note", "MIDCON Note", "note", new[]
    {
        new AddonFieldDef("name",    "char",     "Title",   Required: true),
        new AddonFieldDef("partner", "many2one", "Partner", ComodelName: "res.partner")
    })
});

Console.WriteLine($"Module: {result.ModuleName}");
Console.WriteLine($"Models: {string.Join(", ", result.Models)}");
if (result.RestartRequired)
    Console.WriteLine("Odoo restart required to activate addon.");
```

### AddonInfo

Returned by `IOdooAddonService.ListAsync()`.

| Property | Type | Description |
|----------|------|-------------|
| `Name` | `string` | Technical addon name (e.g. `"midcon_plugin_notes"`) |
| `State` | `string` | Installation state (`"installed"` or `"uninstalled"`) |
| `Models` | `string[]` | Technical names of models registered by this addon |

### IOdooRouteInfoService

Service for querying registered Odoo API routes via `api.route.info`.
Requires `OdooJson2` to be configured in `AddMidconPluginHost()`.

**Methods:**
- `GetRoutesAsync(ct)`: Returns all registered routes as `OdooRouteInfo[]`
- `IsRouteAvailableAsync(route, ct)`: Returns `true` if the route exists and its module is installed

### OdooRouteInfo

Returned by `IOdooRouteInfoService.GetRoutesAsync()`.

| Property | Type | Description |
|----------|------|-------------|
| `Name` | `string` | Display name of the route |
| `Route` | `string` | URL path (e.g. `"/api/my-endpoint"`) |
| `Methods` | `string[]` | Supported HTTP methods (e.g. `["POST"]`) |
| `Auth` | `string` | Authentication type (e.g. `"api_key"`, `"none"`) |
| `Module` | `string` | Odoo module that registers this route |
| `Installed` | `bool` | Whether the route's module is currently installed |
| `Source` | `string` | Source identifier of the route registration |

## MCM Manifest: SDK Compatibility

Every MCM plugin package must declare the SDK version it was compiled against in its `manifest.json`.
The MIDCON Backend Server reads this field and rejects incompatible plugins **before** installation,
preventing `MissingMethodException` errors at runtime (which would otherwise only surface when a route is called).

### manifest.json Example

```json
{
  "name": "my_plugin",
  "version": "1.0.0",
  "compatibility": {
    "backendServer": ">=0.3.0",
    "backendSdk": "0.3.0"
  }
}
```

Set `compatibility.backendSdk` to the version of `MIDCON.Backend.SDK.Server` your plugin references.

### Reading the SDK Version at Build Time

Use `MidconSdkVersion.Current` to read the version at runtime — useful in packaging scripts or diagnostic output:

```csharp
Console.WriteLine(MidconSdkVersion.Current); // e.g. "0.3.0"
```

MCM packaging tools can also read the version directly from the `MIDCON.Backend.SDK.Server.dll` assembly version and populate `compatibility.backendSdk` automatically.

### Compatibility Rules

The SDK follows **Semantic Versioning** — `Major.Minor` is the breaking-change boundary:

| Plugin compiled with | Server has SDK | Result |
|----------------------|---------------|--------|
| `0.3.0` | `0.3.x` | ✅ Compatible (patch difference is OK) |
| `0.2.x` | `0.3.0` | ❌ Blocked (minor bump = breaking change) |
| `0.4.0` | `0.3.x` | ❌ Blocked (plugin newer than server) |

## Best Practices

1. **Always use the `[Authorize(Policy = "OdooApiPolicy")]` attribute** on your controllers
2. **Declare all required Odoo modules** in the `RequiredOdooModules` array
3. **Use SemVer** for your plugin version
4. **Handle exceptions gracefully** - the MIDCON Backend Server will catch unhandled exceptions
5. **Keep plugin names lowercase with underscores** (matching Odoo module naming conventions)
6. **Set `compatibility.backendSdk`** in your `manifest.json` to the SDK version you compiled against

## MCM Plugin Install Lifecycle

Understanding the install lifecycle helps avoid common surprises when deploying plugins for the first time.

### Odoo addon `reload` vs. MCM `restart-status`

There are two distinct "restart needed" signals in the lifecycle:

| Signal | Source | What it means |
|--------|--------|---------------|
| `AddonInstallResult.RestartRequired` | `api.installer` (`reload` field) | Odoo needs a server reload to activate the new addon module |
| `GET /gateway/api/admin/plugins/restart-status` | MIDCON Backend Server | The backend server needs restart to load the new plugin DLL |

These are independent. A plugin install may require both restarts, one, or neither depending on the scenario.

### restart-status after Installation

The `GET /gateway/api/admin/plugins/restart-status` endpoint behaves differently depending on whether a plugin is **new** (first install) or an **update** to an already-loaded plugin:

| Scenario | `restartNeeded` | Reason |
|----------|----------------|--------|
| Update of an already-loaded plugin | `true` | Server compares loaded DLL against new DLL — version mismatch detected |
| **New plugin (first install)** | `false` | No loaded plugin metadata exists for comparison — nothing to diff against |

**What this means in practice:**
After installing a brand-new plugin for the first time, `restart-status` returns `restartNeeded: false` even though the DLL was copied to the plugins folder. The install response will include `nextSteps: ["Restart backend server to load plugin DLL"]` — follow that instruction instead of relying on `restart-status` for new plugins.

Plugins loaded via AutoDiscovery at server startup (e.g. `HelloWorldAddon`) behave like updates on reinstall — `restart-status` works correctly for those.

### displayName in Install Response

After `POST /gateway/api/admin/plugins/install`, the `plugin.displayName` field in the response may be empty for a newly installed plugin:

```json
{
  "plugin": {
    "name": "MyCompany.Gateway.Module",
    "displayName": "",
    "version": "1.0.0"
  }
}
```

**Why:** `displayName` is read from `OdooPluginMetadata.DisplayName` via Reflection on the **loaded** assembly. Since a newly installed plugin has not been loaded yet, there is no in-memory metadata to read from — `manifest.json` is not consulted. The correct `displayName` will appear after the server restarts and loads the DLL.

### odoo.controller.filename (optional, default: `"module"`)

In `manifest.json`, the `odoo.controller.filename` field is **optional**. When omitted, the server uses `"module"` as the default, creating `controllers/module.py` in the Odoo addon:

```json
{
  "odoo": {
    "controller": {
      "filename": "my_controller"
    }
  }
}
```

If `filename` is not specified, the generated file will be `controllers/module.py`. Set it explicitly to a descriptive name when creating multiple controllers or when the default would be ambiguous.

## Version Compatibility

| SDK Version | Backend Server Version | .NET Version |
|------------|----------------------|--------------|
| 0.3.0      | ≥0.3.0              | .NET 10.0    |
| 0.2.x      | ≥0.2.0              | .NET 10.0    |

## Documentation

For complete documentation, visit:
- [MIDCON Backend Server Documentation](https://github.com/JDueckMidCon/midcon-backend-sdk)
- [Odoo API Documentation](https://www.odoo.com/documentation/)

## License

MIT License - See LICENSE file for details

## Support

For questions and support:
- GitHub Issues: https://github.com/JDueckMidCon/midcon-backend-sdk/issues
- Email: support@midconsulting.de
