Examples of implementing Monk Entities using the new TypeScript-based approach with MonkEC compiler.
This repository contains examples of Monk entities implemented using TypeScript source code that gets compiled into YAML and JavaScript files. This approach provides:
- Type Safety: Full TypeScript support with interfaces and type checking
- Better Developer Experience: IDE support, autocomplete, and error detection
- Modular Architecture: Reusable base classes and shared utilities
- Testing Framework: Built-in testing capabilities with functional tests
- Compilation Pipeline: Automatic conversion from TypeScript to Monk-compatible YAML/JS
- Module System: Reusable JavaScript modules with TypeScript definitions
- HTTP Client: Built-in HTTP client for API interactions
# Build all default entity packages (monkec, mongodb-atlas, neon, ...)
./build.sh
# Build specific entity packages
./build.sh mongodb-atlas neon
# Load compiled entity package MANIFEST
cd dist/mongodb-atlas/
monk load MANIFEST
# Test with automatic environment loading
sudo INPUT_DIR=./src/mongodb-atlas/ ./monkec.sh test
# Verbose output
sudo INPUT_DIR=./src/mongodb-atlas/ ./monkec.sh test --verbose
# Specific test file
sudo INPUT_DIR=./src/mongodb-atlas/ ./monkec.sh test --test-file test/stack-integration.test.yaml
# Watch mode
sudo INPUT_DIR=./src/mongodb-atlas/ ./monkec.sh test --watch
src/
├── your-entity/
│ ├── base.ts # Base class and common interfaces
│ ├── entity.ts # Main entity implementation
│ ├── common.ts # Shared utilities and constants
│ ├── README.md # Entity documentation
│ └── test/
│ ├── README.md # Testing instructions
│ ├── env.example # Environment variables template
│ ├── stack-template.yaml # Test stack configuration
│ └── stack-integration.test.yaml # Functional test configuration
├── lib/
│ ├── modules/
│ │ ├── base.d.ts # MonkEC base types
│ │ └── http-client.d.ts # HTTP client types
│ └── builtins/ # Built-in module types
└── monkec/ # MonkEC compiler implementation
See also:
doc/new-entity-guide.md
— end-to-end authoring/testing guidedoc/monk-cli.md
— Monk CLI quick referencedoc/templates.md
— Templates, stacks, and secretsdoc/testing.md
— Test framework and patternsdoc/entity-conventions.md
— Standard patterns for consistent entitiesdoc/scaffold.md
— Canonical scaffold for new entitiesdoc/common-issues.md
— Troubleshooting common problems
After creating src/<package>/
:
- Update the build script defaults in
build.sh
to include<package>
in themodules=(...)
list so./build.sh
compiles it by default. - Update the root
MANIFEST
to includedist/<package>
in theDIRS
line somonk load MANIFEST
picks it up after compilation. - Then run:
INPUT_DIR=./src/<package>/ OUTPUT_DIR=./dist/<package>/ ./monkec.sh compile
monk load MANIFEST
(from repo root) orcd dist/<package>/ && monk load MANIFEST
Create a base class that extends MonkEntity
:
// src/your-entity/base.ts
import { MonkEntity } from "monkec/base";
import { HttpClient } from "monkec/http-client";
import cli from "cli";
export interface YourEntityDefinition {
secret_ref: string;
// Add your entity-specific properties
}
export interface YourEntityState {
existing?: boolean;
// Add your entity-specific state
}
export abstract class YourEntity<
D extends YourEntityDefinition,
S extends YourEntityState
> extends MonkEntity<D, S> {
protected apiKey!: string;
protected httpClient!: HttpClient;
protected override before(): void {
// Initialize authentication and HTTP client
this.apiKey = secret.get(this.definition.secret_ref);
if (!this.apiKey) {
throw new Error(`Failed to retrieve API key from secret: ${this.definition.secret_ref}`);
}
this.httpClient = new HttpClient({
baseUrl: "https://api.yourservice.com",
headers: {
"Authorization": `Bearer ${this.apiKey}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
parseJson: true,
stringifyJson: true,
timeout: 10000,
});
}
protected abstract getEntityName(): string;
protected makeRequest(method: string, path: string, body?: any): any {
try {
const response = this.httpClient.request(method as any, path, { body });
if (!response.ok) {
throw new Error(`API error: ${response.statusCode} ${response.status} - ${response.data}`);
}
return response.data;
} catch (error) {
throw new Error(`${method} request to ${path} failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
protected checkResourceExists(path: string): any | null {
try {
return this.makeRequest("GET", path);
} catch (error) {
return null;
}
}
protected deleteResource(path: string, resourceName: string): void {
if (this.state.existing) {
cli.output(`${resourceName} wasn't created by this entity, skipping delete`);
return;
}
try {
this.makeRequest("DELETE", path);
cli.output(`Successfully deleted ${resourceName}`);
} catch (error) {
throw new Error(`Failed to delete ${resourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
Create your specific entity class:
// src/your-entity/entity.ts
import { YourEntity, YourEntityDefinition, YourEntityState } from "./base.ts";
import { action, Args } from "monkec/base";
import cli from "cli";
export interface SpecificEntityDefinition extends YourEntityDefinition {
name: string;
// Add specific properties
}
export interface SpecificEntityState extends YourEntityState {
id?: string;
name?: string;
// Add specific state properties
}
export class SpecificEntity extends YourEntity<SpecificEntityDefinition, SpecificEntityState> {
// Customize readiness check parameters
static readonly readiness = { period: 10, initialDelay: 2, attempts: 20 };
protected getEntityName(): string {
return this.definition.name;
}
override create(): void {
// Check if resource already exists
const existing = this.checkResourceExists(`/resources/${this.definition.name}`);
if (existing) {
this.state = {
id: existing.id,
name: existing.name,
existing: true
};
return;
}
// Create new resource
const body = {
name: this.definition.name,
// Add other properties
};
const created = this.makeRequest("POST", "/resources", body);
this.state = {
id: created.id,
name: created.name,
existing: false
};
}
override update(): void {
if (!this.state.id) {
this.create();
return;
}
// Update logic here
const body = {
name: this.definition.name,
// Add update properties
};
this.makeRequest("PUT", `/resources/${this.state.id}`, body);
}
override delete(): void {
if (!this.state.id) {
cli.output("Resource does not exist, nothing to delete");
return;
}
this.deleteResource(`/resources/${this.state.id}`, "Resource");
}
override checkReadiness(): boolean {
if (!this.state.id) {
return false;
}
try {
const resource = this.makeRequest("GET", `/resources/${this.state.id}`);
return resource.status === "ready";
} catch (error) {
return false;
}
}
// Custom actions using @action decorator
@action("backup")
backup(args?: Args): void {
cli.output(`Backing up resource: ${this.definition.name}`);
const backupResponse = this.makeRequest("POST", `/resources/${this.state.id}/backup`);
cli.output(`Backup created: ${backupResponse.backupId}`);
}
@action("restore")
restore(args?: Args): void {
const backupId = args?.backupId;
if (!backupId) {
throw new Error("backupId argument is required");
}
cli.output(`Restoring resource from backup: ${backupId}`);
this.makeRequest("POST", `/resources/${this.state.id}/restore`, {
backupId: backupId
});
}
}
Create comprehensive tests using the MonkEC testing framework:
# src/your-entity/test/stack-template.yaml
namespace: your-entity-test
test-resource:
defines: your-entity/specific-entity
secret_ref: your-service-token
name: test-resource-123
permitted-secrets:
your-service-token: true
services:
data:
protocol: custom
# src/your-entity/test/stack-integration.test.yaml
name: Your Entity Integration Test
description: Complete integration test for Your Entity
timeout: 300000
secrets:
global:
your-service-token: "$YOUR_SERVICE_TOKEN"
your-dev-password: "dev-secure-password-123"
setup:
- name: Load compiled entity
action: load
target: dist/your-entity/MANIFEST
expect:
exitCode: 0
- name: Load entity template
action: load
target: test/stack-template.yaml
expect:
exitCode: 0
tests:
- name: Create and start entity
action: run
target: your-entity-test/test-resource
expect:
exitCode: 0
output:
- "Started your-entity-test/test-resource"
- name: Wait for entity to be ready
action: wait
target: your-entity-test/test-resource
waitFor:
condition: ready
timeout: 60000
- name: Test custom action
action: action
target: your-entity-test/test-resource
actionName: backup
expect:
exitCode: 0
output:
- "Backing up resource"
- name: Test action with arguments
action: action
target: your-entity-test/test-resource
actionName: restore
args:
backupId: "backup-123"
expect:
exitCode: 0
output:
- "Restoring resource from backup"
cleanup:
- name: Delete entity
action: delete
target: your-entity-test/test-resource
expect:
exitCode: 0
Create environment template with automatic loading:
# src/your-entity/test/env.example
# Required: Your Service API Token
YOUR_SERVICE_TOKEN=your-actual-api-token-here
# Optional: Test configuration
MONKEC_VERBOSE=true
TEST_TIMEOUT=300000
The testing framework automatically loads .env
files from the test directory.
# 1. Make changes to TypeScript source
vim src/your-entity/entity.ts
# 2. Compile the entity
./build.sh your-entity
# 3. Load the compiled entity
monk load dist/your-entity/MANIFEST
# 4. Test the entity
sudo INPUT_DIR=./src/your-entity/ ./monkec.sh test
# 5. Iterate and repeat
- Type Safety: Use TypeScript interfaces for all definitions and state
- Error Handling: Implement comprehensive error handling with try-catch blocks
- Logging: Use
cli.output()
for user-friendly messages - Testing: Write functional tests for all entity operations
- Environment Isolation: Use separate test environments with
.env
files - Resource Cleanup: Always clean up test resources
- Watch Mode: Use
--watch
flag for rapid development iteration
The MonkEC framework provides a powerful HTTP client for API interactions:
import { HttpClient } from "monkec/http-client";
import cli from "cli";
// Create client with configuration
const client = new HttpClient({
baseUrl: "https://api.example.com",
headers: {
Authorization: "Bearer your-token",
"Content-Type": "application/json",
},
timeout: 10000,
parseJson: true,
stringifyJson: true,
});
// Make requests
const response = client.get("/users/1");
if (response.ok) {
cli.output("User: " + JSON.stringify(response.data));
}
// Error handling
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
Create reusable modules for shared functionality:
namespace: my-app
http-client:
defines: module
source: |
function get(url, options = {}) {
const http = require('http');
return http.get(url, options);
}
function post(url, data, options = {}) {
const http = require('http');
return http.post(url, {
body: JSON.stringify(data),
...options
});
}
module.exports = { get, post };
types: |
export interface HttpOptions {
headers?: Record<string, string>;
timeout?: number;
}
export interface HttpResponse {
status: number;
data: any;
headers: Record<string, string>;
}
export function get(url: string, options?: HttpOptions): HttpResponse;
export function post(url: string, data: any, options?: HttpOptions): HttpResponse;
- Location:
src/mongodb-atlas/
- Features: Project, cluster, and user management
- Documentation: See
src/mongodb-atlas/README.md
- Testing: Comprehensive integration tests with stack and multi-instance scenarios
- Location:
src/neon/
- Features: Project, branch, compute, and role management
- Documentation: See
src/neon/README.md
- Testing: Full lifecycle testing with operation waiting
- Location:
src/netlify/
- Features: Site, deployment, and form management
- Documentation: See
src/netlify/README.md
- Testing: Complete integration tests with site, deploy, and form scenarios
- API: Based on Netlify API documentation
- Location:
src/monkec/
- Features: Base classes, HTTP client, and compilation tools
- Documentation: See
src/monkec/base.ts
andsrc/monkec/http-client.ts
- This repo uses TypeScript entities compiled by MonkEC; see
doc/arrowscript.md
for how ArrowScript relates and when to use it.
-
Compilation Errors
- Check TypeScript syntax and imports
- Verify all required interfaces are defined
- Ensure proper module paths in
tsconfig.json
-
Runtime Errors
- Verify API credentials and permissions
- Check network connectivity to external APIs
- Review entity state and definition validation
-
Test Failures
- Ensure environment variables are set correctly in
.env
file - Check API rate limits and quotas
- Verify test resources are properly cleaned up
- Use
--verbose
flag for detailed debugging
- Ensure environment variables are set correctly in
-
HTTP Client Issues
- Check response.ok before using data
- Verify base URL and headers configuration
- Set appropriate timeouts for your use case
# Enable verbose compilation
./build.sh your-entity
# Enable verbose testing
sudo MONKEC_VERBOSE=true INPUT_DIR=./src/your-entity/ ./monkec.sh test --verbose
# Check entity state
monk describe your-namespace/your-entity
# Watch mode for development
sudo INPUT_DIR=./src/your-entity/ ./monkec.sh test --watch
When contributing new entities:
- Follow the established project structure
- Implement comprehensive TypeScript interfaces
- Add functional tests with proper cleanup
- Document all features and usage examples
- Update this README with new entity information
- Use the module system for reusable functionality
- Implement proper error handling and logging
- Add readiness checks with appropriate timeouts
Use this minimal prompt in Cursor when adding a new entity package from external docs:
Build a new MonkEC entity package using the linked API docs and this repo’s conventions.
Inputs:
- Documentation URL(s)
- Entity package name
- Credential env var(s) (names and values)
References: doc/entity-conventions.md, doc/scaffold.md, doc/templates.md, doc/testing.md, doc/monkec.md, doc/monk-cli.md
Deliverables:
- Code in src/<package>/ following conventions (snake_case Definition/State, kebab-case actions, optional secret_ref with provider default, reserved-name avoidance)
- Tests in src/<package>/test (stack-template.yaml with depends/connections and a data service for providers, stack-integration.test.yaml, env.example; create .env if credentials provided)
- example.yaml and package README
Process:
- Compile: `INPUT_DIR=./src/<package>/ OUTPUT_DIR=./dist/<package>/ ./monkec.sh compile`
- Test: `sudo INPUT_DIR=./src/<package>/ ./monkec.sh test --verbose`
- In tests, MANIFEST path inside container is `dist/input/<package>/MANIFEST`
- Use `existing` state flag to mark resources discovered vs created; readiness should reflect existing resources appropriately
- Use `depends` + `connections` with `connection-target("service") entity-state get-member("field")`
- Map .env vars to provider default secret name(s) via test `secrets`
- Minimal follow-ups only if docs are ambiguous; otherwise iterate via tests
Output only the changed/added files with complete contents.
Example for Cloudflare:
Build a new MonkEC entity package using the linked API docs and this repo’s conventions.
Inputs:
- Documentation URL(s): https://developers.cloudflare.com/api/ (DNS management)
- Entity package name: cloudflare
- Credential env var(s) (names and values): api token <TOKEN>
...
See source code in subfolders and README.md for usage.
Use monk load MANIFEST
to load all entity types at once.
You can see example.yaml in subfolders for example definitions.