API Exposure Guide

Asset & Parameter Management

Mutate the data layer via Auto-CRUD, Custom Contracts, and Raw File APIs.

Asset & Parameter Management

Rescile provides three ways to mutate your architecture’s data layer via API: simple record updates, intent-based orchestration, and raw bulk operations.

1. Automatic Asset Mutations (Auto-CRUD)

When to use: For standard UI forms, simple integrations, and single-record updates where you need strict type-safety and validation.

When Rescile detects a known asset type (a .csv file exists or its schema is defined in a module.toml), it automatically generates mutation endpoints. These endpoints replace raw CSV manipulation with strict JSON/GraphQL type-safety, enforcing mandatory fields, expected data types, and uniqueness constraints.

Example: Adding a New VM

Before the mutation:

# data/vm.csv
name,tenant,cores
app-server-01,marketing,8
db-server-01,finance,16

GraphQL Interface

Rescile auto-generates GraphQL input objects based on the asset’s schema and extends the Mutation type with intent-specific operations: Add<Type>, Change<Type>, and Delete<Type>.

mutation CreateNewVM {
  AddVm(input: {
    name: "web-server-01",
    tenant: "finance",
    cores: 4
  }) {
    success
    message
  }
}

REST Equivalent:

POST /api/mutations/vm
Content-Type: application/json

{
  "name": "web-server-01",
  "tenant": "finance",
  "cores": 4
}

After the mutation:

# data/vm.csv (new row added)
name,tenant,cores
app-server-01,marketing,8
db-server-01,finance,16
web-server-01,finance,4

REST Interface

Dynamic JSON endpoints are automatically mounted for Auto-CRUD operations.

  • POST /api/mutations/:asset (ADD): Inserts a single JSON record. Returns 409 Conflict if the primary key already exists.
  • PUT /api/mutations/:asset/:name (CHANGE): Merges the provided JSON payload with the existing record. Returns 404 Not Found if the item doesn’t exist.
  • DELETE /api/mutations/:asset/:name (DELETE): Deletes the matching record.

Note: The mutation endpoints automatically validate that all required fields are present and that data types match the CSV schema. For example, attempting to add a VM without a name field would return a validation error.

2. Custom API Contracts

When to use: When you need to expose intent-based operations that mutate multiple underlying assets simultaneously safely and atomically.

Instead of requiring external consumers to understand your internal CSV schema and graph relationships, define Custom API Contracts. These contracts provide strict validation boundaries and atomic execution, encapsulating complex intents (like ProvisionVM or DecommissionService) into simple, high-level API endpoints.

Contract Definition

Contracts are defined as .toml files in your module’s contracts/ directory. A contract parses incoming JSON payloads against an [[input]] schema and applies mutations via sequential [[write]] blocks.

Under the hood, mutations leverage the Tera templating engine to dynamically map inputs to target records.

Example: ProvisionVM Contract

# modules/my_module/contracts/provision_vm.toml
name = "ProvisionVM"
description = "Provisions a VM and allocates an IP simultaneously."

# Input Validation Schema
[[input]]
name = "vm_name"
type = "string"
required = true

# Accept arbitrary JSON payloads without strict structural validation
[[input]]
name = "metadata"
type = "json" # "object", "json", or "any" map to GraphQL JSON

[[input]]
name = "environment"
type = "string"
required = false
allowed_values = ["dev", "prod"]

# Atomic Action 1: Add VM to the desired state asset
[[write]]
target_asset = "desired_vms"
action = "add" 
record = { name = "{{ input.vm_name }}", status = "planned", env = "{{ input.environment | default(value='dev') }}" }

# Atomic Action 2: Allocate IP (upsert example)
[[write]]
target_asset = "ip_allocations"
action = "upsert"
# Matches existing rows by 'assigned_device'. If missing, creates it.
lookup_by = { assigned_device = "{{ input.vm_name }}" }
record = { name = "ip-{{ input.vm_name }}", status = "allocated" }

What this contract does:

  1. Input Validation: Accepts a VM name (required), optional metadata as JSON, and an environment (defaults to “dev” if not provided).
  2. First Write Operation: Adds a new VM record to the desired_vms.csv asset with status “planned”.
  3. Second Write Operation: Allocates an IP address in ip_allocations.csv by either updating an existing record or creating a new one (upsert).

Target CSV structure:

The desired_vms.csv file tracks the desired state of VMs in your infrastructure:

name,status,env
web-server-01,active,prod
api-gateway-01,active,prod

The ip_allocations.csv file tracks IP address assignments:

name,status,assigned_device
ip-web-server-01,allocated,web-server-01
ip-api-gateway-01,allocated,api-gateway-01

Execution Lifecycle and Atomicity

To guarantee safe and idempotent state mutations, the Rescile controller processes contracts with transactional guarantees:

  1. Validation: The JSON payload is rigorously validated against the defined [[input]] blocks.
  2. Lock Acquisition: A global asynchronous lock (ASSET_WRITE_LOCK) is acquired to prevent race conditions from concurrent API calls.
  3. In-Memory Transaction: The engine evaluates all [[write]] instructions sequentially against an in-memory representation of the CSV assets.
  4. Pre-condition Guards: match_on arrays act as conditional guards. Evaluated against the input payload, they determine if a specific [[write]] block should execute.
  5. Rollback on Error: If any write block fails (e.g., attempting to add a primary key that already exists, or failing to find a record during a change action), the entire execution aborts instantly. No partial data is written to disk.
  6. Flush & Rebuild: If all mutations succeed, the updated CSV assets are flushed to disk, and a background graph rebuild is triggered automatically to reflect the new state.

Example: Atomicity in Action

Consider the ProvisionVM contract from earlier. If you try to provision a VM that already exists:

mutation Provision {
  ProvisionVM(input: { vm_name: "web-server-01", environment: "prod" }) {
    success
    message
    affected_assets
  }
}

What happens:

  1. The add action for desired_vms fails because web-server-01 already exists.
  2. The entire contract execution is rolled back immediately.
  3. No changes are written to desired_vms.csv or ip_allocations.csv.
  4. The API returns an error message indicating the conflict.

This ensures that your infrastructure state remains consistent even when operations fail.

Modifying Assets: [[write]] Blocks

The [[write]] block is where state mutations happen. You specify the target_asset (the name of the CSV file, without the .csv extension), the action, and the payload.

Available actions:

  • add: Inserts a new row. Fails if the primary key (name) already exists.
  • change: Updates an existing row. Fails if the target row cannot be found.
  • delete: Removes a row. Silently skips if the row does not exist, ensuring idempotency.
  • upsert: Updates the row if it matches criteria, or inserts a new row if it does not.

You can also use lookup_by for multi-column targeting, match_on for precondition guards, and loop_over for dynamic iteration of input arrays.

Consuming Contracts

Once defined in your module, the Rescile controller automatically parses your Contracts and dynamically injects them into both the REST and GraphQL APIs.

Example: Provisioning a VM

Let’s walk through what happens when you execute the ProvisionVM contract shown above.

Before the contract execution:

# data/desired_vms.csv
name,status,env
web-server-01,active,prod
api-gateway-01,active,prod
# data/ip_allocations.csv
name,status,assigned_device
ip-web-server-01,allocated,web-server-01
ip-api-gateway-01,allocated,api-gateway-01

GraphQL API Contracts are exposed as root mutations in the GraphQL schema, strictly typed according to your [[input]] definitions.

mutation Provision {
  ProvisionVM(input: { vm_name: "db-node-01", environment: "prod" }) {
    success
    message
    affected_assets
  }
}

REST API Contracts can be executed directly via HTTP POST, passing the payload as standard JSON.

Endpoint: POST /api/contracts/:contract_name

POST /api/contracts/ProvisionVM
Content-Type: application/json

{
  "vm_name": "db-node-01",
  "environment": "prod"
}

After the contract execution:

# data/desired_vms.csv (new row added)
name,status,env
web-server-01,active,prod
api-gateway-01,active,prod
db-node-01,planned,prod
# data/ip_allocations.csv (new row added via upsert)
name,status,assigned_device
ip-web-server-01,allocated,web-server-01
ip-api-gateway-01,allocated,api-gateway-01
ip-db-node-01,allocated,db-node-01

The contract atomically added a new VM record to desired_vms and allocated a corresponding IP address in ip_allocations. If either operation had failed, both changes would have been rolled back, ensuring data consistency.

3. Raw Asset & Parameter Management

When to use: For full programmatic control over the raw data layer, bulk uploads, generic table editor web apps, or global variable updates.

Raw Asset Files

For applications requiring bulk uploads or raw CSV data manipulation, Rescile exposes a direct REST API under /api/assets.

  • GET /api/assets: Returns a JSON array of available .csv asset files.
  • GET /api/assets/{filename}: Returns the raw text content of the CSV.
  • POST/PUT /api/assets/{filename}: Mutates the asset using multipart/form-data. Accepts a data field replacing the entire content of the CSV file (Full Overwrite).

Example: Uploading a New CSV File

Before the upload:

# Check existing assets
curl http://localhost:7600/api/assets
# Returns: ["vm.csv", "application.csv"]

Upload a new asset file:

curl -X POST http://localhost:7600/api/assets/databases.csv \
  -F "data=name,engine,version
postgres-prod,postgresql,14.2
mysql-staging,mysql,8.0"

After the upload:

# Check updated assets list
curl http://localhost:7600/api/assets
# Returns: ["vm.csv", "application.csv", "databases.csv"]

# Verify the new content
curl http://localhost:7600/api/assets/databases.csv
# Returns the raw CSV content

Important: The raw asset API performs a full overwrite. Any existing data in the file will be replaced entirely.

Module Parameters

Global variables used across models, compliance, and outputs can be read and mutated:

  • GET /api/module-params: Returns current parameters and allowed values.
  • POST/PUT /api/module-params: Accepts a JSON key-value object to update parameters, instantly triggering a graph rebuild.

Example: Updating Module Parameters

Before the update:

# Get current parameters
curl http://localhost:7600/api/module-params
# Returns:
# {
#   "environment": "staging",
#   "region": "us-west-2",
#   "debug_mode": false
# }

Update parameters:

curl -X POST http://localhost:7600/api/module-params \
  -H "Content-Type: application/json" \
  -d '{"environment": "production", "region": "eu-west-1"}'

After the update:

# Verify updated parameters
curl http://localhost:7600/api/module-params
# Returns:
# {
#   "environment": "production",
#   "region": "eu-west-1",
#   "debug_mode": false
# }

Note: Changing module parameters automatically triggers a graph rebuild to reflect the new configuration across your infrastructure.