# Advanced Topics This page covers advanced features and implementation details of the DB Query Operator. ## Change Detection Change detection optimizes database polling by only executing the main query when data has actually changed. ### How It Works ### Implementation The operator stores the last seen timestamp in memory and compares it on each poll: 1. **First Poll**: No timestamp cached, executes main query 2. **Subsequent Polls**: * Queries `SELECT MAX(timestamp_column) FROM table_name` * Compares with cached value * Only runs main query if timestamp increased ### Configuration ```yaml spec: changeDetection: enabled: true tableName: "my_table" timestampColumn: "updated_at" ``` ### Database Requirements Your table must have a timestamp column that updates whenever data changes: ```sql CREATE TABLE my_table ( id TEXT PRIMARY KEY, data JSONB, updated_at TIMESTAMP DEFAULT NOW() ); -- Option 1: Trigger-based update CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER my_table_timestamp BEFORE UPDATE ON my_table FOR EACH ROW EXECUTE FUNCTION update_timestamp(); -- Option 2: Application-managed UPDATE my_table SET data = $1, updated_at = NOW() WHERE id = $2; ``` ### Performance Impact **Without change detection** (pollInterval = 30s): * 120 queries/hour to database * Full table scan every 30s * Continuous resource reconciliation **With change detection** (pollInterval = 30s): * 120 lightweight timestamp checks/hour * Main query only when data changes * Reconciliation only when needed **Example**: For a table updated 5 times/hour: * Reduces main queries from 120 to 5 (96% reduction) * Minimal overhead from timestamp checks ### Limitations * Only detects row updates/inserts, not deletes (use `prune: true` for delete handling) * Requires timestamp column maintained by triggers or application * Single table only (no joins in change detection query) ### Best Practices * Use change detection for tables with infrequent updates * Set aggressive `pollInterval` (e.g., 30s) with change detection enabled * Ensure timestamp column is indexed for performance * Use database triggers for automatic timestamp updates *** ## Connection Pooling The operator reuses database connections across reconciliation cycles. ### Implementation Details * Single `*sql.DB` connection pool per `DatabaseQueryResource` * Connection established on first reconciliation * Reused for all subsequent polls * Closed when `DatabaseQueryResource` is deleted ### Connection Configuration Currently uses `pgx` defaults: * Max open connections: Unlimited * Max idle connections: 2 * Connection max lifetime: Unlimited * Connection max idle time: Unlimited ### Future Enhancements Connection pool configuration via CRD: ```yaml # Future feature spec: database: connectionPool: maxOpenConns: 10 maxIdleConns: 5 connMaxLifetime: "1h" connMaxIdleTime: "5m" ``` *** ## Multi-Statement Query Execution The operator automatically handles multi-statement queries using `pgx.Batch`. ### Detection Logic ```go func splitStatements(sql string) []string { // Splits on semicolons outside of: // - Single quotes: 'text;text' // - Double quotes: "text;text" // - Dollar quotes: $$text;text$$ // - Tagged dollar quotes: $tag$text;text$tag$ } ``` ### Execution Strategy When multiple statements detected: 1. Parse query into individual statements 2. Create `pgx.Batch` 3. Execute all but last statement (setup commands) 4. Execute last statement and return results ### Use Cases #### Setting Session Parameters ```sql SET statement_timeout = '5s'; SET lock_timeout = '3s'; SELECT * FROM large_table WHERE indexed_column = 'value'; ``` The operator automatically: 1. Sets statement timeout 2. Sets lock timeout 3. Executes main query 4. Returns results from step 3 #### Using PostgreSQL Extensions ```sql CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; SET search_path = public, extensions; SELECT uuid_generate_v4() as id, name FROM users; ``` Setup commands executed once, main query returns data. ### Limitations * All statements except the last must not return data * Cannot use transactions (each statement auto-commits) * Error in setup statement aborts entire batch *** ## Resource Ownership and Finalizers The operator uses Kubernetes ownership and finalizers for resource lifecycle management. ### Owner References Every created resource has an owner reference pointing to its `DatabaseQueryResource`: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: generated-config ownerReferences: - apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource name: my-dbqr uid: abc-123 controller: true blockOwnerDeletion: true ``` ### Implications * Deleting a `DatabaseQueryResource` automatically deletes managed resources * Owner references work only for resources in the same namespace * Cross-namespace or cluster-scoped resources require manual cleanup ### Finalizers The operator adds a finalizer to track managed resources: ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: my-dbqr finalizers: - konnektr.io/dbqr-finalizer ``` **Deletion Process**: 1. User deletes `DatabaseQueryResource` 2. Kubernetes marks resource for deletion (`deletionTimestamp` set) 3. Operator finalizer prevents immediate deletion 4. Operator cleans up managed resources 5. Operator removes finalizer 6. Kubernetes completes deletion ### Cross-Namespace Resources For resources in different namespaces, the operator: * Cannot set owner references (Kubernetes limitation) * Tracks resources via labels/annotations * Manually deletes on DBQR deletion via finalizer logic ### Cluster-Scoped Resources Similar to cross-namespace resources: * No owner references possible * Manual tracking and cleanup * Requires cluster-level RBAC permissions *** ## Server-Side Apply The operator uses Kubernetes Server-Side Apply for resource management. ### Benefits * **Conflict Resolution**: Multiple controllers can manage same resource * **Field Ownership**: Operator owns only fields it sets * **Partial Updates**: Only specified fields are updated * **Drift Detection**: Kubernetes tracks who owns which fields ### Field Manager The operator identifies itself as field manager: ```yaml metadata: managedFields: - manager: db-query-operator operation: Apply apiVersion: v1 fields: f:data: f:config.json: {} ``` ### Implications * Manual `kubectl edit` changes to managed fields are reverted * Other controllers can manage non-conflicting fields * Operator updates are atomic and conflict-free ### Example: Shared ConfigMap ```yaml # Operator manages data section apiVersion: v1 kind: ConfigMap metadata: name: shared-config labels: managed-by: db-query-operator # Operator field team: platform # Manual field (preserved) data: config.json: "{...}" # Operator field manual-key: "manual-value" # Manual field (preserved) ``` *** ## Status Updates and Two-Way Sync The `statusUpdateQuery` enables bidirectional synchronization between Kubernetes and the database. ### Architecture ### Use Cases #### Track Deployment Readiness ```yaml spec: statusUpdateQuery: | UPDATE deployments SET ready_replicas = {{ .Status.readyReplicas }}, available_replicas = {{ .Status.availableReplicas }}, last_check = NOW() WHERE name = {{ .Metadata.Name | squote }} ``` #### Track ArgoCD Application Sync Status ```yaml spec: gvk: group: "argoproj.io" version: "v1alpha1" kind: "Application" statusUpdateQuery: | UPDATE applications SET sync_status = {{ .Status.sync.status | squote }}, health_status = {{ .Status.health.status | squote }}, last_synced = {{ .Status.operationState.finishedAt | squote }} WHERE app_name = {{ .Metadata.Name | squote }} ``` ### Available Template Data In `statusUpdateQuery`, you have access to: * `.Metadata`: Resource metadata (name, namespace, labels, annotations) * `.Status`: Complete resource status object * `.Spec`: Resource spec (for reading values) ### Template Functions * `squote`: Single-quote SQL strings safely * `toJson`: Convert object to JSON string * All Sprig functions ### Error Handling * Status update failures are logged but don't block reconciliation * Resource is still created/updated even if status update fails * Useful for read-only database scenarios *** ## Performance Tuning ### Poll Interval Selection **Aggressive (30s-1m)**: * ✅ Use with change detection enabled * ✅ Real-time requirements * ❌ Without change detection (high DB load) **Moderate (2m-5m)**: * ✅ Most production workloads * ✅ Balance between freshness and load * ✅ Default recommendation **Conservative (10m-1h)**: * ✅ Infrequently changing data * ✅ Large result sets * ❌ Time-sensitive applications ### Query Optimization ```sql -- Bad: Full table scan SELECT * FROM resources; -- Good: Indexed filter SELECT * FROM resources WHERE enabled = true AND updated_at > NOW() - INTERVAL '24 hours'; -- Better: Materialized view CREATE MATERIALIZED VIEW active_resources AS SELECT * FROM resources WHERE enabled = true; CREATE INDEX ON active_resources(updated_at); -- Query the view SELECT * FROM active_resources; ``` ### Resource Template Optimization **Minimize Template Complexity**: ```yaml # Complex (slower rendering) resourceTemplate: | {{ range $i, $item := .Row.items | fromJson }} {{ if gt $item.value 100 }} ... {{ end }} {{ end }} # Simple (faster rendering) resourceTemplate: | data: items: {{ .Row.items }} ``` **Pre-compute in Database**: ```sql -- Move logic to database SELECT name, CASE WHEN value > 100 THEN 'high' ELSE 'low' END as tier FROM resources ``` *** ## Security Considerations ### Database Credentials * Store in Kubernetes Secrets * Use namespace-local secrets when possible * Rotate credentials regularly * Use read-only database users where appropriate ### RBAC Permissions The operator needs: ```yaml # Same-namespace resources - apiGroups: [""] resources: ["configmaps", "secrets"] verbs: ["get", "list", "create", "update", "patch", "delete"] # Cross-namespace resources - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "create", "update", "patch", "delete"] resourceNames: [] # Or specific names # Cluster-scoped - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "list", "create", "update", "patch", "delete"] ``` ### SQL Injection Prevention The operator does NOT sanitize SQL queries. Ensure: * Database users have minimal privileges * Queries are written by trusted operators * Use parameterized queries where possible (in `statusUpdateQuery` templates) *** ## Monitoring and Observability ### Logs The operator logs: * Reconciliation cycles * Query execution times * Resource creation/update/delete operations * Errors and warnings ```bash kubectl logs -n dbqo-system deployment/db-query-operator -f ``` ### Metrics **Future feature**: Prometheus metrics ``` dbqr_reconciliation_duration_seconds dbqr_query_duration_seconds dbqr_resources_managed_total dbqr_errors_total ``` ### Status Conditions **Future feature**: Status conditions on DBQR ```yaml status: conditions: - type: Ready status: "True" lastTransitionTime: "2024-01-15T10:00:00Z" - type: DatabaseConnected status: "True" observedGeneration: 5 lastReconcile: "2024-01-15T10:30:00Z" managedResources: 15 ``` *** ## Next Steps * Review [Examples](/docs/db-query-operator/examples) for practical implementations * Check [Troubleshooting](/docs/db-query-operator/troubleshooting) for common issues * See [API Reference](/docs/db-query-operator/api-reference) for complete CRD documentation # API Reference Complete API documentation for the `DatabaseQueryResource` Custom Resource Definition. ## Resource Definition ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: namespace: spec: # ... see sections below ``` *** ## Spec Fields ### pollInterval **Type**: `string`\ **Required**: Yes\ **Format**: Go duration (`"30s"`, `"5m"`, `"1h"`) How frequently to query the database and reconcile resources. ```yaml spec: pollInterval: "2m" ``` *** ### prune **Type**: `boolean`\ **Required**: No\ **Default**: `true` Whether to delete managed resources that no longer appear in query results. ```yaml spec: prune: true ``` **Behavior**: * `true`: Resources removed from database are deleted from Kubernetes * `false`: Resources persist even after database rows are deleted *** ### database **Type**: `object`\ **Required**: Yes Database connection configuration. #### database.type **Type**: `string`\ **Required**: Yes\ **Allowed Values**: `postgres` Database type. ```yaml spec: database: type: postgres ``` #### database.connectionSecretRef **Type**: `object`\ **Required**: Yes Reference to a Secret containing database credentials. ##### name **Type**: `string`\ **Required**: Yes Name of the Secret. ```yaml connectionSecretRef: name: db-credentials ``` ##### namespace **Type**: `string`\ **Required**: No\ **Default**: DatabaseQueryResource's namespace Namespace of the Secret. ```yaml connectionSecretRef: name: db-credentials namespace: database-ns ``` ##### uriKey **Type**: `string`\ **Required**: No\ **Default**: Not used Secret key containing a full PostgreSQL URI. ```yaml connectionSecretRef: name: cnpg-cluster-app uriKey: fqdn-uri # CloudNativePG provides this ``` **Format**: `postgresql://user:password@host:port/database?sslmode=require` **Note**: If `uriKey` is specified, individual connection fields (host, port, etc.) are ignored. ##### hostKey **Type**: `string`\ **Required**: No\ **Default**: `"host"` Secret key containing database hostname. ```yaml connectionSecretRef: hostKey: pghost ``` ##### portKey **Type**: `string`\ **Required**: No\ **Default**: `"port"` Secret key containing database port. ```yaml connectionSecretRef: portKey: pgport ``` ##### usernameKey **Type**: `string`\ **Required**: No\ **Default**: `"username"` Secret key containing database username. ```yaml connectionSecretRef: usernameKey: user ``` ##### passwordKey **Type**: `string`\ **Required**: No\ **Default**: `"password"` Secret key containing database password. ```yaml connectionSecretRef: passwordKey: pass ``` ##### databaseKey **Type**: `string`\ **Required**: No\ **Default**: `"database"` Secret key containing database name. ```yaml connectionSecretRef: databaseKey: dbname ``` *** ### query **Type**: `string` (multi-line)\ **Required**: Yes SQL query to execute. Must return at least one row. Each row becomes one Kubernetes resource. ```yaml spec: query: | SELECT id, name, config FROM my_table WHERE enabled = true ``` **Multi-Statement Support** (v0.6.0+): ```yaml spec: query: | SET statement_timeout = '30s'; SET search_path = myschema, public; SELECT id, name, data FROM my_table WHERE enabled = true; ``` The operator executes all statements, returning results from the last one. *** ### gvk **Type**: `object`\ **Required**: Yes GroupVersionKind of resources to create. #### group **Type**: `string`\ **Required**: Yes\ **Default**: `""` for core resources API group. ```yaml gvk: group: "" # Core resources (ConfigMap, Secret, Service) ``` ```yaml gvk: group: "apps" # apps/v1 resources ``` ```yaml gvk: group: "argoproj.io" # Custom resources ``` #### version **Type**: `string`\ **Required**: Yes API version. ```yaml gvk: version: "v1" ``` #### kind **Type**: `string`\ **Required**: Yes Resource kind. ```yaml gvk: kind: "ConfigMap" ``` ```yaml gvk: kind: "Deployment" ``` **Important**: The operator must have RBAC permissions for the specified GVK. Configure via `gvkPattern` during Helm installation. *** ### resourceTemplate **Type**: `string` (multi-line Go template)\ **Required**: Yes Go template for rendering Kubernetes resources from query rows. ```yaml spec: resourceTemplate: | apiVersion: v1 kind: ConfigMap metadata: name: {{ .Row.id }} namespace: {{ .Metadata.Namespace }} data: value: {{ .Row.value | quote }} ``` **Template Context**: * `.Row`: Map of query result columns for current row * `.Metadata`: DatabaseQueryResource metadata **Template Functions**: All [Sprig functions](http://masterminds.github.io/sprig/) plus: * `toJson`: Convert to JSON string * `fromJson`: Parse JSON string * `toYaml`: Convert to YAML string * `fromYaml`: Parse YAML string *** ### statusUpdateQuery **Type**: `string` (multi-line SQL)\ **Required**: No SQL query to execute after managing each resource, typically to update database with resource status. ```yaml spec: statusUpdateQuery: | UPDATE my_table SET ready_replicas = {{ .Status.readyReplicas | default 0 }}, last_updated = NOW() WHERE id = {{ .Metadata.Name | squote }} ``` **Template Context**: * `.Metadata`: Managed resource metadata (name, namespace, labels, annotations) * `.Status`: Managed resource status * `.Spec`: Managed resource spec **Template Functions**: * `squote`: SQL single-quote escape * All Sprig functions **Error Handling**: Failures are logged but don't prevent resource creation. *** ### changeDetection **Type**: `object`\ **Required**: No Configuration for change detection optimization. #### enabled **Type**: `boolean`\ **Required**: No\ **Default**: `false` Enable change detection. ```yaml spec: changeDetection: enabled: true ``` #### tableName **Type**: `string`\ **Required**: If `enabled: true` Table name to query for changes. ```yaml spec: changeDetection: tableName: "my_table" ``` #### timestampColumn **Type**: `string`\ **Required**: If `enabled: true` Column name containing timestamp of last change. ```yaml spec: changeDetection: timestampColumn: "updated_at" ``` **How It Works**: 1. On each poll, query `SELECT MAX(timestampColumn) FROM tableName` 2. If timestamp unchanged since last poll, skip main query 3. If timestamp changed, execute main query and reconcile **Requirements**: * Table must have a timestamp column * Column must update whenever data changes (use triggers) * Column must be indexed for performance *** ## Complete Example ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: tenant-deployments namespace: platform labels: app.kubernetes.io/name: tenant-deployments app.kubernetes.io/part-of: multi-tenant-platform spec: # Poll every 2 minutes pollInterval: "2m" # Delete resources when rows are removed prune: true # Database connection database: type: postgres connectionSecretRef: name: platform-postgres-app namespace: databases uriKey: fqdn-uri # CloudNativePG secret # Change detection for efficiency changeDetection: enabled: true tableName: "tenants" timestampColumn: "updated_at" # Query for active tenants query: | SELECT tenant_id, app_image, replicas, env_vars::text as env_json FROM tenants WHERE enabled = true ORDER BY tenant_id # Create Deployments gvk: group: "apps" version: "v1" kind: "Deployment" # Deployment template resourceTemplate: | apiVersion: apps/v1 kind: Deployment metadata: name: tenant-{{ .Row.tenant_id }} namespace: {{ .Metadata.Namespace }} labels: tenant: {{ .Row.tenant_id | quote }} managed-by: db-query-operator spec: replicas: {{ .Row.replicas }} selector: matchLabels: app: tenant-{{ .Row.tenant_id }} template: metadata: labels: app: tenant-{{ .Row.tenant_id }} tenant: {{ .Row.tenant_id | quote }} spec: containers: - name: app image: {{ .Row.app_image | quote }} ports: - containerPort: 8080 env: {{- range $key, $value := (.Row.env_json | fromJson) }} - name: {{ $key }} value: {{ $value | quote }} {{- end }} # Sync status back to database statusUpdateQuery: | UPDATE tenants SET ready_replicas = {{ .Status.readyReplicas | default 0 }}, available_replicas = {{ .Status.availableReplicas | default 0 }}, last_synced = NOW() WHERE tenant_id = {{ .Metadata.Name | replace "tenant-" "" | squote }} ``` *** ## Status Subresource **Note**: Status subresource is not currently implemented but planned for future releases. **Planned Fields**: ```yaml status: conditions: - type: Ready status: "True" lastTransitionTime: "2024-01-15T10:00:00Z" reason: ReconcileSuccess message: "Successfully reconciled 15 resources" - type: DatabaseConnected status: "True" lastTransitionTime: "2024-01-15T09:00:00Z" observedGeneration: 5 lastReconcileTime: "2024-01-15T10:30:00Z" managedResourceCount: 15 lastQueryDuration: "142ms" ``` *** ## RBAC Requirements The operator ServiceAccount must have permissions for managed resource types. **Example ClusterRole**: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: db-query-operator rules: # DatabaseQueryResource CRD - apiGroups: ["konnektr.io"] resources: ["databasequeryresources"] verbs: ["get", "list", "watch"] # Managed resources (configured via gvkPattern) - apiGroups: [""] resources: ["configmaps", "secrets", "services"] verbs: ["get", "list", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["deployments", "statefulsets"] verbs: ["get", "list", "create", "update", "patch", "delete"] - apiGroups: ["argoproj.io"] resources: ["applications"] verbs: ["get", "list", "create", "update", "patch", "delete"] ``` **Configure during installation**: ```bash helm install db-query-operator ... \ --set gvkPattern="v1/ConfigMap;apps/v1/Deployment;argoproj.io/v1alpha1/Application" ``` *** ## Validation Rules The CRD includes validation rules: * `pollInterval`: Must be valid Go duration format * `database.type`: Must be `"postgres"` * `gvk`: All fields required * `query`: Must not be empty * `resourceTemplate`: Must not be empty * `changeDetection`: If `enabled: true`, `tableName` and `timestampColumn` required *** ## Next Steps * See [Examples](/docs/db-query-operator/examples) for practical use cases * Review [Configuration Reference](/docs/db-query-operator/configuration) for detailed field descriptions * Check [Troubleshooting](/docs/db-query-operator/troubleshooting) for common issues # Configuration Reference Complete reference for all fields in the `DatabaseQueryResource` Custom Resource Definition. ## CRD Specification ### spec.pollInterval **Type**: `string` (duration)\ **Required**: Yes\ **Format**: Go duration string (e.g., `"30s"`, `"5m"`, `"1h"`) How often to query the database and reconcile resources. ```yaml spec: pollInterval: "1m" # Query every minute ``` **Examples**: * `"30s"` - Every 30 seconds * `"5m"` - Every 5 minutes * `"1h"` - Every hour * `"2h30m"` - Every 2 hours and 30 minutes *** ### `spec.prune` **Type**: `boolean`\ **Required**: No\ **Default**: `true` Whether to delete resources that were previously managed but no longer appear in query results. ```yaml spec: prune: true # Delete stale resources ``` **When to disable**: * Resources should persist after database rows are removed * External processes manage resource lifecycle * Manual cleanup is preferred *** ### `spec.database` **Type**: `object`\ **Required**: Yes Database connection configuration. #### `spec.database.type` **Type**: `string`\ **Required**: Yes\ **Allowed values**: `postgres` Type of database to connect to. ```yaml spec: database: type: postgres ``` **Future support**: MySQL, SQLite, other databases may be added. #### spec.databaseConnectionSecretRef **Type**: `object`\ **Required**: Yes Reference to a Kubernetes Secret containing database credentials. ##### `connectionSecretRef.name` **Type**: `string`\ **Required**: Yes Name of the Secret containing connection details. ```yaml connectionSecretRef: name: db-credentials ``` ##### `connectionSecretRef.namespace` **Type**: `string`\ **Required**: No\ **Default**: DatabaseQueryResource's namespace Namespace where the Secret is located. ```yaml connectionSecretRef: name: db-credentials namespace: database-secrets ``` ##### `connectionSecretRef.uriKey` **Type**: `string`\ **Required**: No Key in the Secret containing a complete PostgreSQL connection URI. **Format**: `postgresql://username:password@host:port/dbname?sslmode=...` ```yaml connectionSecretRef: name: my-postgres-cluster-app uriKey: fqdn-uri # CloudNativePG field ``` **When provided**: Takes precedence over individual field keys. **Use case**: Perfect for CloudNativePG-generated secrets which include `fqdn-uri`. #### hostKey **Type**: `string`\ **Required**: No\ **Default**: `"host"` Key in the Secret for the database hostname. ```yaml connectionSecretRef: hostKey: DB_HOST # Secret key name ``` ##### `connectionSecretRef.portKey` **Type**: `string`\ **Required**: No\ **Default**: `"port"` Key in the Secret for the database port. ##### `connectionSecretRef.userKey` **Type**: `string`\ **Required**: No\ **Default**: `"username"` Key in the Secret for the database username. #### passwordKey **Type**: `string`\ **Required**: No\ **Default**: `"password"` Key in the Secret for the database password. ##### `connectionSecretRef.dbNameKey` **Type**: `string`\ **Required**: No\ **Default**: `"dbname"` Key in the Secret for the database name. ##### `connectionSecretRef.sslModeKey` **Type**: `string`\ **Required**: No\ **Default**: `"sslmode"` Key in the Secret for the SSL mode. **SSL Modes**: `disable`, `require`, `verify-ca`, `verify-full`, `prefer` If the key is not found and not specified, defaults to `"prefer"`. **Example Secret Structure**: ```yaml apiVersion: v1 kind: Secret metadata: name: db-credentials type: Opaque stringData: host: "postgres.database.svc.cluster.local" port: "5432" username: "myuser" password: "mypassword" dbname: "mydb" sslmode: "require" ``` *** ### spec.query **Type**: `string`\ **Required**: Yes SQL query to execute against the database. Must return rows. **Supports**: * Standard SQL SELECT statements * Apache AGE Cypher queries * Multi-statement queries (setup commands + data query) ```yaml spec: query: | SELECT id, name, config FROM resources WHERE active = true; ``` **Multi-statement example** (Apache AGE): ```yaml spec: query: | LOAD '$libdir/plugins/age'; SET search_path = ag_catalog, "$user", public; SELECT * FROM cypher('graph', $$ MATCH (n:Node) RETURN n.id, n.name $$) AS (id text, name text); ``` **How multi-statements work**: 1. All statements except the last are executed as a batch 2. The final SELECT statement returns data 3. Enables setup commands like `LOAD` and `SET` *** ### `spec.template` **Type**: `string`\ **Required**: Yes Go template for rendering Kubernetes resource manifests. Each database row is rendered once. **Template Context**: * `.Row` - Map of column name → value for the current row * `.Metadata` - Parent DatabaseQueryResource metadata **Functions**: All [Sprig functions](http://masterminds.github.io/sprig/) are available. ```yaml spec: template: | apiVersion: v1 kind: ConfigMap metadata: name: {{ .Row.name | lower | replace "_" "-" }} namespace: {{ .Metadata.Namespace }} labels: app: {{ .Row.app_name }} environment: {{ .Row.env | default "dev" }} data: config: {{ .Row.config | toJson }} created: {{ now | date "2006-01-02" }} ``` **Common patterns**: * **String manipulation**: `{{ .Row.name | lower | replace " " "-" }}` * **Conditionals**: `{{ if eq .Row.env "prod" }}3{{ else }}1{{ end }}` * **Defaults**: `{{ .Row.replicas | default 1 }}` * **JSON encoding**: `{{ .Row.data | toJson }}` * **Base64**: `{{ .Row.secret | b64enc }}` *** ### spec.statusUpdateQuery **Type**: `string`\ **Required**: No Optional Go template for an SQL query that updates the database with resource status after reconciliation. **Template Context**: * `.Resource` - The Kubernetes resource object (unstructured) ```yaml spec: statusUpdateQueryTemplate: | UPDATE deployments SET replicas = {{ .Resource.status.replicas | default 0 }}, ready_replicas = {{ .Resource.status.readyReplicas | default 0 }}, updated_at = NOW() WHERE name = '{{ .Resource.metadata.name }}'; ``` **Use cases**: * Sync Kubernetes resource status back to database * Update timestamps or health checks * Trigger database-side logic based on resource state *** ### `spec.changeDetection` **Type**: `object`\ **Required**: No Optional configuration for efficient change detection polling. When enabled, the operator: 1. Polls for changes every `changePollInterval` (lightweight query) 2. Only runs full reconciliation when changes are detected 3. Still runs full reconciliation at `pollInterval` as a safety net #### `changeDetection.enabled` **Type**: `boolean`\ **Required**: Yes (if changeDetection is specified) Enable or disable change detection. #### `changeDetection.tableName` **Type**: `string`\ **Required**: Yes (if enabled) Database table to monitor for changes. Can include schema: `"schema.table"`. #### `changeDetection.timestampColumn` **Type**: `string`\ **Required**: Yes (if enabled) Column name that tracks when rows were last modified (e.g., `"updated_at"`, `"modified_at"`). **Requirements**: * Column type: `TIMESTAMP` or `TIMESTAMPTZ` * Should be indexed for performance * Auto-updated via trigger or application logic #### `changeDetection.changePollInterval` **Type**: `string` (duration)\ **Required**: No\ **Default**: `"10s"` How often to check for changes. Should be shorter than `spec.pollInterval`. **Example**: ```yaml spec: pollInterval: "5m" # Full reconciliation every 5 minutes changeDetection: enabled: true tableName: "public.resources" timestampColumn: "updated_at" changePollInterval: "10s" # Check for changes every 10 seconds ``` **Database setup**: ```sql -- Add timestamp column ALTER TABLE resources ADD COLUMN updated_at TIMESTAMP DEFAULT NOW(); -- Create trigger to auto-update CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER resources_update_timestamp BEFORE UPDATE ON resources FOR EACH ROW EXECUTE FUNCTION update_timestamp(); -- Index for performance CREATE INDEX idx_resources_updated_at ON resources(updated_at); ``` *** ## Status Fields The operator updates these status fields on the DatabaseQueryResource. ### `status.conditions` Array of condition objects indicating the current state. **Condition Types**: * `DBConnected` - Database connection status * `Reconciled` - Overall reconciliation status **Condition Structure**: ```yaml status: conditions: - type: DBConnected status: "True" reason: Connected message: "Successfully connected to the database" lastTransitionTime: "2025-10-26T14:30:00Z" - type: Reconciled status: "True" reason: Success message: "Successfully queried DB and reconciled resources" lastTransitionTime: "2025-10-26T14:30:05Z" ``` ### `status.lastPollTime` Timestamp of the last successful database query. ```yaml status: lastPollTime: "2025-10-26T14:30:00Z" ``` ### `status.lastReconcileTime` Timestamp of the last successful full reconciliation. ```yaml status: lastReconcileTime: "2025-10-26T14:30:05Z" ``` ### `status.managedResources` Array of identifiers for resources currently managed by this DBQR. **Format**: `///` or `//` for cluster-scoped ```yaml status: managedResources: - v1/default/frontend-config - v1/default/backend-config - argoproj.io/v1alpha1/argocd/my-application ``` ### `status.lastChangeCheckTime` (Change detection only) Timestamp used in the last change detection query. ```yaml status: lastChangeCheckTime: "2025-10-26T14:29:50Z" ``` ### `status.observedGeneration` Generation of the spec that was last processed. ```yaml status: observedGeneration: 3 ``` *** ## Complete Example ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: comprehensive-example namespace: default finalizers: - konnektr.io/databasequeryresource-finalizer spec: # Reconciliation settings pollInterval: "5m" prune: true # Change detection for fast updates changeDetection: enabled: true tableName: "public.applications" timestampColumn: "updated_at" changePollInterval: "10s" # Database connection database: type: postgres connectionSecretRef: name: postgres-cluster-app namespace: database-ns uriKey: fqdn-uri # SQL query (with Apache AGE) query: | LOAD '$libdir/plugins/age'; SET search_path = ag_catalog, "$user", public; SELECT * FROM cypher('apps', $$ MATCH (app:Application) WHERE app.active = true RETURN app.id AS id, app.name AS name, app.replicas AS replicas, app.image AS image $$) AS (id text, name text, replicas int, image text); # Resource template template: | apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Row.name | lower }} namespace: {{ .Metadata.Namespace }} labels: app: {{ .Row.name }} managed-by: {{ .Metadata.Name }} spec: replicas: {{ .Row.replicas | default 1 }} selector: matchLabels: app: {{ .Row.name }} template: metadata: labels: app: {{ .Row.name }} spec: containers: - name: app image: {{ .Row.image }} ports: - containerPort: 8080 # Status update query statusUpdateQueryTemplate: | SELECT * FROM cypher('apps', $$ MATCH (app:Application {id: '{{ .Resource.metadata.labels.id }}'}) SET app.replicas = {{ .Resource.status.replicas | default 0 }}, app.ready_replicas = {{ .Resource.status.readyReplicas | default 0 }}, app.updated_at = timestamp() RETURN app $$) AS (app agtype); ``` ## Next Steps See real-world configurations in action Deep dive into change detection, status updates, and more Complete CRD API documentation # Core Concepts This page explains the fundamental concepts behind the DB Query Operator and how it manages Kubernetes resources based on database state. ## Architecture Overview ## DatabaseQueryResource (DBQR) The `DatabaseQueryResource` is a Custom Resource Definition (CRD) that defines: * **Database connection** details (via Secret reference) * **SQL query** to execute * **Go template** for rendering Kubernetes manifests * **Reconciliation settings** (polling interval, pruning, change detection) ### Example Structure ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: example spec: pollInterval: "1m" # How often to query prune: true # Remove stale resources database: type: postgres connectionSecretRef: name: db-credentials query: "SELECT * FROM resources;" # SQL query template: | # Go template for manifests apiVersion: v1 kind: ConfigMap metadata: name: {{ .Row.name }} data: value: "{{ .Row.value }}" ``` ## Reconciliation Loop The operator runs a continuous reconciliation loop for each DatabaseQueryResource: ### 1. Query Phase The operator: * Connects to the database using credentials from the referenced Secret * Executes the SQL query (supporting multi-statement queries) * Retrieves all rows from the result set ### 2. Template Rendering Phase For each row returned: * The row data is made available as `.Row` in the template context * The Go template is rendered using the [Sprig function library](http://masterminds.github.io/sprig/) * The output is parsed as a Kubernetes manifest (YAML or JSON) ### 3. Resource Management Phase For each rendered manifest: * The operator checks if the resource already exists * Compares the desired state with the last applied configuration * Uses **Server-Side Apply** to create or update the resource * Adds the `konnektr.io/managed-by` label for tracking ### 4. Pruning Phase (if enabled) The operator: * Lists all resources with the `konnektr.io/managed-by` label * Identifies resources no longer in the current query results * Deletes stale resources that were managed but no longer exist in the database ### 5. Status Update Phase The operator updates the DatabaseQueryResource status with: * Connection status * Reconciliation success/failure * List of managed resources * Last poll time * Any error messages ## Row-to-Resource Mapping **One-to-One Relationship**: Each row in the query result typically generates one Kubernetes resource. ```sql SELECT id, name, config FROM apps WHERE active = true; -- Returns 3 rows → Creates 3 resources ``` ```yaml # Row 1: id=1, name='frontend', config='prod' apiVersion: v1 kind: ConfigMap metadata: name: frontend-config --- # Row 2: id=2, name='backend', config='staging' apiVersion: v1 kind: ConfigMap metadata: name: backend-config --- # Row 3: id=3, name='worker', config='dev' apiVersion: v1 kind: ConfigMap metadata: name: worker-config ``` ## Go Template Context Inside your template, you have access to: ### `.Row` Contains all columns from the current database row: ```yaml template: | # Access columns directly name: {{ .Row.app_name }} replicas: {{ .Row.replica_count }} # Use with functions name: {{ .Row.app_name | lower }} label: {{ .Row.app_name | replace "_" "-" }} ``` ### `.Metadata` Contains information about the parent DatabaseQueryResource: ```yaml template: | # Get the DBQR's namespace namespace: {{ .Metadata.Namespace }} # Get the DBQR's name labels: managed-by: {{ .Metadata.Name }} ``` ### Sprig Functions All [Sprig template functions](http://masterminds.github.io/sprig/) are available: ```yaml template: | # String functions name: {{ .Row.name | lower | replace " " "-" }} # Date functions created: {{ now | date "2006-01-02" }} # Encoding functions encoded: {{ .Row.data | b64enc }} # Conditional logic replicas: {{ if eq .Row.env "prod" }}3{{ else }}1{{ end }} # Default values timeout: {{ .Row.timeout | default "30s" }} ``` ## Managed Resource Labels All resources created by the operator receive a label: ```yaml labels: konnektr.io/managed-by: ``` This label is used to: * **Track ownership**: Identify which DBQR created the resource * **Enable pruning**: Find resources to delete when rows are removed * **Query resources**: Filter managed resources with `kubectl` ```bash # List all resources managed by a specific DBQR kubectl get all -l konnektr.io/managed-by=my-dbqr ``` ## Resource Ownership ### Same Namespace Resources When a managed resource is in the **same namespace** as the DatabaseQueryResource, the operator sets an **owner reference**: ```yaml metadata: ownerReferences: - apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource name: my-dbqr uid: controller: true ``` **Benefits**: * **Automatic cascade deletion**: Kubernetes automatically deletes managed resources when the DBQR is deleted * **Relationship visibility**: Tools like `kubectl tree` show the parent-child relationship ### Cross-Namespace or Cluster-Scoped Resources When managing resources in **different namespaces** or **cluster-scoped resources** (like ClusterRoles, Namespaces), owner references **cannot be set** (Kubernetes limitation). In these cases: * Only the `konnektr.io/managed-by` label is used for tracking * Manual cleanup is required if the DBQR is deleted (unless pruning + finalizer is used) ## Server-Side Apply The operator uses [Server-Side Apply (SSA)](https://kubernetes.io/docs/reference/using-api/server-side-apply/) to manage resources: ### Benefits * **Field-level conflict resolution**: Multiple controllers can manage different fields of the same resource * **Declarative updates**: Only specified fields are updated, others are preserved * **Field ownership tracking**: Kubernetes tracks which controller owns which fields ### Implications * The operator becomes the **field manager** for all fields in the template * Other controllers can still manage fields not in your template * Updates are atomic and efficient ### Change Detection The operator stores the last applied configuration in an annotation: ```yaml annotations: konnektr.io/last-applied-configuration: '' ``` Before applying a resource, the operator: 1. Fetches the current resource from the cluster 2. Retrieves the last applied configuration 3. Compares current desired state with last applied 4. Only applies if there are differences This **reduces unnecessary API calls** and **prevents update storms**. ## Pruning Behavior When `prune: true` (default): 1. **After each reconciliation**, the operator compares: * Resources currently in query results * Resources with the `konnektr.io/managed-by` label 2. **Stale resources** (labeled but not in results) are **deleted** 3. **Pruning is scoped** to the GVKs (GroupVersionKinds) the operator is configured to watch ### Example ```sql -- Initial state: 3 rows SELECT id FROM apps; -- Returns: 1, 2, 3 -- Creates: app-1, app-2, app-3 -- Row deleted DELETE FROM apps WHERE id = 2; SELECT id FROM apps; -- Returns: 1, 3 -- Deletes: app-2 -- Keeps: app-1, app-3 ``` ### Disabling Pruning ```yaml spec: prune: false # Resources are never deleted, only created/updated ``` Use this when: * You want manual control over resource deletion * Resources should persist even after database rows are removed * You have external processes managing resource lifecycle ## Finalizers To ensure managed resources are deleted when the DatabaseQueryResource is deleted, add a finalizer: ```yaml metadata: finalizers: - konnektr.io/databasequeryresource-finalizer ``` With a finalizer: 1. When the DBQR is deleted, Kubernetes marks it for deletion but doesn't remove it yet 2. The operator detects the deletion timestamp 3. The operator deletes all managed resources 4. Once complete, the operator removes the finalizer 5. Kubernetes completes the DBQR deletion **Without a finalizer**: Deleting the DBQR leaves managed resources in place (unless they have owner references). ## Polling vs Change Detection ### Standard Polling ```yaml spec: pollInterval: "5m" # Query every 5 minutes ``` * Simple and reliable * Works with any database schema * Higher latency (up to `pollInterval`) * More database load ### Change Detection (Optional) ```yaml spec: pollInterval: "5m" # Safety net changeDetection: enabled: true tableName: "apps" timestampColumn: "updated_at" changePollInterval: "10s" # Check for changes every 10s ``` * Lightweight change queries every 10 seconds * Full reconciliation only when changes detected * Lower latency (\~10 seconds) * Reduced database load * Requires `updated_at` timestamp column See [Advanced Topics](/docs/db-query-operator/advanced#change-detection) for implementation details. ## Multi-Statement Query Support The operator automatically handles multi-statement queries using **pgx batch operations**: ```yaml query: | LOAD '$libdir/plugins/age'; SET search_path = ag_catalog, "$user", public; SELECT * FROM cypher('graph', $$ MATCH (n:Node) RETURN n.id AS id, n.name AS name $$) AS (id text, name text); ``` **How it works**: 1. The operator splits the query on semicolons (respecting quotes and dollar-quotes) 2. All statements except the last are executed as a batch 3. The final statement (the SELECT) is executed and results are returned 4. This enables setup commands like `LOAD` and `SET` for Apache AGE ## Next Steps Detailed explanation of all CRD fields and options Real-world examples of DatabaseQueryResources Change detection, status updates, and cross-namespace management # Examples This page provides real-world examples of using the DB Query Operator for various scenarios. ## Multi-Tenant ConfigMaps Manage per-tenant configuration from a central database. ### Database Setup ```sql CREATE TABLE tenants ( tenant_id TEXT PRIMARY KEY, config_json JSONB NOT NULL, enabled BOOLEAN DEFAULT true ); INSERT INTO tenants VALUES ('acme-corp', '{"api_url": "https://acme.api.com", "max_requests": 1000}', true), ('globex', '{"api_url": "https://globex.api.com", "max_requests": 500}', true); ``` ### DatabaseQueryResource ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: tenant-configs namespace: tenants-system spec: pollInterval: "2m" prune: true database: type: postgres connectionSecretRef: name: postgres-credentials namespace: tenants-system uriKey: fqdn-uri # CloudNativePG query: | SELECT tenant_id, config_json FROM tenants WHERE enabled = true template: | apiVersion: v1 kind: ConfigMap metadata: name: tenant-{{ .Row.tenant_id }}-config namespace: tenants-system labels: tenant: {{ .Row.tenant_id | quote }} data: config.json: {{ .Row.config_json | toJson | quote }} ``` **Result**: Creates `tenant-acme-corp-config` and `tenant-globex-config` ConfigMaps automatically. *** ## ArgoCD Application Management Deploy ArgoCD Applications dynamically based on database records. ### Database Setup ```sql CREATE TABLE applications ( app_name TEXT PRIMARY KEY, repo_url TEXT NOT NULL, target_revision TEXT DEFAULT 'main', path TEXT NOT NULL, namespace TEXT NOT NULL, project TEXT DEFAULT 'default', auto_sync BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() ); INSERT INTO applications (app_name, repo_url, path, namespace) VALUES ('app-digitaltwins-env-dbqr-test', 'https://github.com/my-org/manifests', 'apps/digitaltwins/test', 'digitaltwins-test'); ``` ### DatabaseQueryResource ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: argocd-apps namespace: argocd spec: pollInterval: "1m" prune: true database: type: postgres connectionSecretRef: name: postgres-creds uriKey: fqdn-uri query: | SELECT app_name, repo_url, target_revision, path, namespace, project, auto_sync FROM applications WHERE created_at IS NOT NULL template: | apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: {{ .Row.app_name }} namespace: {{ .Metadata.Namespace }} spec: project: {{ .Row.project }} source: repoURL: {{ .Row.repo_url | quote }} targetRevision: {{ .Row.target_revision | quote }} path: {{ .Row.path | quote }} destination: server: https://kubernetes.default.svc namespace: {{ .Row.namespace }} {{- if .Row.auto_sync }} syncPolicy: automated: prune: true selfHeal: true {{- end }} ``` **Use case**: Centrally manage ArgoCD Application deployment from a database interface or API. *** ## Dynamic Deployments Create Kubernetes Deployments from database specifications. ### Database Setup ```sql CREATE TABLE services ( service_name TEXT PRIMARY KEY, image TEXT NOT NULL, replicas INT DEFAULT 1, port INT DEFAULT 80, env_vars JSONB DEFAULT '{}', namespace TEXT NOT NULL ); INSERT INTO services VALUES ('api-server', 'nginx:1.25', 2, 8080, '{"LOG_LEVEL": "info"}', 'production'); ``` ### DatabaseQueryResource ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: dynamic-deployments namespace: apps-system spec: pollInterval: "5m" prune: false # Keep deployments even if removed from DB database: type: postgres connectionSecretRef: name: db-secret uriKey: fqdn-uri query: | SELECT service_name, image, replicas, port, env_vars, namespace FROM services template: | apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Row.service_name }} namespace: {{ .Row.namespace }} spec: replicas: {{ .Row.replicas }} selector: matchLabels: app: {{ .Row.service_name }} template: metadata: labels: app: {{ .Row.service_name }} spec: containers: - name: {{ .Row.service_name }} image: {{ .Row.image | quote }} ports: - containerPort: {{ .Row.port }} env: {{- range $key, $value := (.Row.env_vars | fromJson) }} - name: {{ $key }} value: {{ $value | quote }} {{- end }} ``` *** ## Status Updates with Two-Way Sync Update database when Kubernetes resources change state. ### Database Setup ```sql CREATE TABLE deployments ( deployment_name TEXT PRIMARY KEY, image TEXT NOT NULL, namespace TEXT NOT NULL, ready_replicas INT DEFAULT 0, last_updated TIMESTAMP DEFAULT NOW() ); ``` ### DatabaseQueryResource ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: deployment-sync namespace: sync-system spec: pollInterval: "1m" prune: true database: type: postgres connectionSecretRef: name: db-creds uriKey: fqdn-uri query: | SELECT deployment_name, image, namespace FROM deployments template: | apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Row.deployment_name }} namespace: {{ .Row.namespace }} spec: replicas: 2 selector: matchLabels: app: {{ .Row.deployment_name }} template: metadata: labels: app: {{ .Row.deployment_name }} spec: containers: - name: app image: {{ .Row.image | quote }} # Update database with current ready replicas statusUpdateQueryTemplate: | UPDATE deployments SET ready_replicas = {{ .Status.readyReplicas | default 0 }}, last_updated = NOW() WHERE deployment_name = {{ .Metadata.Name | squote }} AND namespace = {{ .Metadata.Namespace | squote }} ``` **Result**: Deployment status in Kubernetes is continuously synced back to the database. *** ## Change Detection for Large Tables Only query database when data actually changes. ### Database Setup ```sql CREATE TABLE configs ( config_id TEXT PRIMARY KEY, data JSONB NOT NULL, updated_at TIMESTAMP DEFAULT NOW() ); -- Trigger to update timestamp CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER configs_updated_at BEFORE UPDATE ON configs FOR EACH ROW EXECUTE FUNCTION update_updated_at(); ``` ### DatabaseQueryResource ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: efficient-configs namespace: default spec: pollInterval: "30s" # Check frequently but only query if changed prune: true database: type: postgres connectionSecretRef: name: db-secret uriKey: fqdn-uri changeDetection: enabled: true tableName: "configs" timestampColumn: "updated_at" query: | SELECT config_id, data FROM configs template: | apiVersion: v1 kind: ConfigMap metadata: name: config-{{ .Row.config_id }} data: config.json: {{ .Row.data | toJson | quote }} ``` **Benefit**: With 30s polling, change detection reduces database load significantly by only executing the main query when `updated_at` changes. *** ## Cross-Namespace Resource Management Manage resources across multiple namespaces from a single DBQR. ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: cross-namespace-secrets namespace: secrets-controller spec: pollInterval: "5m" prune: true database: type: postgres connectionSecretRef: name: db-creds uriKey: fqdn-uri query: | SELECT secret_name, target_namespace, secret_data FROM shared_secrets WHERE enabled = true template: | apiVersion: v1 kind: Secret metadata: name: {{ .Row.secret_name }} namespace: {{ .Row.target_namespace }} # Target namespace from database type: Opaque stringData: data: {{ .Row.secret_data | quote }} ``` **RBAC Requirement**: The operator ServiceAccount needs permissions in target namespaces. *** ## Next Steps * See [Advanced Topics](/docs/db-query-operator/advanced) for change detection implementation details * Check [Configuration Reference](/docs/db-query-operator/configuration) for all CRD fields * Review [Troubleshooting](/docs/db-query-operator/troubleshooting) for common issues # Getting Started This guide will walk you through installing the DB Query Operator and creating your first DatabaseQueryResource. ## Prerequisites Before you begin, ensure you have: * **Kubernetes cluster** (v1.24+) - kind, minikube, EKS, GKE, AKS, or any Kubernetes distribution * **kubectl** configured to access your cluster * **Helm 3** installed * **PostgreSQL database** accessible from your Kubernetes cluster ## Step 1: Install the Operator ### Option A: Install via OCI Registry (Recommended) ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --version 0.6.0 \ --namespace dbqo-system \ --create-namespace \ --set gvkPattern="v1/ConfigMap;apps/v1/Deployment" ``` ### Option B: Install via Helm Repository ```bash # Add the Konnektr Helm repository helm repo add konnektr https://charts.konnektr.io helm repo update # Install the operator helm install db-query-operator konnektr/db-query-operator \ --namespace dbqo-system \ --create-namespace \ --set gvkPattern="v1/ConfigMap;apps/v1/Deployment" ``` ### Understanding `gvkPattern` The `gvkPattern` parameter specifies which Kubernetes resource types the operator can manage. Format: `group/version/Kind` or `version/Kind` for core resources. Examples: * `v1/ConfigMap` - Core ConfigMaps * `apps/v1/Deployment` - Deployments * `argoproj.io/v1alpha1/Application` - ArgoCD Applications * `postgresql.cnpg.io/v1/Cluster` - CloudNativePG clusters Use semicolons to separate multiple types: ```bash --set gvkPattern="v1/ConfigMap;v1/Secret;apps/v1/Deployment;argoproj.io/v1alpha1/Application" ``` ## Step 2: Verify Installation Check that the operator pod is running: ```bash kubectl get pods -n dbqo-system # Expected output: # NAME READY STATUS RESTARTS AGE # db-query-operator-xxxxx-xxxxx 1/1 Running 0 1m ``` View operator logs: ```bash kubectl logs -n dbqo-system -l app.kubernetes.io/name=db-query-operator -f ``` ## Step 3: Prepare Your Database Create a test table in your PostgreSQL database: ```sql CREATE TABLE app_configs ( id SERIAL PRIMARY KEY, app_name VARCHAR(50) NOT NULL, environment VARCHAR(20) NOT NULL, replicas INT DEFAULT 1, config_data JSONB, active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() ); -- Insert sample data INSERT INTO app_configs (app_name, environment, replicas, config_data) VALUES ('frontend', 'production', 3, '{"version": "1.0.0"}'), ('backend', 'production', 2, '{"version": "2.1.0"}'), ('worker', 'staging', 1, '{"version": "1.5.0"}'); ``` ## Step 4: Create Database Connection Secret Create a Kubernetes Secret with your database credentials: ```yaml title="db-credentials.yaml" apiVersion: v1 kind: Secret metadata: name: db-credentials namespace: default type: Opaque stringData: host: "postgres.database.svc.cluster.local" port: "5432" username: "myuser" password: "mypassword" dbname: "mydb" sslmode: "prefer" ``` Apply the secret: ```bash kubectl apply -f db-credentials.yaml ``` ### Using CloudNativePG Secrets If you're using CloudNativePG, you can reference the auto-generated secrets directly: ```yaml apiVersion: v1 kind: Secret metadata: name: my-postgres-app namespace: database-ns # CNPG automatically populates this secret with: # - fqdn-uri: postgresql://user:pass@host.namespace.svc.cluster.local:5432/db # - host, port, username, password, dbname ``` ## Step 5: Create Your First DatabaseQueryResource Create a file `my-first-dbqr.yaml`: ```yaml title="my-first-dbqr.yaml" apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: app-configs namespace: default spec: # Query the database every minute pollInterval: "1m" # Automatically remove resources when rows are deleted prune: true # Database connection database: type: postgres connectionSecretRef: name: db-credentials # namespace: database-ns # Optional: if secret is in different namespace # SQL query - each row becomes one resource query: | SELECT app_name, environment, replicas, config_data->>'version' as version FROM app_configs WHERE active = true; # Go template to generate Kubernetes resources template: | apiVersion: v1 kind: ConfigMap metadata: name: {{ .Row.app_name }}-{{ .Row.environment }}-config namespace: {{ .Metadata.Namespace }} labels: app: {{ .Row.app_name }} environment: {{ .Row.environment }} data: app_name: "{{ .Row.app_name }}" environment: "{{ .Row.environment }}" replicas: "{{ .Row.replicas }}" version: "{{ .Row.version }}" ``` Apply the resource: ```bash kubectl apply -f my-first-dbqr.yaml ``` ## Step 6: Verify the Results Check the status of your DatabaseQueryResource: ```bash kubectl get databasequeryresource app-configs -o yaml ``` Look for the status section: ```yaml status: conditions: - type: DBConnected status: "True" message: "Successfully connected to the database" - type: Reconciled status: "True" message: "Successfully queried DB and reconciled resources" lastPollTime: "2025-10-26T14:30:00Z" managedResources: - v1/default/frontend-production-config - v1/default/backend-production-config - v1/default/worker-staging-config ``` View the created ConfigMaps: ```bash # List managed ConfigMaps kubectl get configmaps -l konnektr.io/managed-by=app-configs # View a specific ConfigMap kubectl get configmap frontend-production-config -o yaml ``` ## What Happens Next? The operator will: 1. **Poll the database** every `pollInterval` (1 minute in this example) 2. **Execute the query** and get current rows 3. **Render templates** for each row using Go templating 4. **Apply resources** to the cluster using Server-Side Apply 5. **Prune stale resources** if rows are removed from the database 6. **Update status** with reconciliation results ## Testing Changes ### Add a New Row Add a new row to your database: ```sql INSERT INTO app_configs (app_name, environment, replicas, config_data) VALUES ('api', 'production', 2, '{"version": "3.0.0"}'); ``` Wait up to 1 minute (your `pollInterval`), then check for the new ConfigMap: ```bash kubectl get configmap api-production-config ``` ### Modify a Row Update a row: ```sql UPDATE app_configs SET replicas = 5, config_data = '{"version": "1.1.0"}' WHERE app_name = 'frontend' AND environment = 'production'; ``` The operator will update the corresponding ConfigMap on the next poll. ### Delete a Row Delete a row (with pruning enabled): ```sql DELETE FROM app_configs WHERE app_name = 'worker' AND environment = 'staging'; ``` The operator will delete the corresponding ConfigMap. ## Next Steps Learn about reconciliation loops, templates, and pruning Explore all configuration options and CRD fields See real-world examples including ArgoCD Applications and more Learn about change detection, status updates, and cross-namespace resources ## Troubleshooting If you encounter issues: 1. **Check operator logs**: ```bash kubectl logs -n dbqo-system -l app.kubernetes.io/name=db-query-operator ``` 2. **Check DatabaseQueryResource status**: ```bash kubectl describe databasequeryresource app-configs ``` 3. **Verify database connectivity**: * Ensure the secret has correct credentials * Check network policies allow operator → database traffic * Verify database host is resolvable from the cluster See the [Troubleshooting Guide](/docs/db-query-operator/troubleshooting) for more help. # Installation This guide covers all installation methods for the DB Query Operator, from quick starts to production deployments. ## Prerequisites * **Kubernetes**: v1.24 or later * **Helm**: v3.8 or later * **kubectl**: Configured to access your cluster * **PostgreSQL**: v12 or later ## Installation Methods ### Method 1: Helm with OCI Registry (Recommended) The operator is published as an OCI artifact to GitHub Container Registry: ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --version 0.6.0 \ --namespace dbqo-system \ --create-namespace ``` **Advantages**: * Direct pull from GHCR * Version immutability * Faster downloads ### Method 2: Helm Repository Add the Konnektr Helm repository: ```bash # Add repository helm repo add konnektr https://charts.konnektr.io helm repo update # Install operator helm install db-query-operator konnektr/db-query-operator \ --namespace dbqo-system \ --create-namespace ``` ### Method 3: Install CRDs Only If you want to install CRDs separately (e.g., for GitOps workflows): ```bash kubectl apply -f https://github.com/konnektr-io/db-query-operator/releases/latest/download/crds.yaml ``` Then install the operator without CRDs: ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --namespace dbqo-system \ --create-namespace \ --skip-crds ``` ## Configuration Options ### Required: Resource Types (GVK Pattern) The operator needs to know which Kubernetes resource types it can manage. Configure this with `gvkPattern`: ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --namespace dbqo-system \ --create-namespace \ --set gvkPattern="v1/ConfigMap;apps/v1/Deployment;argoproj.io/v1alpha1/Application" ``` **Format**: `//` or `/` for core resources **Common Examples**: * Core resources: `v1/ConfigMap`, `v1/Secret`, `v1/Service`, `v1/Namespace` * Apps: `apps/v1/Deployment`, `apps/v1/StatefulSet`, `apps/v1/DaemonSet` * Batch: `batch/v1/Job`, `batch/v1/CronJob` * ArgoCD: `argoproj.io/v1alpha1/Application` * CNPG: `postgresql.cnpg.io/v1/Cluster` * Kusto: `kusto.azure.com/v1api20230815/Database` Use semicolons to separate multiple types. ### Image Configuration Specify custom image registry or version: ```bash --set image.repository=ghcr.io/konnektr-io/db-query-operator \ --set image.tag=0.6.0 \ --set image.pullPolicy=IfNotPresent ``` ### Resource Limits Configure CPU and memory: ```bash --set resources.limits.cpu=500m \ --set resources.limits.memory=256Mi \ --set resources.requests.cpu=100m \ --set resources.requests.memory=128Mi ``` ### Namespace Override Deploy to a specific namespace (creates if doesn't exist): ```bash --set namespaceOverride=my-operators ``` ### Service Account Use an existing service account: ```bash --set serviceAccount.create=false \ --set serviceAccount.name=my-service-account ``` ### Security Context The chart includes secure defaults: ```yaml securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL ``` ## Complete Installation Example Here's a production-ready installation: ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --version 0.6.0 \ --namespace dbqo-system \ --create-namespace \ --set gvkPattern="v1/ConfigMap;v1/Secret;apps/v1/Deployment;argoproj.io/v1alpha1/Application;postgresql.cnpg.io/v1/Cluster" \ --set resources.limits.cpu=1000m \ --set resources.limits.memory=512Mi \ --set resources.requests.cpu=200m \ --set resources.requests.memory=256Mi \ --set replicaCount=1 ``` ## Values File Alternatively, create a `values.yaml` file: ```yaml title="values.yaml" replicaCount: 1 image: repository: ghcr.io/konnektr-io/db-query-operator tag: "0.6.0" pullPolicy: IfNotPresent gvkPattern: "v1/ConfigMap;v1/Secret;apps/v1/Deployment;argoproj.io/v1alpha1/Application" resources: limits: cpu: 1000m memory: 512Mi requests: cpu: 200m memory: 256Mi serviceAccount: create: true name: "" rbac: create: true securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL ``` Install with the values file: ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --version 0.6.0 \ --namespace dbqo-system \ --create-namespace \ -f values.yaml ``` ## Verification Check that the operator is running: ```bash # Check pod status kubectl get pods -n dbqo-system # View operator logs kubectl logs -n dbqo-system -l app.kubernetes.io/name=db-query-operator -f # Check CRD installation kubectl get crd databasequeryresources.konnektr.io ``` Expected output: ``` NAME READY STATUS RESTARTS AGE db-query-operator-xxxxx-xxxxx 1/1 Running 0 30s ``` ## Upgrading ### Helm Upgrade ```bash helm upgrade db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --version 0.7.0 \ --namespace dbqo-system \ --reuse-values ``` ### Check Release History ```bash helm history db-query-operator -n dbqo-system ``` ### Rollback ```bash helm rollback db-query-operator -n dbqo-system ``` ## Uninstallation ### Remove Helm Release ```bash helm uninstall db-query-operator -n dbqo-system ``` **Note**: This does NOT delete: * DatabaseQueryResource custom resources * Resources managed by DBQRs * The namespace `dbqo-system` ### Delete CRDs ```bash kubectl delete crd databasequeryresources.konnektr.io ``` **Warning**: Deleting the CRD will delete all DatabaseQueryResource instances! ### Complete Cleanup ```bash # 1. Delete all DatabaseQueryResources (optional: manage cleanup) kubectl delete databasequeryresources --all --all-namespaces # 2. Uninstall Helm release helm uninstall db-query-operator -n dbqo-system # 3. Delete CRDs kubectl delete crd databasequeryresources.konnektr.io # 4. Delete namespace kubectl delete namespace dbqo-system ``` ## GitOps Deployment (ArgoCD) For GitOps workflows, create an ArgoCD Application: ```yaml title="argocd-app.yaml" apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: db-query-operator namespace: argocd spec: project: default source: chart: db-query-operator repoURL: https://charts.konnektr.io targetRevision: 0.6.0 helm: values: | gvkPattern: "v1/ConfigMap;apps/v1/Deployment" resources: limits: cpu: 500m memory: 256Mi requests: cpu: 100m memory: 128Mi destination: server: https://kubernetes.default.svc namespace: dbqo-system syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true ``` Apply the Application: ```bash kubectl apply -f argocd-app.yaml ``` ## Air-Gapped Environments For air-gapped deployments: 1. **Pull the Helm chart**: ```bash helm pull oci://ghcr.io/konnektr-io/charts/db-query-operator --version 0.6.0 ``` 2. **Pull and re-tag the image**: ```bash docker pull ghcr.io/konnektr-io/db-query-operator:0.6.0 docker tag ghcr.io/konnektr-io/db-query-operator:0.6.0 your-registry.com/db-query-operator:0.6.0 docker push your-registry.com/db-query-operator:0.6.0 ``` 3. **Install with custom image**: ```bash helm install db-query-operator ./db-query-operator-0.6.0.tgz \ --namespace dbqo-system \ --create-namespace \ --set image.repository=your-registry.com/db-query-operator \ --set image.tag=0.6.0 ``` ## RBAC Permissions The operator requires these permissions: * **DatabaseQueryResources**: Full CRUD + status updates * **Secrets**: Read access (for database credentials) * **Managed Resource Types** (configured via `gvkPattern`): Full CRUD The Helm chart automatically creates: * ServiceAccount * ClusterRole with required permissions * ClusterRoleBinding ## Network Policies If using network policies, allow traffic: **From Operator to Database**: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-operator-to-db namespace: dbqo-system spec: podSelector: matchLabels: app.kubernetes.io/name: db-query-operator policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: name: database-namespace ports: - protocol: TCP port: 5432 ``` **From Operator to Kubernetes API**: ```yaml egress: - to: - namespaceSelector: {} ports: - protocol: TCP port: 443 ``` ## Next Steps Create your first DatabaseQueryResource Learn about all configuration options Explore real-world use cases # Database Query Operator The **Database Query Operator** is a Kubernetes operator that bridges your database state with your Kubernetes cluster. It periodically queries a PostgreSQL database, executes user-defined SQL queries, and dynamically creates, updates, or deletes Kubernetes resources based on the results. ## Why Use the DB Query Operator? * **Database-Driven Infrastructure**: Manage your Kubernetes resources directly from your application's database state. No need for separate configuration files or manual updates. * **Dynamic Resource Management**: Automatically create, update, and prune resources as your database state changes. Perfect for multi-tenant environments and dynamic workloads. * **Multi-Statement Queries**: Support for multi-statement SQL queries, enabling setup commands and complex query patterns. * **Change Detection**: Efficient polling with optional change detection reduces database load while maintaining quick response times to state changes. ## Key Features * **PostgreSQL Support**: Query relational data with standard SQL or multi-statement queries * **Go Templating**: Use powerful Go templates with Sprig functions to generate any Kubernetes manifest * **Multi-Statement Queries**: Execute setup commands (LOAD, SET) alongside your data queries * **CloudNativePG Integration**: Native support for CNPG-managed databases with URI-based connections * **Change Detection**: Optional lightweight polling for sub-second response to database changes * **Automatic Pruning**: Clean up resources that no longer exist in query results * **Cross-Namespace Resources**: Manage resources across namespaces and cluster-scoped resources * **Status Synchronization**: Optionally write Kubernetes resource status back to your database ## Quick Start Install the operator via Helm: ```bash helm install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --namespace dbqo-system \ --create-namespace ``` Create your first DatabaseQueryResource: ```yaml apiVersion: konnektr.io/v1alpha1 kind: DatabaseQueryResource metadata: name: my-first-dbqr spec: pollInterval: "1m" database: type: postgres connectionSecretRef: name: db-credentials query: "SELECT id, name FROM resources WHERE active = true;" template: | apiVersion: v1 kind: ConfigMap metadata: name: resource-{{ .Row.id }} data: name: "{{ .Row.name }}" ``` ## Use Cases ### Multi-Tenant Environments Automatically provision tenant-specific resources (namespaces, databases, applications) based on tenant records in your database. ### Digital Twin Management Use graph queries to discover relationships between digital twins and create corresponding Kubernetes resources for IoT workloads, monitoring, or data processing. ### Dynamic Application Deployment Deploy ArgoCD Applications, Helm releases, or other resources based on environment configurations stored in your database. ### Infrastructure as Data Treat your database as the source of truth for infrastructure configuration, enabling programmatic resource management through standard database operations. ## Architecture The operator runs as a Kubernetes Deployment and: 1. Periodically queries your database 2. Renders Kubernetes manifests using Go templates 3. Applies resources to the cluster using Server-Side Apply 4. Optionally prunes resources no longer in query results 5. Updates database with resource status ## Next Steps * **[Getting Started](/docs/db-query-operator/getting-started)**: Install the operator and create your first DatabaseQueryResource * **[Core Concepts](/docs/db-query-operator/core-concepts)**: Understand how the operator works and key concepts * **[Examples](/docs/db-query-operator/examples)**: Explore real-world examples and common patterns * **[Configuration](/docs/db-query-operator/configuration)**: Learn about all configuration options and fields ## Community & Support * **GitHub**: [konnektr-io/db-query-operator](https://github.com/konnektr-io/db-query-operator) * **Issues**: Report bugs or request features on GitHub * **License**: Apache 2.0 # Troubleshooting Common problems and their solutions when using the DB Query Operator. ## Connection Issues ### Error: "connection refused" **Symptom**: ``` Error: failed to connect to database: dial tcp :5432: connect: connection refused ``` **Causes**: 1. Database not accessible from cluster 2. Incorrect host/port in secret 3. Network policies blocking traffic **Solutions**: #### Check Database Secret ```bash kubectl get secret postgres-credentials -n my-namespace -o yaml ``` Verify fields: * `host` or `fqdn-uri` is correct * `port` matches database (usually `5432`) * Credentials are valid #### Test Connection from Pod ```bash kubectl run -it --rm debug --image=postgres:15 --restart=Never -- \ psql -h db-host.namespace.svc.cluster.local -U myuser -d mydb ``` #### Check Network Policies ```bash kubectl get networkpolicies -A kubectl describe networkpolicy -n ``` *** ### Error: "FATAL: password authentication failed" **Symptom**: ``` Error: failed to authenticate: FATAL: password authentication failed for user "myuser" ``` **Solutions**: 1. **Verify Secret Keys**: ```bash kubectl get secret my-secret -o jsonpath='{.data.password}' | base64 -d ``` 2. **Check Secret Ref Configuration**: ```yaml spec: database: connectionSecretRef: name: my-secret passwordKey: password # Must match secret key name usernameKey: username ``` 3. **Test Credentials Manually**: ```bash PGPASSWORD="actual-password" psql -h host -U username -d database ``` *** ### Error: "no such host" **Symptom**: ``` Error: lookup db-host.wrong-namespace: no such host ``` **Cause**: Incorrect hostname for cross-namespace database access. **Solution**: Use URI-based connection with FQDN: ```yaml spec: database: connectionSecretRef: name: postgres-creds namespace: database-namespace uriKey: fqdn-uri # CloudNativePG provides this ``` Or specify full hostname: ```yaml # In secret host: postgres-cluster-rw.database-namespace.svc.cluster.local ``` *** ## Query Errors ### Error: "cannot insert multiple commands into a prepared statement" **Symptom**: ``` Error executing query: cannot insert multiple commands into a prepared statement ``` **Cause**: Using multi-statement query with older operator version. **Solution**: Upgrade to v0.6.0+ which supports multi-statement queries via `pgx.Batch`: ```bash helm upgrade db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ --version 0.6.0 \ -n dbqo-system ``` **Example Working Query**: ```yaml spec: query: | SET statement_timeout = '30s'; SET search_path = myschema, public; SELECT id, name, config FROM my_table WHERE enabled = true; ``` *** ### Error: "relation does not exist" **Symptom**: ``` Error: relation "my_table" does not exist ``` **Solutions**: 1. **Verify Table Name**: ```sql \dt -- List tables in psql SELECT tablename FROM pg_tables WHERE schemaname = 'public'; ``` 2. **Check Schema**: ```yaml spec: query: | SET search_path = my_schema, public; SELECT * FROM my_table; ``` 3. **Verify Database Name**: Ensure `databaseKey` in secret points to correct database. *** ### Error: "column does not exist" **Symptom**: ``` Error: column "my_column" does not exist ``` **Solutions**: 1. **Check Column Names**: ```sql SELECT column_name FROM information_schema.columns WHERE table_name = 'my_table'; ``` 2. **Case Sensitivity**: PostgreSQL folds unquoted identifiers to lowercase: ```sql -- Wrong (if column is actually "myColumn") SELECT MyColumn FROM table; -- Right SELECT "MyColumn" FROM table; -- Or rename in query SELECT my_column FROM table; ``` *** ## Template Rendering Errors ### Error: "template: :X:Y: executing template" **Symptom**: ``` Error rendering template: template: :12:5: executing "template" at <.Row.missing_field>: map has no entry for key "missing_field" ``` **Cause**: Template references field not returned by query. **Solutions**: 1. **Verify Query Returns Field**: ```sql -- Check actual query output SELECT * FROM my_table LIMIT 1; ``` 2. **Use Default Value**: ```yaml resourceTemplate: | data: value: {{ .Row.optional_field | default "default-value" }} ``` 3. **Conditional Rendering**: ```yaml resourceTemplate: | {{- if .Row.optional_field }} data: value: {{ .Row.optional_field }} {{- end }} ``` *** ### Error: "yaml: line X: did not find expected key" **Symptom**: Resources not created, YAML parsing errors in logs. **Cause**: Template renders invalid YAML. **Solutions**: 1. **Test Template Locally**: ```bash # Save template to file cat > template.yaml << 'EOF' apiVersion: v1 kind: ConfigMap metadata: name: test-{{ .Row.id }} data: value: {{ .Row.value }} EOF # Test with sample data echo '{"id": "123", "value": "test"}' | \ jq -r 'to_entries | map(.key + "=" + (.value | tostring)) | .[]' | \ ... # Use Go template engine ``` 2. **Quote String Values**: ```yaml # Wrong data: value: {{ .Row.text }} # Right data: value: {{ .Row.text | quote }} ``` 3. **Check for Special Characters**: ```yaml # Escape special YAML characters data: value: {{ .Row.text | quote }} json: {{ .Row.data | toJson | quote }} ``` *** ## Resource Management Issues ### Resources Not Created **Check DatabaseQueryResource Status**: ```bash kubectl describe databasequeryresource my-dbqr -n my-namespace ``` **Common Causes**: 1. **Query Returns No Rows**: * Verify query in database directly * Check WHERE clauses 2. **Template Rendering Failed**: * Check operator logs for template errors * Validate template syntax 3. **RBAC Permissions Missing**: ```bash # Check operator logs kubectl logs -n dbqo-system deployment/db-query-operator | grep "forbidden" ``` **Solution**: Update operator RBAC or gvkPattern: ```bash helm upgrade db-query-operator ... \ --set gvkPattern="v1/ConfigMap;apps/v1/Deployment" ``` *** ### Resources Not Pruned **Symptom**: Resources remain after removing database rows. **Solutions**: 1. **Enable Pruning**: ```yaml spec: prune: true # Default, but verify ``` 2. **Check Owner References** (same-namespace only): ```bash kubectl get configmap my-resource -o yaml | grep -A5 ownerReferences ``` 3. **Cross-Namespace Resources**: Pruning for cross-namespace resources uses labels/tracking: ```bash # Check if resource is tracked kubectl get configmap -n other-namespace my-resource -o yaml | grep "managed-by" ``` *** ### Resources Keep Reverting **Symptom**: Manual `kubectl edit` changes are reverted. **Cause**: Server-Side Apply - operator owns those fields. **Solutions**: 1. **Update Database Instead**: ```sql UPDATE my_table SET value = 'new-value' WHERE id = 'my-resource'; ``` 2. **Change Non-Managed Fields** (safe): ```bash kubectl label configmap my-resource custom-label=value kubectl annotate configmap my-resource custom-annotation=value ``` 3. **Disable Operator for Resource**: ```sql -- Remove from query results temporarily UPDATE my_table SET enabled = false WHERE id = 'my-resource'; ``` *** ## Performance Issues ### High Database Load **Symptoms**: * High CPU/memory on database * Slow query responses * Connection pool exhaustion **Solutions**: 1. **Enable Change Detection**: ```yaml spec: changeDetection: enabled: true tableName: "my_table" timestampColumn: "updated_at" ``` 2. **Increase Poll Interval**: ```yaml spec: pollInterval: "5m" # From 30s ``` 3. **Optimize Query**: ```sql -- Add indexes CREATE INDEX idx_enabled ON my_table(enabled) WHERE enabled = true; CREATE INDEX idx_updated ON my_table(updated_at); -- Use indexed columns in WHERE SELECT * FROM my_table WHERE enabled = true; ``` 4. **Limit Result Set**: ```sql -- Only active records SELECT * FROM my_table WHERE enabled = true; -- Recent changes only SELECT * FROM my_table WHERE updated_at > NOW() - INTERVAL '24 hours'; ``` *** ### Slow Resource Creation **Symptoms**: * Long reconciliation times * Resources appear slowly **Solutions**: 1. **Simplify Template**: ```yaml # Avoid complex logic in templates # Move logic to SQL query instead ``` 2. **Check Network Latency**: ```bash kubectl run -it --rm debug --image=nicolaka/netshoot -- \ time curl -k https://kubernetes.default.svc ``` 3. **Review Operator Logs**: ```bash kubectl logs -n dbqo-system deployment/db-query-operator -f ``` *** ## CRD and RBAC Issues ### Error: "no matches for kind DatabaseQueryResource" **Cause**: CRD not installed. **Solution**: ```bash # Check CRD kubectl get crd databasequeryresources.konnektr.io # Install/upgrade operator helm upgrade --install db-query-operator \ oci://ghcr.io/konnektr-io/charts/db-query-operator \ -n dbqo-system --create-namespace \ --set gvkPattern="v1/ConfigMap" ``` *** ### Error: "forbidden: User cannot create resource" **Symptom**: ``` Error: failed to apply resource: forbidden: User "system:serviceaccount:dbqo-system:db-query-operator" cannot create resource "deployments" in API group "apps" ``` **Cause**: Operator ServiceAccount lacks RBAC permissions. **Solution**: 1. **Add Resource Type to gvkPattern**: ```bash helm upgrade db-query-operator ... \ --set gvkPattern="v1/ConfigMap;apps/v1/Deployment" ``` 2. **For Cross-Namespace Resources**, manually create ClusterRole: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: db-query-operator-cross-ns rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: db-query-operator-cross-ns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: db-query-operator-cross-ns subjects: - kind: ServiceAccount name: db-query-operator namespace: dbqo-system ``` *** ## Debugging Checklist When troubleshooting, check in order: 1. **DatabaseQueryResource Status**: ```bash kubectl describe databasequeryresource -n ``` 2. **Operator Logs**: ```bash kubectl logs -n dbqo-system deployment/db-query-operator --tail=100 -f ``` 3. **Database Connectivity**: ```bash kubectl run -it --rm debug --image=postgres:15 -- \ psql "$(kubectl get secret -o jsonpath='{.data.fqdn-uri}' | base64 -d)" ``` 4. **Query Results**: ```sql -- Run query directly in database SELECT * FROM my_table; ``` 5. **Template Rendering**: * Copy template to local file * Test with sample data * Validate resulting YAML 6. **Resource Creation**: ```bash kubectl get -A -l managed-by=db-query-operator ``` 7. **RBAC Permissions**: ```bash kubectl auth can-i create deployments \ --as=system:serviceaccount:dbqo-system:db-query-operator \ -n ``` *** ## Getting Help ### Logs to Collect ```bash # Operator logs kubectl logs -n dbqo-system deployment/db-query-operator --tail=200 > operator.log # DBQR description kubectl describe databasequeryresource -n > dbqr.yaml # CRD definition kubectl get databasequeryresource -n -o yaml > dbqr-spec.yaml ``` ### Information to Provide When reporting issues: 1. Operator version (`helm list -n dbqo-system`) 2. Kubernetes version (`kubectl version`) 3. Database type and version 4. Sanitized DBQR spec (remove credentials) 5. Operator logs showing error 6. Query results (sample) ### Community Support * **GitHub Issues**: [github.com/konnektr-io/db-query-operator/issues](https://github.com/konnektr-io/db-query-operator/issues) * **Documentation**: [docs.konnektr.io/docs/db-query-operator](https://docs.konnektr.io/docs/db-query-operator) *** ## Frequently Asked Questions ### Can I use multiple databases? Yes, create separate `DatabaseQueryResource` objects with different `connectionSecretRef`. ### Does it support MySQL/MongoDB/etc? Currently PostgreSQL only. Other databases planned for future releases. ### Can one DBQR create multiple resource types? No, one DBQR creates one resource type (GVK). Create multiple DBQRs for different types. ### What happens if the database is down? The operator logs errors and retries on next `pollInterval`. Existing resources remain unchanged. ### Can I use with read replicas? Yes, point connection secret to read replica host for read-only queries. ### Does it support database transactions? No, each query auto-commits. Use database-level constraints for consistency. ### Can templates call external APIs? No, templates are pure Go templates with Sprig functions. No network calls. ### How do I rotate database credentials? Update the Secret referenced by `connectionSecretRef`. Operator will reconnect automatically on next reconciliation. *** ## Next Steps * Review [Examples](/docs/db-query-operator/examples) for working configurations * See [Advanced Topics](/docs/db-query-operator/advanced) for optimization strategies * Check [Configuration Reference](/docs/db-query-operator/configuration) for all options # JEXL Extended Documentation Welcome to JEXL Extended, a powerful context-based expression parser and evaluator with 80+ built-in functions. Available in multiple programming languages with identical JEXL syntax. ## Available Implementations ### 🟨 JavaScript/TypeScript * **Repository**: [jexl-extended](https://github.com/konnektr-io/jexl-extended) * **Installation**: `npm install jexl-extended` * **Documentation**: [JavaScript Guide](./javascript/) ### 🐍 Python * **Repository**: [pyjexl-extended](https://github.com/konnektr-io/pyjexl-extended) * **Installation**: `pip install pyjexl-extended` * **Documentation**: [Python Guide](./python/) ### 🔷 C\# * **Repository**: [JexlNet](https://github.com/konnektr-io/JexlNet) * **Installation**: `Install-Package JexlNet` * **Documentation**: [C# Guide](./csharp/) ## Quick Examples The same JEXL expressions work across all implementations: **JavaScript:** ```javascript import jexl from 'jexl-extended'; const result = jexl.evalSync('user.name | uppercase', { user: { name: 'John' } }); // "JOHN" ``` **Python:** ```python from pyjexl_extended import jexl result = jexl.eval('user.name | uppercase', {'user': {'name': 'John'}}) # "JOHN" ``` **C#:** ```csharp using JexlNet; var jexl = new Jexl(new ExtendedGrammar()); var result = jexl.Eval("user.name | uppercase", new { user = new { name = "John" } }); // "JOHN" ``` ## Universal Language Guide The JEXL language syntax is identical across all implementations: ### 📖 [Language Reference](./language/) * [Syntax Overview](./language/syntax) - Basic JEXL syntax rules * [Data Types](./language/data-types) - Strings, numbers, arrays, objects * [Operators](./language/operators) - Arithmetic, comparison, logical operators * [Expressions](./language/expressions) - Complex expression patterns * [Context and Variables](./language/context) - Working with data context ### � [Function Reference](./reference/) Complete documentation for all 80+ functions: * [Array Functions](./reference/array/) - Collection processing * [String Functions](./reference/string/) - Text manipulation * [Math Functions](./reference/math/) - Numerical operations * [Date/Time Functions](./reference/datetime/) - Date handling * [Utility Functions](./reference/utility/) - Helpers and conversions ## Usage Guides Choose your programming language for detailed usage instructions: ### � [JavaScript Implementation](./usage/javascript/) * Installation with npm/yarn * Basic usage and integration patterns * Monaco Editor setup with IntelliSense * TypeScript support and type definitions * Performance optimization techniques ### 🐍 [Python Implementation](./usage/python/) * Installation with pip * Basic usage patterns * Integration with Flask/Django * Error handling strategies * Performance considerations ### 🔷 [C# Implementation](./usage/csharp/) * NuGet package installation * Basic usage with JsonNode * ASP.NET Core integration * Async/sync evaluation patterns * Custom functions and transforms ## 🎮 Interactive Playground Try JEXL Extended expressions online with our interactive playground: ### [Launch Playground →](https://konnektr-io.github.io/jexl-playground/) The playground features: * **Live Expression Editor** - Monaco Editor with syntax highlighting and IntelliSense * **Real-time Evaluation** - See results as you type * **Sample Data** - Pre-loaded with example contexts * **Save & Share** - Save your expressions for later * **Examples Library** - Learn from practical examples 📖 [Playground Guide](./playground/) - Learn how to use all playground features ## Key Features * **🚀 80+ Built-in Functions** - String manipulation, math, arrays, objects, dates, and more * **🎨 Monaco Editor Support** - Syntax highlighting, IntelliSense, and hover documentation * **📝 TypeScript Support** - Full type definitions included * **🔧 Modular** - Use the entire library or import individual functions ## Examples ### Data Transformation ```javascript // Transform an array of users const users = [ { name: "Alice", age: 28, department: "Engineering" }, { name: "Bob", age: 32, department: "Marketing" }, { name: "Charlie", age: 24, department: "Engineering" } ]; // Get names of engineers older than 25 jexl.evalSync('users|filter("value.department == \\"Engineering\\" && value.age > 25")|map("value.name")', { users }); // ["Alice"] ``` ### String Processing ```javascript // Process and format text jexl.evalSync('"hello world" | uppercase | split(" ") | join("-")'); // "HELLO-WORLD" ``` ### Mathematical Operations ```javascript // Calculate statistics jexl.evalSync('numbers | sum / length(numbers)', { numbers: [1, 2, 3, 4, 5] }); // 3 (average) ``` ## Contributing JEXL Extended is open source. Contributions are welcome! Please see our GitHub repository for more information. ## License MIT License - see LICENSE file for details. # Playground Guide # JEXL Extended Playground The JEXL Extended Playground is an interactive web application that lets you experiment with JEXL expressions in real-time. Perfect for learning, testing, and prototyping. ## 🚀 [Launch Playground](https://jexl-playground.konnektr.io/) ## Features Overview ### 🎨 **Rich Expression Editor** * Monaco Editor with JEXL syntax highlighting * IntelliSense auto-completion for all 80+ functions * Hover documentation with examples * Real-time syntax validation ### ⚡ **Live Evaluation** * Automatic expression evaluation as you type * 500ms debounced for optimal performance * Visual indicators for evaluation status * Detailed error messages with suggestions ### 💾 **Session Management** * Auto-save your work (restored for 24 hours) * Save named sessions with descriptions * Load and manage saved expressions * Import/export functionality ### 📚 **Examples Library** * Pre-built examples for common use cases * Copy examples to start experimenting * Learn JEXL patterns and best practices ## Interface Layout ### Left Panel: Sessions & Examples * **Saved Sessions** - Your saved expressions * **Examples** - Pre-built examples to learn from * **Quick Actions** - Save, load, and manage sessions ### Right Panel: Editors #### **Top Row: Input** * **JEXL Expression Editor** (left) - Write your expressions here * **Context Editor** (right) - JSON data for your expressions #### **Bottom Row: Output** * **Result Display** - Formatted output with type information * **Error Display** - Detailed error messages when things go wrong ## Getting Started ### 1. **Open the Playground** Visit [jexl-playground.konnektr.io](https://jexl-playground.konnektr.io/) ### 2. **Try the Default Example** The playground loads with a sample expression: ``` users|filter('value.active')|map('value.name')|sort() ``` ### 3. **Modify the Expression** Try changing the expression to: ``` users|filter('value.age > 25')|map('value.name + " (" + value.department + ")"') ``` ### 4. **Update the Context** Modify the JSON context data to see how it affects the results. ## Working with Context Data ### **JSON Path Detection** Click anywhere in the context editor to see the JSON path for that location. This helps you understand how to reference data in your expressions. ### **Sample Data Structure** The playground includes sample data with: ```json { "users": [ { "name": "Alice", "age": 28, "active": true, "department": "Engineering" }, { "name": "Bob", "age": 32, "active": false, "department": "Sales" }, // ... more users ], "products": [ { "name": "Laptop", "price": 999.99, "category": "Electronics", "inStock": true }, // ... more products ], "settings": { "theme": "dark", "language": "en", "notifications": true } } ``` ## Expression Examples ### **Filter and Transform Users** ``` // Get active engineering users users|filter('value.active && value.department == "Engineering"')|map('value.name') // Count users by department users|groupBy('value.department')|entries|map('[value.key, length(value.value)]')|toObject ``` ### **Work with Products** ``` // Find expensive in-stock items products|filter('value.price > 500 && value.inStock')|sort('value.price') // Calculate total inventory value products|filter('value.inStock')|map('value.price')|sum ``` ### **String Processing** ``` // Format user display names users|map('value.name | uppercase | pad(15) + " - " + value.department') // Create email addresses users|map('value.name | lowercase | replace(" ", ".") + "@company.com"') ``` ### **Mathematical Operations** ``` // Calculate statistics [users|map('value.age')|average, users|map('value.age')|max, users|map('value.age')|min] // Generate random sample users|shuffle|slice(0, 2) ``` ## Keyboard Shortcuts * **Ctrl/Cmd + Enter** - Evaluate expression * **Ctrl/Cmd + S** - Save current session * **Ctrl/Cmd + /** - Toggle comments (in editors) * **F1** - Show command palette (Monaco) * **Ctrl/Cmd + F** - Find in editor ## Tips & Best Practices ### **💡 Learning Tips** 1. **Start with Examples** - Use the examples library to learn patterns 2. **Use IntelliSense** - Press Ctrl+Space to see available functions 3. **Read Hover Docs** - Hover over function names for documentation 4. **Experiment Incrementally** - Build complex expressions step by step ### **🔧 Troubleshooting** **Expression Not Working?** * Check the error message in the output panel * Verify your JSON context is valid * Use the JSON path indicator to reference data correctly * Try simpler expressions first **Performance Issues?** * Large datasets may slow evaluation * Use filters early in expression chains * Consider breaking complex expressions into steps **Context Data Problems?** * Ensure JSON is properly formatted * Check for trailing commas or syntax errors * Use the browser's JSON validator ### **🎯 Expression Best Practices** 1. **Filter Early**: `data|filter(condition)|map(transform)` is faster than `data|map(transform)|filter(condition)` 2. **Use Appropriate Functions**: * `find` instead of `filter` for single items * `any`/`all` instead of `filter` + `length` for boolean checks 3. **Chain Logically**: Build expressions that read naturally left-to-right 4. **Handle Edge Cases**: Check for null/undefined values when needed ## Saving and Sharing ### **Auto-Save** * Your work is automatically saved locally * Restored when you return (within 24 hours) * No account required ### **Named Sessions** 1. Click "Save Session" in the left panel 2. Enter a name and optional description 3. Access saved sessions anytime 4. Export sessions as JSON for sharing ### **Sharing Expressions** * Copy the expression text to share with others * Export context data along with expressions * Use GitHub gists for longer examples ## Advanced Features ### **Custom Context** Replace the sample data with your own: ```json { "myData": [ // Your data here ], "config": { // Your configuration } } ``` ### **Complex Expressions** Build sophisticated data transformations: ``` // Multi-step aggregation users |groupBy('value.department') |entries |map('{ "department": value.key, "count": length(value.value), "avgAge": value.value|map("value.age")|average|round(1), "activeCount": value.value|filter("value.active")|length }') |sort('value.count') ``` ### **Error Handling** Test expressions with edge cases: ``` // Safe property access users|map('value.profile?.bio || "No bio available"') // Handle missing data products|filter('value.price != null && value.price > 0') ``` ## Browser Compatibility The playground works in all modern browsers: * Chrome 80+ * Firefox 80+ * Safari 14+ * Edge 80+ ## Technical Details * Built with React and TypeScript * Monaco Editor for code editing * Real-time expression evaluation * Local storage for session persistence * Responsive design for mobile devices ## Need Help? * 📖 [Language Reference](./language/) - Complete JEXL syntax guide * 🔍 [Function Reference](./reference/) - Documentation for all functions * 💬 [GitHub Issues](https://github.com/konnektr-io/jexl-extended/issues) - Report bugs or ask questions * 🌟 [GitHub Discussions](https://github.com/konnektr-io/jexl-extended/discussions) - Community support Happy exploring! The playground is the perfect place to learn JEXL Extended and prototype your expressions before using them in your applications. # KtrlPlane Documentation Welcome to the KtrlPlane documentation. KtrlPlane is the **Control Plane** of the Konnektr Platform - a centralized management system for users, organizations, projects, resources, billing, and RBAC. ## Getting Started If you're new to KtrlPlane, start with these guides: * [Quick Start Guide](/docs/ktrlplane/getting-started/quick-start) - Get up and running in minutes * [Creating Your First Project](/docs/ktrlplane/getting-started/first-project) - Step-by-step project creation * [Understanding Organizations](/docs/ktrlplane/getting-started/organizations) - Learn about the organizational structure ## Core Concepts Learn about the fundamental building blocks of KtrlPlane: * [Organizations](/docs/ktrlplane/concepts/organizations) - Top-level containers for your teams * [Projects](/docs/ktrlplane/concepts/projects) - Workspaces for your applications and resources * [Resources](/docs/ktrlplane/concepts/resources) - The actual services and applications you deploy * [Access Control](/docs/ktrlplane/concepts/access-control) - Managing permissions and roles ## User Guides Detailed guides for common tasks: * [Managing Organizations](/docs/ktrlplane/guides/organizations) - Create and configure organizations * [Working with Projects](/docs/ktrlplane/guides/projects) - Project lifecycle management * [Resource Management](/docs/ktrlplane/guides/resources) - Deploy and manage your resources * [Billing & Subscriptions](/docs/ktrlplane/guides/billing) - Payment and subscription management * [Access Control](/docs/ktrlplane/guides/access-control) - User permissions and role management ## API Reference Complete API documentation for developers: * [Authentication](/docs/ktrlplane/api/authentication) - API authentication and tokens * [Organizations API](/docs/ktrlplane/api/organizations) - Organization management endpoints * [Projects API](/docs/ktrlplane/api/projects) - Project management endpoints * [Resources API](/docs/ktrlplane/api/resources) - Resource management endpoints * [RBAC API](/docs/ktrlplane/api/rbac) - Access control endpoints * [Billing API](/docs/ktrlplane/api/billing) - Billing and subscription endpoints ## Self-Hosting For teams who want to host KtrlPlane themselves: * [Installation Guide](/docs/ktrlplane/self-hosting/installation) - Deploy KtrlPlane in your environment * [Configuration](/docs/ktrlplane/self-hosting/configuration) - Environment and system configuration * [Database Setup](/docs/ktrlplane/self-hosting/database) - PostgreSQL configuration and migrations * [Authentication Setup](/docs/ktrlplane/self-hosting/authentication) - Auth0 integration * [Deployment](/docs/ktrlplane/self-hosting/deployment) - Production deployment strategies * [Monitoring](/docs/ktrlplane/self-hosting/monitoring) - Logging, metrics, and health checks * [Troubleshooting](/docs/ktrlplane/self-hosting/troubleshooting) - Common issues and solutions ## Development For contributors and developers extending KtrlPlane: * [Development Setup](/docs/ktrlplane/development/setup) - Local development environment * [Architecture](/docs/ktrlplane/development/architecture) - System design and components * [Contributing](/docs/ktrlplane/development/contributing) - How to contribute to the project * [Testing](/docs/ktrlplane/development/testing) - Running and writing tests * [API Development](/docs/ktrlplane/development/api) - Extending the API ## Support * [FAQ](/docs/ktrlplane/support/faq) - Frequently asked questions * [Troubleshooting](/docs/ktrlplane/support/troubleshooting) - Common issues and solutions * [Community](/docs/ktrlplane/support/community) - Get help from the community # Context and Variables # Context and Variables JEXL expressions are evaluated against a **context** - a JavaScript object that provides the data and variables available to the expression. Understanding how context works is essential for writing effective JEXL expressions. ## What is Context? Context is the data environment in which a JEXL expression is evaluated. It's a JavaScript object that contains: * Variables and their values * Objects and their properties * Arrays and their elements * Functions (when applicable) ### Basic Context Example ```javascript // Context object const context = { name: "John Doe", age: 30, email: "john@example.com", scores: [85, 92, 78, 96], user: { profile: { displayName: "Johnny", preferences: { theme: "dark" } } } }; // JEXL expressions using this context name // "John Doe" age > 25 // true scores | length // 4 user.profile.displayName // "Johnny" ``` ## Variable Access ### Direct Variable Access Variables in the context root are accessed directly by name: ```javascript // Context: { firstName: "John", lastName: "Doe", age: 30 } firstName // "John" lastName // "Doe" age // 30 firstName + " " + lastName // "John Doe" ``` ### Case Sensitivity Variable names are case-sensitive: ```javascript // Context: { Name: "John", name: "Jane" } Name // "John" name // "Jane" NAME // undefined (would cause an error) ``` ### Special Variable Names Variables with special characters require bracket notation: ```javascript // Context: { "user-id": 123, "first name": "John", "2023-data": [...] } user-id // Error - interpreted as subtraction ["user-id"] // 123 - correct access ["first name"] // "John" ["2023-data"] // array data ``` ## Property Access ### Nested Object Access Access nested properties using dot notation: ```javascript // Context const context = { user: { personal: { name: "John", age: 30 }, work: { company: "Acme Corp", position: "Developer" } } }; // Property access user.personal.name // "John" user.work.company // "Acme Corp" user.personal.age > 25 // true ``` ### Dynamic Property Access Use bracket notation for dynamic property names: ```javascript // Context const context = { user: { name: "John", email: "john@example.com" }, propertyName: "email", fieldMap: { userName: "name", userEmail: "email" } }; // Dynamic access user[propertyName] // "john@example.com" user[fieldMap.userEmail] // "john@example.com" user["name"] // "John" ``` ## Array Context ### Array Element Access Access array elements by index: ```javascript // Context const context = { scores: [85, 92, 78, 96], users: [ { name: "Alice", age: 28 }, { name: "Bob", age: 32 }, { name: "Charlie", age: 24 } ] }; // Array access scores[0] // 85 scores[-1] // 96 (last element) users[1].name // "Bob" users[0].age // 28 ``` ### Array as Root Context When the context is an array itself: ```javascript // Context is directly an array: [1, 2, 3, 4, 5] [0] // 1 (first element) length // 5 sum // 15 filter("value > 3") // [4, 5] ``` ## Context Scope ### Global Context The main context object provides the global scope: ```javascript // Global context const context = { appName: "MyApp", version: "1.0.0", config: { debug: true }, users: [...] }; // All expressions can access global context appName // "MyApp" config.debug // true users | length // Number of users ``` ### Transform Context Within transforms, `value` refers to the current item being processed: ```javascript // Context: { numbers: [1, 2, 3, 4, 5] } // In map transform, 'value' is each number numbers | map("value * 2") // [2, 4, 6, 8, 10] // In filter transform, 'value' is each number being tested numbers | filter("value > 3") // [4, 5] // Global context still accessible numbers | map("value * multiplier") // Uses global 'multiplier' ``` ### Nested Transform Context In nested transforms, inner transforms can access outer context: ```javascript // Context: { users: [...], minAge: 21 } users | filter("value.age >= minAge") // Uses global minAge | map("value.name | uppercase") // Inner transform on name ``` ## Context Manipulation ### Adding Computed Properties You can reference computed values within expressions: ```javascript // Context: { firstName: "John", lastName: "Doe", scores: [85, 92, 78] } // Computed full name fullName = firstName + " " + lastName // Can't assign in JEXL, but conceptually // Use in expressions "Hello " + firstName + " " + lastName // "Hello John Doe" ``` ### Context Merging Merge objects to extend context: ```javascript // Base context const baseContext = { name: "John", age: 30 }; // Additional data const additionalData = { email: "john@example.com", role: "admin" }; // Merged context (done in JavaScript, not JEXL) const fullContext = { ...baseContext, ...additionalData }; // Now JEXL can access all properties name + " (" + role + ")" // "John (admin)" ``` ## Context Best Practices ### 1. Structure Context Logically ```javascript // Good - organized structure const context = { user: { personal: { name: "John", age: 30 }, work: { company: "Acme", role: "Dev" }, preferences: { theme: "dark" } }, app: { version: "1.0.0", features: ["auth", "reporting"] } }; // Clear access patterns user.personal.name app.version user.preferences.theme ``` ### 2. Use Consistent Naming ```javascript // Good - consistent camelCase const context = { firstName: "John", lastName: "Doe", emailAddress: "john@example.com", phoneNumber: "555-1234" }; // Avoid mixed naming styles const badContext = { first_name: "John", // snake_case LastName: "Doe", // PascalCase "email-address": "...", // kebab-case phoneNumber: "..." // camelCase }; ``` ### 3. Provide Safe Defaults ```javascript // Good - handle missing data const context = { user: user || {}, settings: settings || { theme: "light" }, permissions: permissions || [] }; // JEXL expressions can safely access user.name || "Anonymous" settings.theme permissions | length > 0 ``` ### 4. Avoid Deep Nesting ```javascript // Good - reasonable depth const context = { user: { profile: { name: "John", email: "..." }, settings: { theme: "dark" } } }; // Harder to work with - too deep const deepContext = { app: { modules: { user: { management: { profile: { personal: { details: { name: "John" // Too deep! } } } } } } } }; ``` ## Advanced Context Patterns ### Context with Functions While JEXL Extended provides built-in functions, you can add custom functions to context: ```javascript // Context with custom functions (JavaScript setup) const context = { data: [1, 2, 3], customMultiplier: 5, // Custom functions would be added at the JEXL level, not in context }; // Use JEXL Extended's built-in functions instead data | map("value * customMultiplier") ``` ### Dynamic Context Building Build context dynamically based on conditions: ```javascript // JavaScript context preparation const buildContext = (user, permissions) => { const context = { user: user || { name: "Guest" }, isAuthenticated: !!user, permissions: permissions || [] }; // Add computed properties if (user) { context.displayName = user.firstName + " " + user.lastName; context.initials = (user.firstName[0] + user.lastName[0]).toUpperCase(); } return context; }; // JEXL expressions using dynamic context isAuthenticated ? displayName : "Please log in" permissions | contains("admin") ? "Full Access" : "Limited Access" ``` ### Context Validation Validate context structure before evaluation: ```javascript // JavaScript validation const validateContext = (context) => { const required = ['user', 'settings', 'data']; const missing = required.filter(key => !(key in context)); if (missing.length > 0) { throw new Error(`Missing required context: ${missing.join(', ')}`); } return context; }; // Safe JEXL evaluation with validated context const safeContext = validateContext(rawContext); ``` ## Context in Different Scenarios ### API Response Processing ```javascript // API response as context const apiResponse = { status: "success", data: { users: [...], pagination: { page: 1, total: 100 } }, meta: { timestamp: "2023-01-01T00:00:00Z", version: "v1" } }; // JEXL expressions for API processing status == "success" && data.users | length > 0 data.pagination.page * 10 <= data.pagination.total meta.timestamp | dateTimeFormat("YYYY-MM-DD") ``` ### Form Validation Context ```javascript // Form data as context const formContext = { firstName: "John", lastName: "Doe", email: "john@example.com", age: 25, agreedToTerms: true, preferences: { newsletter: true, notifications: false } }; // Validation expressions firstName | trim | length > 0 email | contains("@") && email | contains(".") age >= 18 agreedToTerms == true ``` ### Configuration Context ```javascript // App configuration as context const configContext = { environment: "production", features: { darkMode: true, analytics: true, debugging: false }, limits: { maxUsers: 1000, maxFileSize: 10485760 // 10MB }, endpoints: { api: "https://api.example.com", auth: "https://auth.example.com" } }; // Configuration-based expressions features.darkMode ? "dark-theme" : "light-theme" environment == "development" && features.debugging limits.maxFileSize / 1048576 + "MB" // Convert to MB ``` ## Error Handling with Context ### Safe Property Access ```javascript // Safe access patterns user && user.profile && user.profile.name || "Unknown" "email" in user ? user.email : "No email provided" typeof user.age == "number" ? user.age : 0 ``` ### Context Existence Checks ```javascript // Check if context properties exist typeof users != "undefined" && users | length > 0 settings != null && "theme" in settings data && Array.isArray(data) && data | length > 0 ``` ### Default Context Values ```javascript // Provide defaults for missing context name || "Anonymous" settings.timeout || 5000 permissions || [] config.maxRetries || 3 ``` ## Context Debugging When debugging JEXL expressions, understanding the context is crucial: ### Context Inspection ```javascript // JavaScript debugging console.log('Context:', JSON.stringify(context, null, 2)); // Check specific paths console.log('User exists:', 'user' in context); console.log('User name:', context.user?.name); console.log('Data type:', typeof context.data); ``` ### Expression Testing ```javascript // Test expressions with different context values const testContexts = [ { user: { name: "John" } }, { user: {} }, { user: null }, {} ]; testContexts.forEach(ctx => { try { const result = jexl.evalSync('user.name || "Unknown"', ctx); console.log('Result:', result); } catch (error) { console.error('Error:', error.message); } }); ``` Understanding context and variables is fundamental to mastering JEXL. The context provides the data foundation for all expressions, and proper context design makes expressions more powerful and maintainable. Next: Explore practical [Usage Guides](../usage/) to see how context works in real applications. # Data Types # JEXL Data Types JEXL supports all JavaScript data types and provides seamless integration with JavaScript's dynamic typing system. Understanding how JEXL handles different data types is crucial for writing effective expressions. ## Primitive Types ### String Strings represent textual data and are enclosed in single or double quotes. ```javascript // String literals "hello world" 'single quotes' "embedded 'quotes'" 'embedded "quotes"' // Escape sequences "line 1\nline 2" // Newline "tab\tseparated" // Tab "quote: \"hello\"" // Escaped quote "backslash: \\" // Escaped backslash ``` **String Operations:** ```javascript // Concatenation "hello" + " " + "world" // "hello world" // Length length("hello") // 5 // Case conversion "Hello" | uppercase // "HELLO" "Hello" | lowercase // "hello" // Substring operations "hello world" | substring(0, 5) // "hello" "hello world" | split(" ") // ["hello", "world"] ``` ### Number Numbers can be integers, decimals, or scientific notation. ```javascript // Integer literals 42 -17 0 // Decimal literals 3.14159 -2.5 0.001 // Scientific notation 1.23e5 // 123000 4.56e-3 // 0.00456 // Special values Infinity -Infinity ``` **Number Operations:** ```javascript // Arithmetic 10 + 5 // 15 10 - 3 // 7 10 * 2 // 20 10 / 3 // 3.333... 10 % 3 // 1 2 ^ 3 // 8 // Math functions abs(-5) // 5 round(3.7) // 4 floor(3.7) // 3 ceil(3.2) // 4 max([1,2,3]) // 3 min([1,2,3]) // 1 ``` ### Boolean Booleans represent logical true/false values. ```javascript // Boolean literals true false // Boolean expressions age >= 18 // true/false name == "John" // true/false !isActive // negation active && verified // logical AND admin || moderator // logical OR ``` **Truthiness in JEXL:** * `true` → true * `false` → false * `0` → false * `""` (empty string) → false * `null` → false * `undefined` → false * Everything else → true ### Null Represents the absence of a value. ```javascript // Null literal null // Null checks value == null // Check if null value != null // Check if not null // Default values name || "Anonymous" // Use "Anonymous" if name is null/empty ``` ## Complex Types ### Array Arrays are ordered collections of values. ```javascript // Array literals [] // Empty array [1, 2, 3] // Number array ["a", "b", "c"] // String array [true, 1, "mixed"] // Mixed types [user.name, user.age] // Expression elements ``` **Array Access:** ```javascript // Index access users[0] // First element users[-1] // Last element users[1].name // Property of array element // Dynamic access users[index] // Using variable ``` **Array Operations:** ```javascript // Length length([1, 2, 3]) // 3 // Transform operations [1, 2, 3] | map("value * 2") // [2, 4, 6] [1, 2, 3, 4] | filter("value > 2") // [3, 4] ["c", "a", "b"] | sort // ["a", "b", "c"] // Aggregation [1, 2, 3, 4] | sum // 10 [1, 2, 3, 4] | average // 2.5 [1, 2, 3, 4] | max // 4 // Array manipulation [1, 2] | append(3) // [1, 2, 3] [1, 2, 3] | reverse // [3, 2, 1] [1, 2, 2, 3] | distinct // [1, 2, 3] ``` ### Object Objects are collections of key-value pairs. ```javascript // Object literals {} // Empty object {name: "John", age: 30} // Simple object {x: 1, y: 2, sum: x + y} // Computed values {"key-with-dash": value} // Quoted keys {[dynamicKey]: value} // Computed keys ``` **Object Access:** ```javascript // Property access user.name // Dot notation user["name"] // Bracket notation user[propertyName] // Dynamic property // Nested access user.profile.email user.settings.theme.dark ``` **Object Operations:** ```javascript // Object inspection keys({a: 1, b: 2}) // ["a", "b"] values({a: 1, b: 2}) // [1, 2] entries({a: 1, b: 2}) // [["a", 1], ["b", 2]] // Object merging merge({a: 1}, {b: 2}) // {a: 1, b: 2} // Property existence "name" in user // true if property exists ``` ## Type Conversion JEXL handles automatic type conversion in many contexts. ### String Conversion ```javascript // Automatic string conversion "Score: " + 95 // "Score: 95" "Items: " + 3 // "Items: 3" // Explicit conversion string(42) // "42" string(true) // "true" string([1, 2, 3]) // "1,2,3" ``` ### Number Conversion ```javascript // Automatic number conversion "10" * 2 // 20 "3.14" + 1 // 4.14 // Explicit conversion number("42") // 42 number("3.14") // 3.14 number(true) // 1 number(false) // 0 // Parsing integers parseInteger("42") // 42 parseInteger("42.7") // 42 parseInteger("42px") // 42 ``` ### Boolean Conversion ```javascript // Automatic boolean conversion !!"hello" // true !0 // true !"" (empty string) // true // Explicit conversion boolean(1) // true boolean(0) // false boolean("hello") // true boolean("") // false ``` ## Working with JSON JEXL can parse and generate JSON strings. ```javascript // Parse JSON json('{"name": "John", "age": 30}') // {name: "John", age: 30} // Generate JSON (automatic) {name: "John", age: 30} // Becomes JSON when serialized ``` ## Type Checking While JEXL is dynamically typed, you can check types when needed. ```javascript // Type-specific operations indicate type length(value) // Works with strings and arrays keys(value) // Works with objects value + "" // Convert to string value * 1 // Convert to number !!value // Convert to boolean ``` ## Special Values ### Undefined vs Null ```javascript // Undefined properties user.nonexistentProperty // undefined // Explicit null user.optionalField = null // null // Both are falsy !undefined // true !null // true ``` ### Infinity and NaN ```javascript // Infinity 1 / 0 // Infinity -1 / 0 // -Infinity // Not a Number (NaN) "hello" * 2 // NaN sqrt(-1) // NaN ``` ## Array vs Object Distinction Understanding when to use arrays vs objects: ### Use Arrays When: * You have ordered data * You need indexed access * You want to use array transforms (`map`, `filter`, etc.) ```javascript scores = [85, 92, 78, 96] scores | average // Calculate average scores | filter("value > 80") // Filter high scores ``` ### Use Objects When: * You have key-value relationships * You need named properties * You want to group related data ```javascript user = {name: "John", age: 30, email: "john@example.com"} user.name // Access by property name keys(user) // Get all property names ``` ## Common Type Patterns ### Safe Property Access ```javascript // Handle missing properties user.profile?.email || "No email" user && user.profile && user.profile.email // Using in operator "email" in user ? user.email : "No email" ``` ### Type-Safe Operations ```javascript // Ensure array before using array operations length(users) > 0 ? users | map("value.name") : [] // Ensure string before string operations typeof name == "string" ? name | uppercase : name ``` ### Default Values ```javascript // Provide defaults for missing values name || "Anonymous" age || 0 tags || [] settings || {} ``` ## Best Practices ### 1. Be Explicit with Types ```javascript // Good - clear intent string(value) | uppercase // Less clear - relies on conversion value + "" | uppercase ``` ### 2. Handle Missing Data ```javascript // Good - safe access users && users | length > 0 ? users[0].name : "No users" // Risky - might throw error users[0].name ``` ### 3. Use Appropriate Data Structures ```javascript // Good - use array for ordered data scores | sort | reverse // Good - use object for key-value data settings.theme.primaryColor ``` Next: Learn about [JEXL Operators](./operators) and how they work with different data types. # Expressions # JEXL Expressions JEXL expressions are the heart of the language, combining literals, variables, operators, functions, and transforms to create powerful data processing pipelines. This guide covers how to build and structure complex expressions effectively. ## Expression Fundamentals ### Simple Expressions ```javascript // Literal values 42 "hello world" true null // Variable access name user.email scores[0] ``` ### Complex Expressions ```javascript // Arithmetic with variables (score1 + score2 + score3) / 3 // String manipulation firstName + " " + lastName | uppercase // Conditional logic age >= 18 ? "adult" : "minor" ``` ## Function Calls Functions perform operations and return values. JEXL Extended provides 80+ built-in functions. ### Basic Function Calls ```javascript // Single argument length("hello") // 5 abs(-10) // 10 uppercase("hello") // "HELLO" // Multiple arguments max([1, 5, 3, 9, 2]) // 9 substring("hello world", 0, 5) // "hello" contains("hello world", "world") // true ``` ### Nested Function Calls ```javascript // Functions within functions length(split("a,b,c", ",")) // 3 max(map([1, 2, 3], "value * 2")) // 6 round(average([1.1, 2.7, 3.9]), 2) // 2.57 ``` ### Functions with Complex Arguments ```javascript // Using expressions as arguments filter(users, "value.age > " + minAge) map(items, "value.price * " + taxRate) sort(products, "value.priority == 'high' ? 1 : 2") ``` ## Transform Operations Transforms use the pipe operator (`|`) to create data processing pipelines. ### Single Transforms ```javascript " hello world " | trim // "hello world" [1, 2, 3, 4, 5] | length // 5 {a: 1, b: 2, c: 3} | keys // ["a", "b", "c"] ``` ### Transform Chains ```javascript // String processing pipeline text | trim | lowercase | split(" ") | join("-") // Array processing pipeline numbers | filter("value > 0") | map("value * 2") | sort | reverse // Mixed data processing users | filter("value.active") | map("value.email") | distinct | sort ``` ### Transforms with Arguments ```javascript // Transform functions with parameters "hello world" | substring(0, 5) // "hello" [1, 2, 3, 4] | filter("value > 2") // [3, 4] "apple,banana,cherry" | split(",") // ["apple", "banana", "cherry"] ``` ## Conditional Expressions ### Ternary Operator ```javascript // Simple conditions status = isActive ? "online" : "offline" message = count == 1 ? "1 item" : count + " items" // Nested conditions grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F" ``` ### Logical Operations for Conditions ```javascript // Multiple conditions canVote = age >= 18 && citizenship == "US" && registered == true hasAccess = isAdmin || (isMember && subscription.active) // Short-circuit evaluation userName = user && user.profile && user.profile.name || "Anonymous" ``` ## Array Operations ### Array Creation and Manipulation ```javascript // Creating arrays scores = [95, 87, 92, 78, 88] names = ["Alice", "Bob", "Charlie"] mixed = [user.name, user.age, user.active] // Array transformations highScores = scores | filter("value > 85") // [95, 87, 92, 88] upperNames = names | map("value | uppercase") // ["ALICE", "BOB", "CHARLIE"] sortedScores = scores | sort | reverse // [95, 92, 88, 87, 78] ``` ### Array Aggregations ```javascript // Statistical operations totalScore = scores | sum // 440 averageScore = scores | average // 88 highestScore = scores | max // 95 lowestScore = scores | min // 78 // Array analysis uniqueValues = data | distinct itemCount = items | length hasHighScores = scores | any("value > 90") // true allPassing = scores | all("value >= 60") // true ``` ### Advanced Array Processing ```javascript // Group and process usersByDepartment = users | groupBy("value.department") departmentCounts = usersByDepartment | map("length(value)") // Find operations firstAdult = users | find("value.age >= 18") adminIndex = users | findIndex("value.role == 'admin'") // Array reduction concatenated = strings | reduce("acc + value", "") product = numbers | reduce("acc * value", 1) ``` ## Object Operations ### Object Creation and Access ```javascript // Creating objects user = { name: firstName + " " + lastName, age: currentYear - birthYear, isAdult: age >= 18, email: name | lowercase | replace(" ", ".") + "@company.com" } // Dynamic property access property = "email" userEmail = user[property] ``` ### Object Transformation ```javascript // Extract object information userKeys = user | keys // ["name", "age", "isAdult", "email"] userValues = user | values // ["John Doe", 30, true, "john.doe@company.com"] userEntries = user | entries // [["name", "John Doe"], ...] // Object merging defaults = {theme: "light", timeout: 5000} userPrefs = {theme: "dark"} settings = merge(defaults, userPrefs) // {theme: "dark", timeout: 5000} ``` ## String Processing ### String Manipulation Chains ```javascript // Clean and format text cleanTitle = rawTitle | trim | lowercase | replace(/[^a-z0-9\s]/g, "") | split(" ") | join("-") // Text analysis wordCount = text | split(" ") | length hasKeyword = text | lowercase | contains(searchTerm | lowercase) // String formatting formatted = template | replace("{name}", user.name) | replace("{date}", now() | dateTimeFormat("YYYY-MM-DD")) ``` ### String Validation ```javascript // Email validation pattern isValidEmail = email | contains("@") && email | contains(".") && length(email) > 5 // Password strength isStrongPassword = password | length >= 8 && password | contains(/[A-Z]/) && password | contains(/[a-z]/) && password | contains(/[0-9]/) ``` ## Mathematical Expressions ### Calculations ```javascript // Complex calculations totalPrice = items | map("value.price * value.quantity") | sum taxAmount = totalPrice * taxRate finalPrice = totalPrice + taxAmount // Statistics variance = numbers | map("(value - " + (numbers | average) + ") ^ 2") | average standardDeviation = sqrt(variance) // Financial calculations monthlyPayment = principal * (rate * (1 + rate)^months) / ((1 + rate)^months - 1) ``` ### Mathematical Functions ```javascript // Trigonometry and advanced math hypotenuse = sqrt(a^2 + b^2) area = 3.14159 * radius^2 compound = principal * (1 + rate/periods)^(periods * years) // Rounding and formatting formatted = value | round(2) | formatNumber("$#,##0.00") percentage = (value / total * 100) | round(1) + "%" ``` ## Date and Time Operations ### Date Calculations ```javascript // Current time operations currentTime = now() currentMillis = millis() formatted = currentTime | dateTimeFormat("YYYY-MM-DD HH:mm:ss") // Date arithmetic futureDate = currentTime | dateTimeAdd("days", 30) pastDate = currentTime | dateTimeAdd("months", -6) // Time comparisons isRecent = (now() | dateTimeToMillis) - (createdDate | dateTimeToMillis) < 86400000 // 24 hours age = (now() | dateTimeToMillis) - (birthDate | dateTimeToMillis) | millisToDateTime | dateTimeFormat("YYYY") ``` ## Error Handling and Safety ### Safe Property Access ```javascript // Null-safe operations userName = user && user.profile && user.profile.name || "Unknown" email = user?.profile?.contact?.email || "No email" // Checking existence hasEmail = "email" in user && user.email != null && user.email != "" validUser = user != null && "name" in user && "id" in user ``` ### Type Safety ```javascript // Type checking before operations safeLength = typeof value == "string" || Array.isArray(value) ? length(value) : 0 safeUppercase = typeof text == "string" ? text | uppercase : text // Default values for different types safeName = typeof name == "string" && name | trim | length > 0 ? name | trim : "Unknown" safeNumber = typeof num == "number" && !isNaN(num) ? num : 0 ``` ## Expression Composition ### Building Complex Logic ```javascript // User eligibility check isEligible = user.age >= minAge && user.status == "active" && user.credits >= requiredCredits && user.lastLogin | dateTimeToMillis >= cutoffDate | dateTimeToMillis // Data processing pipeline result = rawData | filter("value != null && value.id != null") | map("merge(value, {processedAt: now()})") | sort("value.priority") | filter("value.category in allowedCategories") | map("transform(value)") ``` ### Reusable Sub-expressions ```javascript // Define common calculations taxRate = config.taxRate || 0.08 shippingCost = weight > 50 ? 25 : weight > 20 ? 15 : 5 discount = membership == "premium" ? 0.15 : membership == "standard" ? 0.10 : 0 // Use in main calculation finalPrice = (basePrice * (1 - discount) * (1 + taxRate)) + shippingCost ``` ## Performance Considerations ### Efficient Expression Structure ```javascript // Good - filter early, transform late result = largeArray | filter("value.active && value.score > threshold") | map("complexTransformation(value)") // Less efficient - transform everything first result = largeArray | map("complexTransformation(value)") | filter("value.active && value.score > threshold") ``` ### Minimize Function Calls ```javascript // Good - calculate once currentTime = now() recent = items | filter("value.timestamp > " + (currentTime - 86400000)) // Less efficient - calculate repeatedly recent = items | filter("value.timestamp > " + (now() - 86400000)) ``` ## Common Expression Patterns ### Data Validation ```javascript // Form validation isValidForm = name | trim | length > 0 && email | contains("@") && age >= 18 && termsAccepted == true // Data completeness isComplete = requiredFields | all("value in data && data[value] != null") ``` ### Data Transformation ```javascript // API response transformation apiResponse = rawData | map("{ id: value.id, name: value.full_name | trim, email: value.email_address | lowercase, isActive: value.status == 'active', lastSeen: value.last_login | dateTimeFormat('YYYY-MM-DD') }") ``` ### Aggregation and Reporting ```javascript // Sales report report = { totalSales: orders | sum("value.amount"), averageOrder: orders | average("value.amount"), topCustomer: orders | groupBy("value.customerId") | map("sum(value, 'amount')") | max, ordersByStatus: orders | groupBy("value.status") | map("length(value)") } ``` ### Search and Filtering ```javascript // Advanced search searchResults = products | filter(" (searchTerm == null || value.name | lowercase | contains(searchTerm | lowercase)) && (minPrice == null || value.price >= minPrice) && (maxPrice == null || value.price <= maxPrice) && (category == null || value.category == category) ") | sort("value.relevanceScore") | map("merge(value, {highlighted: highlightMatches(value.name, searchTerm)})") ``` ## Best Practices ### 1. Use Meaningful Names ```javascript // Good isEligibleCustomer = customer.age >= 18 && customer.creditScore > 600 customerFullName = customer.firstName + " " + customer.lastName // Less clear result = c.a >= 18 && c.cs > 600 name = c.fn + " " + c.ln ``` ### 2. Break Complex Expressions ```javascript // Good - readable steps eligibilityAge = customer.age >= minimumAge eligibilityCredit = customer.creditScore >= minimumCredit eligibilityStatus = customer.status == "active" isEligible = eligibilityAge && eligibilityCredit && eligibilityStatus // Harder to read isEligible = customer.age >= minimumAge && customer.creditScore >= minimumCredit && customer.status == "active" ``` ### 3. Use Comments for Complex Logic ```javascript // Calculate compound interest: P(1 + r/n)^(nt) futureValue = principal * (1 + annualRate / compoundingsPerYear) ^ (compoundingsPerYear * years) // Filter active users who logged in within the last 30 days activeUsers = users | filter("value.status == 'active' && (now() - value.lastLogin) < 2592000000") ``` ### 4. Validate Inputs ```javascript // Good - safe processing result = input != null && typeof input == "string" ? input | trim | lowercase | split(",") | map("value | trim") : [] // Risky - assumes input is valid result = input | trim | lowercase | split(",") | map("value | trim") ``` Next: Learn about [Context and Variables](./context) in JEXL expressions. # Language Guide # JEXL Language Guide JEXL (JavaScript Expression Language) is a powerful expression language for evaluating expressions within JSON structures and JavaScript applications. This guide covers the complete JEXL syntax and language features. ## What is JEXL? JEXL provides a simple, yet powerful syntax for: * **Data transformation** - Transform objects and arrays * **Filtering** - Select data based on conditions * **Calculations** - Perform mathematical operations * **String manipulation** - Process and format text * **Conditional logic** - Make decisions in expressions ## Language Sections ### [Syntax Overview](./syntax) Learn the basic JEXL syntax, expression structure, and evaluation rules. ### [Data Types](./data-types) Understand JEXL's data types: strings, numbers, booleans, arrays, objects, and null. ### [Operators](./operators) Complete reference for all JEXL operators including arithmetic, comparison, logical, and more. ### [Expressions](./expressions) Learn how to write complex expressions using functions, transforms, and nested operations. ### [Context and Variables](./context) Understand how JEXL accesses data through context variables and property paths. ## Quick Examples ### Basic Expression ```javascript // Simple property access name // Returns the 'name' property from context ``` ### Array Operations ```javascript // Filter and transform array users|filter("value.age > 21")|map("value.name") // Filters users by age, then extracts names ``` ### Mathematical Calculations ```javascript // Calculate percentage (score / maxScore) * 100 // Calculates percentage from score and maxScore ``` ### String Processing ```javascript // Clean and format text title | trim | lowercase | replace(" ", "-") // Cleans title and converts to slug format ``` ### Conditional Logic ```javascript // Conditional expression age >= 18 ? "adult" : "minor" // Returns status based on age ``` ## Key Concepts ### Expressions vs Statements JEXL uses **expressions** that always return a value, not statements. Every JEXL expression evaluates to a result. ### Context-Driven JEXL expressions are evaluated against a **context** - a JavaScript object that provides the data for the expression. ### Functional Style JEXL encourages a functional programming style with transforms (pipe operations) that chain data transformations. ### Type Flexibility JEXL handles JavaScript's dynamic typing, automatically converting between types when needed. ## Getting Started 1. **[Start with Syntax](./syntax)** - Learn the basic JEXL syntax 2. **[Explore Data Types](./data-types)** - Understand how JEXL handles different data types 3. **[Master Operators](./operators)** - Learn all available operators 4. **[Practice Expressions](./expressions)** - Build complex expressions 5. **[Understand Context](./context)** - Learn how data flows through expressions Ready to dive in? Start with the [Syntax Overview](./syntax)! # Operators # JEXL Operators JEXL provides a comprehensive set of operators for performing calculations, comparisons, logical operations, and data transformations. Understanding operator precedence and behavior is essential for writing correct expressions. ## Arithmetic Operators Perform mathematical calculations on numbers. ### Addition (`+`) ```javascript 5 + 3 // 8 1.5 + 2.5 // 4.0 -10 + 5 // -5 // String concatenation "Hello" + " " + "World" // "Hello World" "Score: " + 95 // "Score: 95" ``` ### Subtraction (`-`) ```javascript 10 - 3 // 7 5.5 - 2.2 // 3.3 0 - 5 // -5 ``` ### Multiplication (`*`) ```javascript 4 * 3 // 12 2.5 * 4 // 10.0 -3 * 2 // -6 ``` ### Division (`/`) ```javascript 10 / 2 // 5 7 / 3 // 2.333... 10 / 0 // Infinity ``` ### Modulus (`%`) ```javascript 10 % 3 // 1 7 % 2 // 1 12 % 4 // 0 ``` ### Exponentiation (`^`) ```javascript 2 ^ 3 // 8 4 ^ 0.5 // 2 (square root) 10 ^ 2 // 100 ``` ### Unary Minus (`-`) ```javascript -5 // -5 -(3 + 2) // -5 -(-5) // 5 ``` ## Comparison Operators Compare values and return boolean results. ### Equality (`==`) ```javascript 5 == 5 // true "hello" == "hello" // true true == true // true 5 == "5" // true (type coercion) null == null // true ``` ### Inequality (`!=`) ```javascript 5 != 3 // true "a" != "b" // true true != false // true 5 != "5" // false (type coercion) ``` ### Less Than (`<`) ```javascript 3 < 5 // true "a" < "b" // true (lexicographic) 10 < 10 // false ``` ### Less Than or Equal (`<=`) ```javascript 3 <= 5 // true 5 <= 5 // true 10 <= 9 // false ``` ### Greater Than (`>`) ```javascript 5 > 3 // true "b" > "a" // true 10 > 10 // false ``` ### Greater Than or Equal (`>=`) ```javascript 5 >= 3 // true 5 >= 5 // true 3 >= 5 // false ``` ## Logical Operators Perform logical operations and combine boolean expressions. ### Logical AND (`&&`) ```javascript true && true // true true && false // false false && true // false false && false // false // Short-circuit evaluation age >= 18 && hasLicense // Only checks hasLicense if age >= 18 user && user.name // Safe property access ``` ### Logical OR (`||`) ```javascript true || true // true true || false // true false || true // true false || false // false // Default values name || "Anonymous" // Use "Anonymous" if name is falsy config.timeout || 5000 // Default timeout ``` ### Logical NOT (`!`) ```javascript !true // false !false // true !"hello" // false (truthy string) !"" // true (empty string is falsy) !0 // true (zero is falsy) !null // true (null is falsy) ``` ## Membership Operator Test if a value exists within another value. ### In Operator (`in`) ```javascript // Array membership "apple" in ["apple", "banana", "orange"] // true 5 in [1, 2, 3, 4, 5] // true // Object property existence "name" in {name: "John", age: 30} // true "email" in {name: "John", age: 30} // false // String substring "ell" in "hello" // true "xyz" in "hello" // false ``` ## Conditional (Ternary) Operator Provide conditional logic with the `? :` operator. ### Basic Ternary (`? :`) ```javascript age >= 18 ? "adult" : "minor" score >= 60 ? "pass" : "fail" user ? user.name : "Guest" ``` ### Nested Ternary ```javascript // Grade calculation score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F" // Status determination errors > 0 ? "error" : warnings > 0 ? "warning" : "success" ``` ## Transform (Pipe) Operator Apply functions to values using the pipe operator (`|`). ### Basic Transforms ```javascript "hello" | uppercase // "HELLO" [1, 2, 3] | length // 3 " text " | trim // "text" ``` ### Chained Transforms ```javascript // String processing " Hello World " | trim | lowercase | split(" ") | join("-") // Result: "hello-world" // Array processing [1, 2, 3, 4, 5] | filter("value > 2") | map("value * 2") | sum // Result: 24 (sum of [6, 8, 10]) ``` ### Transforms with Arguments ```javascript "hello world" | substring(0, 5) // "hello" [1, 2, 3] | map("value * " + multiplier) // Multiply by variable users | filter("value.age > " + minAge) // Filter with variable ``` ## Property Access Operators Access properties and array elements. ### Dot Notation (`.`) ```javascript user.name // Simple property user.profile.email // Nested property config.database.host // Deep nesting ``` ### Bracket Notation (`[]`) ```javascript user["name"] // Property access user[propertyName] // Dynamic property array[0] // Array index array[index] // Dynamic index array[-1] // Negative index (last element) ``` ## Operator Precedence Operators are evaluated in the following order (highest to lowest precedence): | Precedence | Operators | Description | Associativity | | ---------- | ----------------- | --------------------------------- | ------------- | | 1 | `.` `[]` `()` | Property access, function calls | Left-to-right | | 2 | `!` `-` (unary) | Logical NOT, unary minus | Right-to-left | | 3 | `^` | Exponentiation | Right-to-left | | 4 | `*` `/` `%` | Multiplication, division, modulus | Left-to-right | | 5 | `+` `-` | Addition, subtraction | Left-to-right | | 6 | `<` `<=` `>` `>=` | Comparison | Left-to-right | | 7 | `==` `!=` | Equality | Left-to-right | | 8 | `in` | Membership | Left-to-right | | 9 | `&&` | Logical AND | Left-to-right | | 10 | `\|\|` | Logical OR | Left-to-right | | 11 | `? :` | Ternary conditional | Right-to-left | | 12 | `\|` | Transform (pipe) | Left-to-right | ### Precedence Examples ```javascript // Arithmetic precedence 2 + 3 * 4 // 14, not 20 (multiplication first) (2 + 3) * 4 // 20 (parentheses override) // Comparison and logical precedence age > 18 && hasLicense // Comparison first, then AND !(age > 18) // NOT applied to comparison result // Transform precedence users | length > 0 // Transform first: (users | length) > 0 users | (length > 0) // Would be invalid syntax ``` ## Associativity When operators have the same precedence, associativity determines evaluation order. ### Left-to-right (Left Associative) ```javascript 10 - 5 - 2 // (10 - 5) - 2 = 3 a | b | c // (a | b) | c ``` ### Right-to-left (Right Associative) ```javascript 2 ^ 3 ^ 2 // 2 ^ (3 ^ 2) = 2 ^ 9 = 512 a ? b : c ? d : e // a ? b : (c ? d : e) ``` ## Operator Overloading Some operators work differently based on operand types. ### Addition (`+`) ```javascript // Numeric addition 5 + 3 // 8 // String concatenation "Hello" + " World" // "Hello World" // Mixed types (converts to string) "Count: " + 5 // "Count: 5" ``` ### Comparison with Type Coercion ```javascript // Numeric comparison 5 > 3 // true // String comparison (lexicographic) "b" > "a" // true "10" > "2" // false (string comparison) // Mixed type comparison "10" > 2 // true (converts to number) ``` ## Best Practices ### 1. Use Parentheses for Clarity ```javascript // Good - clear intent (score >= 90) && (attendance >= 0.8) // Less clear - relies on precedence score >= 90 && attendance >= 0.8 ``` ### 2. Avoid Complex Precedence Mixing ```javascript // Good - explicit grouping result = (a + b) * (c - d) // Harder to read result = a + b * c - d ``` ### 3. Use Meaningful Variable Names in Complex Expressions ```javascript // Good isEligibleStudent = (age >= 18) && (gpa >= 3.0) && (creditsCompleted >= 60) // Less readable result = a >= 18 && b >= 3.0 && c >= 60 ``` ### 4. Break Down Complex Ternary Operations ```javascript // Good - readable chain grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F" // Harder to read - nested ternary grade = score >= 90 ? "A" : (score >= 80 ? "B" : (score >= 70 ? "C" : (score >= 60 ? "D" : "F"))) ``` ### 5. Use Short-Circuit Evaluation Safely ```javascript // Good - safe property access user && user.profile && user.profile.email // Good - provide defaults config.timeout || 5000 // Be careful with falsy values count || 0 // Wrong if count should be 0 count != null ? count : 0 // Safer ``` ## Common Patterns ### Safe Navigation ```javascript // Check existence before access user && user.address && user.address.city // Using in operator "address" in user && "city" in user.address ? user.address.city : null ``` ### Default Value Assignment ```javascript // Simple defaults name = inputName || "Anonymous" timeout = config.timeout || 5000 // More specific defaults port = config.port != null ? config.port : 3000 ``` ### Range Checks ```javascript // Age range isValidAge = age >= 0 && age <= 120 // Score range isValidScore = score >= 0 && score <= 100 ``` ### Type Checking with Operations ```javascript // Check if numeric isNumber = typeof value == "number" && !isNaN(value) // Check if non-empty string isValidString = typeof value == "string" && value.length > 0 ``` Next: Learn how to build complex [JEXL Expressions](./expressions) using these operators. # Syntax Overview # JEXL Syntax Overview JEXL (JavaScript Expression Language) uses a clean, readable syntax that combines the best features of JavaScript expressions with powerful data transformation capabilities. ## Basic Structure A JEXL expression consists of: * **Identifiers** - Variable and property names * **Literals** - Fixed values (strings, numbers, booleans, etc.) * **Operators** - Symbols that perform operations * **Functions** - Callable procedures that return values * **Transforms** - Functions applied via the pipe operator (`|`) ## Identifiers and Property Access ### Simple Identifiers ```javascript name // Access 'name' property age // Access 'age' property isActive // Access 'isActive' property ``` ### Nested Property Access ```javascript user.name // Access nested property user.profile.email // Access deeply nested property company.address.city // Multiple levels of nesting ``` ### Array Access ```javascript users[0] // Access first element users[0].name // Access property of array element scores[-1] // Access last element (negative indexing) ``` ### Dynamic Property Access ```javascript user[propertyName] // Use variable as property name data["complex-key"] // Access property with special characters settings[configKey] // Dynamic configuration access ``` ## Literals ### String Literals ```javascript "hello world" // Double quotes 'hello world' // Single quotes "It's working" // Escaping with different quotes 'She said "hi"' // Escaping with different quotes "Line 1\nLine 2" // Escape sequences ``` ### Number Literals ```javascript 42 // Integer 3.14159 // Decimal -17 // Negative 1.23e-4 // Scientific notation 0xFF // Hexadecimal ``` ### Boolean Literals ```javascript true // Boolean true false // Boolean false ``` ### Array Literals ```javascript [] // Empty array [1, 2, 3] // Number array ["a", "b", "c"] // String array [true, false, null] // Mixed types [user.name, user.age] // Expressions as elements ``` ### Object Literals ```javascript {} // Empty object {name: "John", age: 30} // Simple object {x: 1, y: 2, z: x + y} // Expressions as values {"complex-key": value} // Quoted keys {[dynamicKey]: value} // Computed keys ``` ### Null Literal ```javascript null // Null value ``` ## Operators ### Arithmetic Operators ```javascript a + b // Addition a - b // Subtraction a * b // Multiplication a / b // Division a % b // Modulus a ^ b // Exponentiation ``` ### Comparison Operators ```javascript a == b // Equality a != b // Inequality a < b // Less than a <= b // Less than or equal a > b // Greater than a >= b // Greater than or equal ``` ### Logical Operators ```javascript a && b // Logical AND a || b // Logical OR !a // Logical NOT ``` ### Other Operators ```javascript a in b // Membership test a ? b : c // Ternary conditional a | transform // Transform (pipe operator) ``` ## Functions Functions are called with parentheses and can take multiple arguments: ```javascript length(array) // Single argument max([1, 2, 3, 4, 5]) // Array argument substring(text, 0, 5) // Multiple arguments contains(text, "search") // String arguments ``` ## Transforms Transforms use the pipe operator (`|`) to apply functions to values: ```javascript // Single transform "hello" | uppercase // "HELLO" // Chained transforms " hello world " | trim | uppercase | split(" ") // ["HELLO", "WORLD"] // Transform with arguments array | filter("value > 10") | map("value * 2") // Filter then transform each element ``` ## Comments JEXL supports both line and block comments: ```javascript // This is a line comment name | uppercase // Comment at end of line /* * This is a block comment * spanning multiple lines */ ``` ## Expression Evaluation ### Left-to-Right Evaluation ```javascript a + b * c // Evaluates as: a + (b * c) a | b | c // Evaluates as: (a | b) | c ``` ### Operator Precedence From highest to lowest precedence: 1. Property access (`.`, `[]`) 2. Function calls (`()`) 3. Unary operators (`!`, `-`) 4. Exponentiation (`^`) 5. Multiplication, Division, Modulus (`*`, `/`, `%`) 6. Addition, Subtraction (`+`, `-`) 7. Comparison (`<`, `<=`, `>`, `>=`) 8. Equality (`==`, `!=`) 9. Membership (`in`) 10. Logical AND (`&&`) 11. Logical OR (`||`) 12. Ternary (`? :`) 13. Transforms (`|`) ### Parentheses for Grouping ```javascript (a + b) * c // Force addition before multiplication a && (b || c) // Group logical operations (user | profile).name // Transform then access property ``` ## Context Variables JEXL expressions are evaluated against a context object: ```javascript // Context: { name: "John", age: 30, users: [...] } name // "John" age > 25 // true users | length // Number of users ``` ## Advanced Syntax ### Nested Expressions ```javascript users | filter("value.age > " + minAge) | map("value.name") // Uses variable in filter expression ``` ### Complex Object Construction ```javascript { fullName: firstName + " " + lastName, isAdult: age >= 18, summary: name + " is " + age + " years old" } ``` ### Conditional Chains ```javascript score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F" ``` ## Best Practices ### 1. Use Meaningful Names ```javascript // Good user.profile.displayName // Less clear u.p.dn ``` ### 2. Chain Transforms Logically ```javascript // Good - logical flow data | filter("value.active") | sort("value.name") | map("value.email") // Harder to read data | map("value.email") | sort | filter("value") ``` ### 3. Use Comments for Complex Logic ```javascript // Calculate weighted average score (homework * 0.3 + midterm * 0.3 + final * 0.4) | round(2) ``` ### 4. Group Related Operations ```javascript // Group calculations score = (quiz1 + quiz2 + quiz3) / 3; grade = score >= 90 ? "A" : score >= 80 ? "B" : "C" ``` ## Common Patterns ### Data Filtering and Mapping ```javascript // Get active users' names users | filter("value.status == 'active'") | map("value.name") ``` ### Aggregations ```javascript // Calculate total and average items | sum("value.price") items | average("value.rating") ``` ### String Processing ```javascript // Clean and format text title | trim | lowercase | replace(" ", "-") ``` ### Conditional Logic ```javascript // Status based on conditions status = errors > 0 ? "error" : warnings > 0 ? "warning" : "success" ``` Next: Learn about [Data Types](./data-types) in JEXL expressions. # Advanced Usage # Advanced Usage This guide covers advanced techniques for using JEXL Extended, including performance optimization, custom extensions, error handling strategies, and integration patterns for complex applications. ## Performance Optimization ### Expression Compilation and Caching For applications that evaluate the same expressions repeatedly, compile and cache them: ```typescript class ExpressionCache { private cache = new Map(); private jexl: any; constructor(jexl: any) { this.jexl = jexl; } compile(key: string, expression: string) { if (!this.cache.has(key)) { this.cache.set(key, this.jexl.compile(expression)); } return this.cache.get(key); } eval(key: string, context: any) { const compiled = this.cache.get(key); if (!compiled) { throw new Error(`Expression '${key}' not found in cache`); } return compiled.evalSync(context); } invalidate(key?: string) { if (key) { this.cache.delete(key); } else { this.cache.clear(); } } getStats() { return { size: this.cache.size, keys: Array.from(this.cache.keys()) }; } } // Usage import jexl from 'jexl-extended'; const expressionCache = new ExpressionCache(jexl); // Compile frequently used expressions expressionCache.compile('userFullName', 'user.firstName + " " + user.lastName'); expressionCache.compile('eligibilityCheck', 'user.age >= 18 && user.verified && user.status == "active"'); expressionCache.compile('salesReport', 'orders | sum("value.amount")'); // Fast repeated evaluation const users = getUsersFromDatabase(); users.forEach(user => { const fullName = expressionCache.eval('userFullName', { user }); const isEligible = expressionCache.eval('eligibilityCheck', { user }); console.log(`${fullName}: ${isEligible ? 'Eligible' : 'Not eligible'}`); }); ``` ### Context Optimization Optimize context creation for large datasets: ```typescript class OptimizedContext { private baseContext: any; private computedCache = new Map(); private accessLog = new Set(); constructor(baseContext: any) { this.baseContext = baseContext; } // Lazy computation of expensive operations get(key: string): any { this.accessLog.add(key); if (this.computedCache.has(key)) { return this.computedCache.get(key); } const value = this.computeValue(key); this.computedCache.set(key, value); return value; } private computeValue(key: string): any { switch (key) { case 'totalUsers': return this.baseContext.users?.length || 0; case 'activeUsers': return this.baseContext.users?.filter((u: any) => u.active).length || 0; case 'totalRevenue': return this.baseContext.orders?.reduce((sum: number, order: any) => sum + order.amount, 0) || 0; case 'averageOrderValue': const orders = this.baseContext.orders || []; return orders.length > 0 ? this.get('totalRevenue') / orders.length : 0; default: return this.baseContext[key]; } } // Get context with only accessed properties computed getOptimizedContext(): any { const context = { ...this.baseContext }; for (const key of this.accessLog) { if (this.computedCache.has(key)) { context[key] = this.computedCache.get(key); } } return context; } // Performance metrics getMetrics() { return { accessedKeys: Array.from(this.accessLog), computedKeys: Array.from(this.computedCache.keys()), cacheHitRatio: this.accessLog.size > 0 ? this.computedCache.size / this.accessLog.size : 0 }; } } // Usage function processLargeDataset(rawData: any) { const optimizedContext = new OptimizedContext(rawData); const expressions = [ 'totalUsers > 1000 ? "Large" : "Small"', 'activeUsers / totalUsers * 100', 'averageOrderValue > 100 ? "Premium" : "Standard"' ]; const results = expressions.map(expr => jexl.evalSync(expr, optimizedContext.getOptimizedContext()) ); console.log('Metrics:', optimizedContext.getMetrics()); return results; } ``` ### Batch Processing Process multiple expressions efficiently: ```typescript class BatchProcessor { private jexl: any; private compiledExpressions = new Map(); constructor(jexl: any) { this.jexl = jexl; } // Prepare expressions for batch processing prepare(expressions: { [key: string]: string }) { for (const [key, expression] of Object.entries(expressions)) { this.compiledExpressions.set(key, this.jexl.compile(expression)); } } // Process batch with shared context processBatch(contexts: any[]): any[] { return contexts.map(context => { const result: any = {}; for (const [key, compiled] of this.compiledExpressions) { try { result[key] = compiled.evalSync(context); } catch (error) { result[key] = { error: error.message }; } } return result; }); } // Process with streaming for large datasets async processStream(contexts: any[], onBatch?: (results: any[]) => void, batchSize = 100): Promise { const allResults: any[] = []; for (let i = 0; i < contexts.length; i += batchSize) { const batch = contexts.slice(i, i + batchSize); const batchResults = this.processBatch(batch); allResults.push(...batchResults); if (onBatch) { onBatch(batchResults); } // Allow event loop to process other tasks await new Promise(resolve => setTimeout(resolve, 0)); } return allResults; } } // Usage const processor = new BatchProcessor(jexl); processor.prepare({ fullName: 'firstName + " " + lastName', isEligible: 'age >= 18 && verified', scoreGrade: 'score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F"', monthsActive: '(now() - joinDate | dateTimeToMillis) / (30 * 24 * 60 * 60 * 1000) | floor' }); const users = getLargeUserDataset(); // 10,000+ users // Process in batches with progress tracking processor.processStream(users, (batchResults) => { console.log(`Processed batch of ${batchResults.length} users`); }, 500); ``` ## Custom Extensions ### Adding Transform Functions Extend JEXL with custom transform functions: ```typescript import jexl from 'jexl-extended'; // Add custom transforms jexl.addTransform('slugify', (value: string) => { return value .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); }); jexl.addTransform('truncate', (value: string, length: number = 50, suffix: string = '...') => { if (!value || value.length <= length) return value; return value.substring(0, length) + suffix; }); jexl.addTransform('currency', (value: number, currency: string = 'USD', locale: string = 'en-US') => { return new Intl.NumberFormat(locale, { style: 'currency', currency: currency }).format(value); }); jexl.addTransform('highlight', (text: string, searchTerm: string, highlightClass: string = 'highlight') => { if (!searchTerm) return text; const regex = new RegExp(`(${searchTerm})`, 'gi'); return text.replace(regex, `$1`); }); // Usage const title = "Hello World Example"; const slugified = jexl.evalSync('title | slugify', { title }); // "hello-world-example" const longText = "This is a very long piece of text that needs truncation"; const truncated = jexl.evalSync('text | truncate(20)', { text: longText }); // "This is a very long..." const price = 1234.56; const formatted = jexl.evalSync('price | currency("EUR", "de-DE")', { price }); // "1.234,56 €" ``` ### Custom Functions Add custom functions for complex operations: ```typescript // Add custom functions jexl.addFunction('distance', (lat1: number, lon1: number, lat2: number, lon2: number) => { const R = 6371; // Earth's radius in kilometers const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; }); jexl.addFunction('creditScore', (income: number, debt: number, paymentHistory: number) => { // Simplified credit score calculation const debtToIncomeRatio = debt / income; const baseScore = 300; const incomeBonus = Math.min(income / 1000, 200); const debtPenalty = debtToIncomeRatio * 100; const historyBonus = paymentHistory * 50; return Math.max(300, Math.min(850, baseScore + incomeBonus - debtPenalty + historyBonus)); }); jexl.addFunction('validateEmail', (email: string) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }); // Usage const userLocation = { lat: 40.7128, lon: -74.0060 }; // New York const storeLocation = { lat: 34.0522, lon: -118.2437 }; // Los Angeles const distanceKm = jexl.evalSync( 'distance(user.lat, user.lon, store.lat, store.lon)', { user: userLocation, store: storeLocation } ); // ~3944 km const financialData = { income: 75000, debt: 25000, paymentHistory: 0.95 }; const score = jexl.evalSync( 'creditScore(income, debt, paymentHistory)', financialData ); // Credit score calculation ``` ## Advanced Error Handling ### Expression Validation Pipeline ```typescript interface ValidationResult { valid: boolean; errors: string[]; warnings: string[]; suggestions: string[]; } class ExpressionValidator { private jexl: any; private customValidators: Array<(expr: string) => ValidationResult> = []; constructor(jexl: any) { this.jexl = jexl; this.setupDefaultValidators(); } private setupDefaultValidators() { // Syntax validation this.addValidator((expression) => { try { this.jexl.compile(expression); return { valid: true, errors: [], warnings: [], suggestions: [] }; } catch (error) { return { valid: false, errors: [`Syntax error: ${error.message}`], warnings: [], suggestions: ['Check parentheses and quotes', 'Verify function names'] }; } }); // Performance validation this.addValidator((expression) => { const warnings: string[] = []; const suggestions: string[] = []; // Check for potentially expensive operations if (expression.includes('| map(') && expression.includes('| filter(')) { const mapIndex = expression.indexOf('| map('); const filterIndex = expression.indexOf('| filter('); if (mapIndex < filterIndex) { warnings.push('Consider filtering before mapping for better performance'); suggestions.push('Move filter operations before map operations when possible'); } } // Check for nested function calls const nestedFunctionRegex = /\w+\([^()]*\w+\([^()]*\)[^()]*\)/g; if (nestedFunctionRegex.test(expression)) { suggestions.push('Consider breaking down complex nested functions for readability'); } return { valid: true, errors: [], warnings, suggestions }; }); } addValidator(validator: (expr: string) => ValidationResult) { this.customValidators.push(validator); } validate(expression: string): ValidationResult { const results = this.customValidators.map(validator => validator(expression)); return { valid: results.every(r => r.valid), errors: results.flatMap(r => r.errors), warnings: results.flatMap(r => r.warnings), suggestions: results.flatMap(r => r.suggestions) }; } } // Usage const validator = new ExpressionValidator(jexl); // Add custom business logic validator validator.addValidator((expression) => { const warnings: string[] = []; // Check for potentially unsafe operations if (expression.includes('eval(')) { return { valid: false, errors: ['Use of eval() function is not allowed for security reasons'], warnings: [], suggestions: ['Use other transformation functions instead'] }; } // Check for deprecated functions if (expression.includes('oldFunction(')) { warnings.push('oldFunction() is deprecated, use newFunction() instead'); } return { valid: true, errors: [], warnings, suggestions: [] }; }); const validationResult = validator.validate('users | map("value.name") | filter("value.length > 0")'); console.log(validationResult); ``` ### Graceful Error Recovery ```typescript class SafeEvaluator { private jexl: any; private fallbackStrategies = new Map(); constructor(jexl: any) { this.jexl = jexl; this.setupDefaultFallbacks(); } private setupDefaultFallbacks() { // Fallback for undefined properties this.fallbackStrategies.set('undefined_property', { detect: (error: Error) => error.message.includes('Cannot read property'), handle: (expression: string, context: any, error: Error) => { console.warn(`Property access failed: ${error.message}`); return null; } }); // Fallback for type errors this.fallbackStrategies.set('type_error', { detect: (error: Error) => error.message.includes('is not a function') || error.message.includes('Cannot read property'), handle: (expression: string, context: any, error: Error) => { console.warn(`Type error in expression: ${error.message}`); return undefined; } }); } evalWithRecovery(expression: string, context: any, options: { defaultValue?: any; maxRetries?: number; onError?: (error: Error, attempt: number) => void; } = {}) { const { defaultValue = null, maxRetries = 3, onError } = options; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return this.jexl.evalSync(expression, context); } catch (error) { if (onError) { onError(error, attempt); } // Try fallback strategies for (const [name, strategy] of this.fallbackStrategies) { if (strategy.detect(error)) { try { return strategy.handle(expression, context, error); } catch (fallbackError) { console.warn(`Fallback strategy '${name}' failed:`, fallbackError.message); } } } // If this is the last attempt, return default value if (attempt === maxRetries) { console.error(`Expression evaluation failed after ${maxRetries} attempts:`, error.message); return defaultValue; } } } return defaultValue; } // Batch evaluation with individual error handling evalBatch(expressions: Array<{ key: string; expression: string; context: any; defaultValue?: any }>) { const results: { [key: string]: any } = {}; const errors: { [key: string]: string } = {}; for (const { key, expression, context, defaultValue } of expressions) { try { results[key] = this.evalWithRecovery(expression, context, { defaultValue }); } catch (error) { errors[key] = error.message; results[key] = defaultValue || null; } } return { results, errors }; } } // Usage const safeEvaluator = new SafeEvaluator(jexl); const result = safeEvaluator.evalWithRecovery( 'user.profile.preferences.theme || "light"', { user: {} }, // Missing nested properties { defaultValue: 'light', onError: (error, attempt) => { console.log(`Attempt ${attempt} failed: ${error.message}`); } } ); ``` ## Integration Patterns ### Plugin Architecture ```typescript interface JexlPlugin { name: string; version: string; install(jexl: any): void; uninstall?(jexl: any): void; } class PluginManager { private jexl: any; private installedPlugins = new Map(); constructor(jexl: any) { this.jexl = jexl; } install(plugin: JexlPlugin) { if (this.installedPlugins.has(plugin.name)) { throw new Error(`Plugin '${plugin.name}' is already installed`); } try { plugin.install(this.jexl); this.installedPlugins.set(plugin.name, plugin); console.log(`Plugin '${plugin.name}' v${plugin.version} installed successfully`); } catch (error) { console.error(`Failed to install plugin '${plugin.name}':`, error); throw error; } } uninstall(pluginName: string) { const plugin = this.installedPlugins.get(pluginName); if (!plugin) { throw new Error(`Plugin '${pluginName}' is not installed`); } try { if (plugin.uninstall) { plugin.uninstall(this.jexl); } this.installedPlugins.delete(pluginName); console.log(`Plugin '${pluginName}' uninstalled successfully`); } catch (error) { console.error(`Failed to uninstall plugin '${pluginName}':`, error); } } getInstalledPlugins() { return Array.from(this.installedPlugins.values()); } } // Example plugins const mathPlugin: JexlPlugin = { name: 'advanced-math', version: '1.0.0', install: (jexl) => { jexl.addFunction('factorial', (n: number) => { if (n <= 1) return 1; return n * jexl.evalSync('factorial(' + (n - 1) + ')'); }); jexl.addFunction('fibonacci', (n: number) => { if (n <= 1) return n; return jexl.evalSync(`fibonacci(${n - 1}) + fibonacci(${n - 2})`); }); jexl.addTransform('toRadians', (degrees: number) => degrees * Math.PI / 180); jexl.addTransform('toDegrees', (radians: number) => radians * 180 / Math.PI); } }; const validationPlugin: JexlPlugin = { name: 'validation-helpers', version: '1.0.0', install: (jexl) => { jexl.addFunction('isEmail', (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)); jexl.addFunction('isPhone', (phone: string) => /^\+?[\d\s\-\(\)]+$/.test(phone)); jexl.addFunction('isUrl', (url: string) => { try { new URL(url); return true; } catch { return false; } }); jexl.addTransform('sanitizeHtml', (html: string) => { return html.replace(/<[^>]*>/g, ''); }); } }; // Usage const pluginManager = new PluginManager(jexl); pluginManager.install(mathPlugin); pluginManager.install(validationPlugin); // Use plugin functions const factorialResult = jexl.evalSync('factorial(5)'); // 120 const emailValid = jexl.evalSync('isEmail("user@example.com")'); // true ``` ### Middleware System ```typescript interface MiddlewareContext { expression: string; context: any; result?: any; error?: Error; metadata: { [key: string]: any }; } type Middleware = (ctx: MiddlewareContext, next: () => Promise) => Promise; class MiddlewareEngine { private middlewares: Middleware[] = []; private jexl: any; constructor(jexl: any) { this.jexl = jexl; } use(middleware: Middleware) { this.middlewares.push(middleware); } async eval(expression: string, context: any = {}, metadata: any = {}) { const ctx: MiddlewareContext = { expression, context, metadata: { startTime: Date.now(), ...metadata } }; const executeMiddleware = async (index: number): Promise => { if (index >= this.middlewares.length) { // Execute the actual evaluation try { ctx.result = this.jexl.evalSync(ctx.expression, ctx.context); } catch (error) { ctx.error = error; } return; } const middleware = this.middlewares[index]; await middleware(ctx, () => executeMiddleware(index + 1)); }; await executeMiddleware(0); if (ctx.error) { throw ctx.error; } return ctx.result; } } // Example middlewares const loggingMiddleware: Middleware = async (ctx, next) => { console.log(`Evaluating: ${ctx.expression}`); const start = Date.now(); await next(); const duration = Date.now() - start; console.log(`Completed in ${duration}ms`); }; const cachingMiddleware: Middleware = async (ctx, next) => { const cache = new Map(); const key = `${ctx.expression}:${JSON.stringify(ctx.context)}`; if (cache.has(key)) { ctx.result = cache.get(key); return; } await next(); if (!ctx.error) { cache.set(key, ctx.result); } }; const securityMiddleware: Middleware = async (ctx, next) => { // Block potentially dangerous expressions const dangerousPatterns = [/eval\s*\(/, /function\s*\(/, /constructor/]; for (const pattern of dangerousPatterns) { if (pattern.test(ctx.expression)) { ctx.error = new Error('Expression contains potentially dangerous code'); return; } } await next(); }; // Usage const engine = new MiddlewareEngine(jexl); engine.use(securityMiddleware); engine.use(loggingMiddleware); engine.use(cachingMiddleware); const result = await engine.eval('users | filter("value.active") | length', { users: [...] }); ``` ## Testing Strategies ### Expression Testing Framework ```typescript interface TestCase { name: string; expression: string; context: any; expected: any; shouldThrow?: boolean; errorMessage?: string; } class ExpressionTester { private jexl: any; private results: Array<{ name: string; passed: boolean; error?: string }> = []; constructor(jexl: any) { this.jexl = jexl; } test(testCase: TestCase) { try { const result = this.jexl.evalSync(testCase.expression, testCase.context); if (testCase.shouldThrow) { this.results.push({ name: testCase.name, passed: false, error: 'Expected expression to throw an error, but it succeeded' }); return; } const passed = this.deepEqual(result, testCase.expected); this.results.push({ name: testCase.name, passed, error: passed ? undefined : `Expected ${JSON.stringify(testCase.expected)}, got ${JSON.stringify(result)}` }); } catch (error) { if (testCase.shouldThrow) { const messageMatches = !testCase.errorMessage || error.message.includes(testCase.errorMessage); this.results.push({ name: testCase.name, passed: messageMatches, error: messageMatches ? undefined : `Expected error message to contain "${testCase.errorMessage}", got "${error.message}"` }); } else { this.results.push({ name: testCase.name, passed: false, error: `Unexpected error: ${error.message}` }); } } } runSuite(testCases: TestCase[]) { this.results = []; testCases.forEach(testCase => this.test(testCase)); return this.getReport(); } private deepEqual(a: any, b: any): boolean { return JSON.stringify(a) === JSON.stringify(b); } getReport() { const passed = this.results.filter(r => r.passed).length; const total = this.results.length; return { passed, failed: total - passed, total, success: passed === total, results: this.results }; } } // Usage const tester = new ExpressionTester(jexl); const testSuite: TestCase[] = [ { name: 'Basic arithmetic', expression: '2 + 3 * 4', context: {}, expected: 14 }, { name: 'String operations', expression: 'name | uppercase | split(" ") | join("-")', context: { name: "John Doe" }, expected: "JOHN-DOE" }, { name: 'Array filtering', expression: 'numbers | filter("value > 5") | length', context: { numbers: [1, 6, 3, 8, 2, 9] }, expected: 3 }, { name: 'Error handling', expression: 'user.invalid.property', context: { user: {} }, shouldThrow: true, errorMessage: 'Cannot read property' } ]; const report = tester.runSuite(testSuite); console.log(`Tests: ${report.passed}/${report.total} passed`); report.results.forEach(result => { if (!result.passed) { console.error(`❌ ${result.name}: ${result.error}`); } else { console.log(`✅ ${result.name}`); } }); ``` ## Production Considerations ### Monitoring and Metrics ```typescript class ExpressionMetrics { private metrics = { evaluations: 0, errors: 0, totalTime: 0, expressionCounts: new Map(), errorTypes: new Map() }; recordEvaluation(expression: string, duration: number, error?: Error) { this.metrics.evaluations++; this.metrics.totalTime += duration; const count = this.metrics.expressionCounts.get(expression) || 0; this.metrics.expressionCounts.set(expression, count + 1); if (error) { this.metrics.errors++; const errorType = error.constructor.name; const errorCount = this.metrics.errorTypes.get(errorType) || 0; this.metrics.errorTypes.set(errorType, errorCount + 1); } } getMetrics() { return { ...this.metrics, averageTime: this.metrics.evaluations > 0 ? this.metrics.totalTime / this.metrics.evaluations : 0, errorRate: this.metrics.evaluations > 0 ? this.metrics.errors / this.metrics.evaluations : 0, topExpressions: Array.from(this.metrics.expressionCounts.entries()) .sort(([,a], [,b]) => b - a) .slice(0, 10), topErrors: Array.from(this.metrics.errorTypes.entries()) .sort(([,a], [,b]) => b - a) }; } reset() { this.metrics = { evaluations: 0, errors: 0, totalTime: 0, expressionCounts: new Map(), errorTypes: new Map() }; } } // Wrapper with metrics class MonitoredJexl { private jexl: any; private metrics = new ExpressionMetrics(); constructor(jexl: any) { this.jexl = jexl; } evalSync(expression: string, context: any = {}) { const start = Date.now(); let error: Error | undefined; try { const result = this.jexl.evalSync(expression, context); return result; } catch (e) { error = e; throw e; } finally { const duration = Date.now() - start; this.metrics.recordEvaluation(expression, duration, error); } } getMetrics() { return this.metrics.getMetrics(); } resetMetrics() { this.metrics.reset(); } } ``` Advanced JEXL usage opens up powerful possibilities for building sophisticated expression evaluation systems. These patterns help you build maintainable, performant, and secure applications that leverage the full power of JEXL Extended. ## Next Steps * **Explore Examples** - Check out real-world examples in the repository * **Performance Testing** - Benchmark your expressions with your data * **Security Review** - Validate your custom extensions for security * **Community** - Share your custom plugins and extensions The advanced patterns in this guide provide a foundation for building enterprise-grade applications with JEXL Extended. # C# Implementation # C# Implementation JexlNet brings JEXL Extended functionality to .NET applications with full async support and JsonNode integration. ## Installation You need to install both the core JexlNet package and the ExtendedGrammar package: ```powershell # Package Manager Console Install-Package JexlNet Install-Package Jexl.ExtendedGrammar # .NET CLI dotnet add package JexlNet dotnet add package Jexl.ExtendedGrammar # PackageReference ``` ## Basic Usage ```csharp using JexlNet; using System.Text.Json.Nodes; // Create a Jexl instance with ExtendedGrammar var jexl = new Jexl(new ExtendedGrammar()); // Simple expression evaluation var result = jexl.Eval("5 + 3 * 2"); // 11 // With context data (as anonymous object) var context = new { name = "Alice", scores = new[] { 85, 92, 78 } }; var greeting = jexl.Eval("\"Hello \" + name", context); // "Hello Alice" var average = jexl.Eval("scores | average", context); // 85 // With JsonObject context var jsonContext = new JsonObject { ["user"] = new JsonObject { ["name"] = "John", ["age"] = 30 } }; var userName = jexl.Eval("user.name | uppercase", jsonContext); // "JOHN" ``` ## Async vs Sync Evaluation Both async and sync methods support async functions and transforms: ```csharp // Create Jexl instance with ExtendedGrammar var jexl = new Jexl(new ExtendedGrammar()); // Synchronous evaluation var result = jexl.Eval("expression", context); // Asynchronous evaluation var result = await jexl.EvalAsync("expression", context); // Creating reusable expressions var compiled = jexl.CreateExpression("user.name | uppercase"); var result1 = await compiled.EvalAsync(new { user = new { name = "Alice" } }); // "ALICE" var result2 = await compiled.EvalAsync(new { user = new { name = "Bob" } }); // "BOB" ``` ## Expression Examples Some working examples: ### String Operations ```csharp // Create Jexl instance var jexl = new Jexl(new ExtendedGrammar()); // String conversion and manipulation jexl.Eval("123456|toString"); // "123456" jexl.Eval("{'a':123456}|toString"); // "{\"a\":123456}" jexl.Eval("'123456'|string"); // "123456" // Case conversion jexl.Eval("'baz'|uppercase"); // "BAZ" jexl.Eval("$lowercase('FOObar')"); // "foobar" // camelCase and PascalCase jexl.Eval("'foo bar'|camelCase"); // "fooBar" jexl.Eval("$camelCase('Foo_bar')"); // "fooBar" jexl.Eval("'FooBar'|toCamelCase"); // "fooBar" jexl.Eval("'foo bar'|toPascalCase"); // "FooBar" jexl.Eval("'fooBar'|toPascalCase"); // "FooBar" // Substring operations jexl.Eval("substring(123456,2,2)"); // "34" jexl.Eval("substring('foo',1)"); // "oo" jexl.Eval("$substring('test',(-2))"); // "st" jexl.Eval("substringBefore(123456,2)"); // "1" jexl.Eval("substringAfter(123456,2)"); // "3456" // String utilities jexl.Eval("'baz '|trim"); // "baz" jexl.Eval("'__baz--'|trim('-')"); // "__baz" jexl.Eval("'foo'|pad(5)"); // "foo " jexl.Eval("'foo'|pad(-5,0)"); // "00foo" jexl.Eval("'foo-bar'|contains('bar')"); // true jexl.Eval("'foo-bar'|contains('baz')"); // false ``` ### Array Operations ```csharp // Array manipulation jexl.Eval("['foo','bar']|join('-')"); // "foo-bar" jexl.Eval("'f,b,a,d,e,c'|split(',')|sort|join"); // "a,b,c,d,e,f" jexl.Eval("'f,b,a,d,e,c'|split(',')|sort|join('')"); // "abcdef" // Contains operations jexl.Eval("'foo-bar'|contains('bar')"); // true jexl.Eval("['foo-bar']|contains('bar')"); // false jexl.Eval("['foo-bar']|contains('foo-bar')"); // true jexl.Eval("['baz', 'foo', 'bar']|contains('bar')"); // true // Split operations var splitResult = jexl.Eval("split('foo-bar', '-')"); // Returns JsonArray: ["foo", "bar"] ``` ### Mathematical Operations ```csharp // Number operations jexl.Eval("$number('1')"); // 1 jexl.Eval("$number('1.1')"); // 1.1 jexl.Eval("$number('-1.1')"); // -1.1 jexl.Eval("$number(-1.1)|floor"); // -2 jexl.Eval("$number('10.6')|ceil"); // 11 jexl.Eval("'5e2'|toNumber"); // 500 jexl.Eval("10.123456|round(2)"); // 10.12 jexl.Eval("3|power(2)"); // 9 jexl.Eval("3|power"); // 9 jexl.Eval("9|sqrt"); // 3 jexl.Eval("random() < 1 ? 1 : 0"); // 1 // Formatting jexl.Eval("16325.62|formatNumber('0,0.000')"); // "16,325.620" jexl.Eval("12|formatBase(16)"); // "c" jexl.Eval("9407886870244|formatBase(16)"); // "88e71c146e4" jexl.Eval("16325.62|formatInteger('0000000')"); // "0016325" // Integer operations with different bases jexl.Eval("'16325'|toInt"); // 16325 jexl.Eval("(9/2)|toInt"); // 4 jexl.Eval("'FF'|toInt(16)"); // 255 jexl.Eval("'1010'|toInt(2)"); // 10 jexl.Eval("'777'|toInt(8)"); // 511 // Aggregations jexl.Eval("[1,2,3]|sum"); // 6 jexl.Eval("sum(1,2,3,4,5)"); // 15 jexl.Eval("[1,3]|sum(1,2,3,4,5)"); // 19 jexl.Eval("[1,3]|max([1,2,3,4,5])"); // 5 jexl.Eval("[2,3]|min([1,2,3,4,5])"); // 1 jexl.Eval("[4,5,6]|avg"); // 5 ``` ### Boolean Operations ```csharp // Boolean conversion and logic jexl.Eval("1|toBoolean"); // true jexl.Eval("'true'|toBoolean"); // true jexl.Eval("0|toBoolean"); // false jexl.Eval("'false'|toBoolean"); // false jexl.Eval("''|toBoolean"); // false // Case statements jexl.Eval("2|case(1,'a',2,'b',3,'c')"); // "b" jexl.Eval("'bar'|case('foo','a','bar','b','baz','c')"); // "b" ``` ### Encoding and Conversion ```csharp // Base64 encoding jexl.Eval("'foobar'|base64Encode"); // "Zm9vYmFy" jexl.Eval("'Zm9vYmFy'|base64Decode"); // "foobar" // URL encoding jexl.Eval("{foo:'bar',baz:'tek'}|formUrlEncoded"); // "foo=bar&baz=tek" // Regular expressions jexl.Eval("'foobar'|regexMatch('foo')"); // true jexl.Eval("'bazbar'|regexReplace('baz', 'foo')"); // "foobar" // Text replacement jexl.Eval("replace('foo-bar', '-', '_')"); // "foo_bar" jexl.Eval("'123ab123ab123ab'|replace('123')"); // "ababab" ``` ## Integration Patterns ### ASP.NET Core Integration ```csharp using JexlNet; using Microsoft.AspNetCore.Mvc; using System.Text.Json.Nodes; [ApiController] [Route("api/[controller]")] public class ExpressionController : ControllerBase { private readonly Jexl _jexl; public ExpressionController() { _jexl = new Jexl(); } [HttpPost("evaluate")] public async Task EvaluateExpression([FromBody] ExpressionRequest request) { try { var result = await _jexl.EvalAsync(request.Expression, request.Context); return Ok(new { result }); } catch (Exception ex) { return BadRequest(new { error = ex.Message }); } } } public class ExpressionRequest { public string Expression { get; set; } = ""; public JsonObject? Context { get; set; } } ``` ### Dependency Injection Setup ```csharp // Program.cs or Startup.cs services.AddSingleton(); // Usage in controller public class DataController : ControllerBase { private readonly Jexl _jexl; public DataController(Jexl jexl) { _jexl = jexl; } [HttpGet("users/filtered")] public async Task GetFilteredUsers([FromQuery] string? filter = null) { var users = await GetUsersAsync(); if (!string.IsNullOrEmpty(filter)) { var context = new { users }; var filteredUsers = await _jexl.EvalAsync($"users | filter(\"{filter}\")", context); return Ok(filteredUsers); } return Ok(users); } } ``` ### Configuration System ```csharp public class FeatureFlags { private readonly Jexl _jexl; private readonly JsonObject _config; public FeatureFlags(IConfiguration configuration) { _jexl = new Jexl(); _config = JsonNode.Parse(configuration.GetSection("Features").Get())?.AsObject() ?? new JsonObject(); } public async Task IsFeatureEnabledAsync(string featureName, object userContext) { if (!_config.ContainsKey(featureName)) return false; var feature = _config[featureName]?.AsObject(); var condition = feature?["condition"]?.ToString() ?? "false"; var context = new JsonObject { ["user"] = JsonSerializer.SerializeToNode(userContext), ["config"] = _config }; try { var result = await _jexl.EvalAsync(condition, context); return result?.GetValue() ?? false; } catch { return false; } } } // Usage var user = new { Role = "admin", Subscription = "premium" }; var hasFeature = await featureFlags.IsFeatureEnabledAsync("advanced_analytics", user); ``` ### Data Processing Pipeline ```csharp public class DataProcessor { private readonly Jexl _jexl; public DataProcessor() { _jexl = new Jexl(); } public async Task ProcessDataAsync(JsonArray data, string[] expressions) { var results = new JsonObject(); var context = new JsonObject { ["data"] = data }; foreach (var expression in expressions) { try { var result = await _jexl.EvalAsync(expression, context); results[expression] = result; } catch (Exception ex) { results[expression] = JsonValue.Create($"Error: {ex.Message}"); } } return results; } } // Usage var processor = new DataProcessor(); var data = JsonNode.Parse("[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]")?.AsArray(); var expressions = new[] { "data | length", "data | average(\"age\")", "data | map(\"name\") | join(\", \")" }; var results = await processor.ProcessDataAsync(data, expressions); ``` ## Custom Functions and Transforms ### Basic Custom Functions ```csharp // Create Jexl with ExtendedGrammar var jexl = new Jexl(new ExtendedGrammar()); // Add custom transform jexl.Grammar.AddTransform("slugify", (JsonValue val) => { var text = val?.ToString() ?? ""; return text.ToLowerInvariant() .Replace(" ", "-") .Replace("_", "-"); }); // Add custom function jexl.Grammar.AddFunction("formatCurrency", (JsonNode amount, JsonNode currency) => { var value = amount?.GetValue() ?? 0; var curr = currency?.ToString() ?? "USD"; return curr switch { "USD" => $"${value:N2}", "EUR" => $"€{value:N2}", _ => $"{value:N2} {curr}" }; }); // Add async function jexl.Grammar.AddFunction("fetchUserData", async (JsonNode userId) => { var id = userId?.GetValue() ?? 0; // Simulate async database call await Task.Delay(100); return new JsonObject { ["id"] = id, ["name"] = $"User {id}", ["active"] = true }; }); // Usage var title = await jexl.EvalAsync("'Hello World'|slugify"); // "hello-world" var price = await jexl.EvalAsync("formatCurrency(99.99, 'EUR')"); // "€99.99" var userData = await jexl.EvalAsync("fetchUserData(123)"); ``` ### Advanced: Jexl Wrapper with Caching For production applications, create a wrapper class with expression caching: ```csharp using JexlNet; using Microsoft.Extensions.Caching.Memory; using System.Text.Json.Nodes; public partial class JexlService { private readonly Jexl _jexl; private readonly MemoryCache _expressionCache; public JexlService() { // Initialize with ExtendedGrammar _jexl = new Jexl(new ExtendedGrammar()); // Configure expression cache for performance _expressionCache = new MemoryCache(new MemoryCacheOptions() { SizeLimit = 1000 }); // Add custom transforms _jexl.Grammar.AddTransform("toEpoch", ExtendedGrammar.DateTimeToMillis); _jexl.Grammar.AddTransform("hexToFloat32", HexToFloat32); _jexl.Grammar.AddTransform("regExTest", RegExTest); // Add custom functions _jexl.Grammar.AddFunction("currentEpoch", ExtendedGrammar.Millis); _jexl.Grammar.AddFunction("parseDouble", ExtendedGrammar.ToNumber); } /// /// Creates an expression from a string or retrieves it from cache /// public Expression? CreateExpression(string exprStr) { // Support Excel-style expressions starting with = if (exprStr.StartsWith('=')) { exprStr = exprStr[1..]; } return _expressionCache.GetOrCreate( exprStr, entry => { entry.Size = 1; return new Expression(_jexl.Grammar, exprStr); } ); } /// /// Evaluates a cached expression asynchronously /// public async Task EvalAsync( string expression, JsonObject? context = null, CancellationToken cancellationToken = default) { if (expression.StartsWith('=')) { expression = expression[1..]; } Expression? expr = CreateExpression(expression); if (expr != null) { return await expr.EvalAsync(context, cancellationToken); } return null; } /// /// Evaluates a cached expression synchronously /// public JsonNode? Eval( string expression, JsonObject? context = null, CancellationToken cancellationToken = default) { if (expression.StartsWith('=')) { expression = expression[1..]; } Expression? expr = CreateExpression(expression); if (expr != null) { return expr.Eval(context, cancellationToken); } return null; } // Custom transform: Hex string to float32 private static JsonNode? HexToFloat32(JsonNode input) { if (input is JsonValue value && value.GetValueKind() == JsonValueKind.String) { string hex = value.ToString(); if (hex.StartsWith("0x")) { return float.Parse(hex[2..], NumberStyles.HexNumber); } return float.Parse(hex, NumberStyles.HexNumber); } return null; } // Custom transform: Regex test private static JsonNode? RegExTest(JsonNode input, JsonNode regex) { if (input is JsonValue inputValue && regex is JsonValue regexValue && inputValue.GetValueKind() == JsonValueKind.String && regexValue.GetValueKind() == JsonValueKind.String) { return System.Text.RegularExpressions.Regex.IsMatch( inputValue.ToString(), regexValue.ToString() ); } return null; } } // Usage with dependency injection services.AddSingleton(); ``` ## Error Handling ```csharp public static class JexlExtensions { public static async Task SafeEvalAsync(this Jexl jexl, string expression, object? context = null, T? defaultValue = default) { try { var result = await jexl.EvalAsync(expression, context); return result?.GetValue() ?? defaultValue; } catch (Exception ex) { Console.WriteLine($"Expression evaluation failed: {ex.Message}"); return defaultValue; } } } // Usage var jexl = new Jexl(); var context = new { user = new { name = "John" } }; var name = await jexl.SafeEvalAsync("user.name | uppercase", context, "Unknown"); var email = await jexl.SafeEvalAsync("user.profile.email", context, "No email"); ``` ## Form Validation Example ```csharp public class FormValidator { private readonly Jexl _jexl; private readonly Dictionary _rules; public FormValidator() { _jexl = new Jexl(); _rules = new Dictionary { ["email"] = "email && email | contains('@') && email | contains('.')", ["age"] = "age >= 18 && age <= 120", ["password"] = "password && password | length >= 8", ["confirmPassword"] = "password == confirmPassword" }; } public async Task> ValidateAsync(object formData) { var results = new Dictionary(); foreach (var (field, rule) in _rules) { try { var result = await _jexl.EvalAsync(rule, formData); results[field] = result?.GetValue() ?? false; } catch { results[field] = false; } } return results; } } // Usage var validator = new FormValidator(); var formData = new { email = "user@example.com", age = 25, password = "securepass123", confirmPassword = "securepass123" }; var results = await validator.ValidateAsync(formData); var isValid = results.All(r => r.Value); ``` ## Testing JEXL Expressions ```csharp using Xunit; using JexlNet; public class JexlExpressionTests { private readonly Jexl _jexl; public JexlExpressionTests() { _jexl = new Jexl(); } [Theory] [InlineData("string(123)", "123")] [InlineData("'hello'|uppercase", "HELLO")] [InlineData("'WORLD'|lowercase", "world")] public async Task StringOperations_ShouldWork(string expression, string expected) { var result = await _jexl.EvalAsync(expression); Assert.Equal(expected, result?.ToString()); } [Theory] [InlineData("[1,2,3]|sum", 6)] [InlineData("3|power(2)", 9)] [InlineData("9|sqrt", 3)] public async Task MathOperations_ShouldWork(string expression, decimal expected) { var result = await _jexl.EvalAsync(expression); Assert.Equal(expected, result?.GetValue()); } [Fact] public async Task ArrayFiltering_ShouldWork() { var context = new { users = new[] { new { name = "Alice", age = 30, active = true }, new { name = "Bob", age = 25, active = false } } }; var result = await _jexl.EvalAsync("users | filter('value.active') | length", context); Assert.Equal(1, result?.GetValue()); } } ``` ## Performance Tips 1. **Reuse Jexl instances**: Create once and reuse across your application 2. **Use compiled expressions**: For repeated evaluations, compile expressions 3. **Optimize context size**: Only include necessary data in the context 4. **Consider async**: Use `EvalAsync` for I/O-bound operations ```csharp // Efficient pattern public class ExpressionService { private readonly Jexl _jexl; private readonly ConcurrentDictionary _compiledExpressions; public ExpressionService() { _jexl = new Jexl(); _compiledExpressions = new ConcurrentDictionary(); } public async Task EvaluateAsync(string expression, object context) { var compiled = _compiledExpressions.GetOrAdd(expression, expr => _jexl.CreateExpression(expr)); return await compiled.EvalAsync(context); } } ``` ## Next Steps * **Language Guide** - Learn [JEXL syntax](../language/) that works across all implementations * **Function Reference** - Browse all [built-in functions](../reference/) * **GitHub Repository** - Check [JexlNet](https://github.com/konnektr-io/JexlNet) for latest updates * **Test Examples** - See the [test files](https://github.com/konnektr-io/JexlNet/tree/main/JexlNet.Test) for more examples # Getting Started with JEXL Extended # Getting Started with JEXL Extended Welcome to JEXL Extended! This guide will get you up and running with JavaScript Expression Language (JEXL) and show you how to use the 80+ built-in functions to transform and manipulate data. ## What is JEXL Extended? JEXL Extended is a powerful JavaScript library that extends the original JEXL (JavaScript Expression Language) with: * **80+ Built-in Functions** - String manipulation, math, arrays, objects, dates, and more * **Monaco Editor Support** - Rich IDE experience with syntax highlighting and IntelliSense * **TypeScript Support** - Full type definitions included * **Modular Design** - Use the entire library or import individual functions ## Installation Install JEXL Extended using your preferred package manager: ```bash # Using npm npm install jexl-extended # Using yarn yarn add jexl-extended # Using pnpm pnpm add jexl-extended ``` ## Your First JEXL Expression Let's start with a simple example: ```javascript import jexl from 'jexl-extended'; // Simple expression const result = jexl.evalSync('5 + 3'); console.log(result); // 8 // Using variables const data = { name: "Alice", age: 28 }; const greeting = jexl.evalSync('"Hello " + name + "!"', data); console.log(greeting); // "Hello Alice!" ``` ## Basic Concepts ### Context and Variables JEXL expressions are evaluated against a **context** - a JavaScript object containing your data: ```javascript const context = { user: { name: "John Doe", age: 30, email: "john@example.com" }, scores: [85, 92, 78, 96] }; // Access properties jexl.evalSync('user.name', context); // "John Doe" jexl.evalSync('user.age > 25', context); // true jexl.evalSync('scores[0]', context); // 85 ``` ### Functions vs Transforms JEXL Extended provides two ways to call functions: **Functions** are called directly: ```javascript jexl.evalSync('length("hello")'); // 5 jexl.evalSync('max([1, 5, 3, 9, 2])'); // 9 jexl.evalSync('uppercase("hello")'); // "HELLO" ``` **Transforms** use the pipe operator (`|`): ```javascript jexl.evalSync('"hello" | length'); // 5 jexl.evalSync('[1, 5, 3, 9, 2] | max'); // 9 jexl.evalSync('"hello" | uppercase'); // "HELLO" ``` ### Chaining Operations The real power comes from chaining operations together: ```javascript const users = [ { name: "Alice", age: 28, active: true }, { name: "Bob", age: 32, active: false }, { name: "Charlie", age: 24, active: true } ]; // Chain multiple operations const activeUserNames = jexl.evalSync( 'users | filter("value.active") | map("value.name") | join(", ")', { users } ); console.log(activeUserNames); // "Alice, Charlie" ``` ## Common Examples ### String Processing ```javascript // Clean and format text const messyText = " Hello World "; const clean = jexl.evalSync('text | trim | lowercase | split(" ") | join("-")', { text: messyText }); console.log(clean); // "hello-world" // Extract information const email = "john.doe@company.com"; const domain = jexl.evalSync('email | split("@")[1]', { email }); console.log(domain); // "company.com" ``` ### Array Operations ```javascript const numbers = [1, 2, 3, 4, 5]; // Statistical operations const stats = jexl.evalSync(`{ sum: numbers | sum, average: numbers | average, max: numbers | max, min: numbers | min, count: numbers | length }`, { numbers }); console.log(stats); // { sum: 15, average: 3, max: 5, min: 1, count: 5 } // Filter and transform const evenSquares = jexl.evalSync( 'numbers | filter("value % 2 == 0") | map("value * value")', { numbers } ); console.log(evenSquares); // [4, 16] ``` ### Object Manipulation ```javascript const user = { firstName: "John", lastName: "Doe", email: "john@example.com", preferences: { theme: "dark", notifications: true } }; // Extract and transform const summary = jexl.evalSync(`{ fullName: firstName + " " + lastName, domain: email | split("@")[1], hasNotifications: preferences.notifications, profileComplete: firstName && lastName && email }`, user); console.log(summary); // { // fullName: "John Doe", // domain: "example.com", // hasNotifications: true, // profileComplete: true // } ``` ### Date and Time ```javascript // Current time operations const now = jexl.evalSync('now()'); const formatted = jexl.evalSync('now() | dateTimeFormat("YYYY-MM-DD HH:mm")'); const tomorrow = jexl.evalSync('now() | dateTimeAdd("days", 1)'); console.log('Now:', now); console.log('Formatted:', formatted); console.log('Tomorrow:', tomorrow); // Time calculations const birthDate = "1990-05-15"; const age = jexl.evalSync( '(now() | dateTimeToMillis - birthDate | dateTimeToMillis) / (365.25 * 24 * 60 * 60 * 1000) | floor', { birthDate } ); console.log('Age:', age); ``` ## Error Handling Always handle potential errors when evaluating expressions: ```javascript try { const result = jexl.evalSync('user.name | uppercase', { user: { name: "John" } }); console.log(result); // "JOHN" } catch (error) { console.error('Expression error:', error.message); } // Safe property access const safeName = jexl.evalSync('user && user.name || "Unknown"', { user: null }); console.log(safeName); // "Unknown" ``` ## Async Evaluation JEXL Extended supports both synchronous and asynchronous evaluation: ```javascript // Synchronous (most common) const syncResult = jexl.evalSync('5 + 3'); // Asynchronous const asyncResult = await jexl.eval('5 + 3'); // Both produce the same result console.log(syncResult === asyncResult); // true ``` ## Interactive Playground Want to experiment with JEXL expressions? Try the online playground at [nikoraes.github.io/jexl-playground/](https://nikoraes.github.io/jexl-playground/) The playground includes: * Live expression evaluation * Sample data to work with * Function reference * Syntax highlighting * Error messages and debugging ## Common Patterns ### Data Validation ```javascript const formData = { email: "user@example.com", age: 25, agreedToTerms: true }; const isValid = jexl.evalSync(` email | contains("@") && age >= 18 && agreedToTerms == true `, formData); console.log('Form valid:', isValid); // true ``` ### Configuration Logic ```javascript const config = { environment: "production", features: { darkMode: true, analytics: true }, user: { role: "admin", premium: true } }; const shouldShowFeature = jexl.evalSync(` environment == "production" && features.analytics && (user.role == "admin" || user.premium) `, config); console.log('Show feature:', shouldShowFeature); // true ``` ### Dynamic Queries ```javascript const users = [ { name: "Alice", age: 28, department: "Engineering", salary: 75000 }, { name: "Bob", age: 32, department: "Marketing", salary: 65000 }, { name: "Charlie", age: 24, department: "Engineering", salary: 70000 } ]; // Dynamic filter based on criteria const criteria = { minAge: 25, department: "Engineering", minSalary: 70000 }; const query = `users | filter(" value.age >= minAge && value.department == department && value.salary >= minSalary ") | map("value.name")`; const matches = jexl.evalSync(query, { users, ...criteria }); console.log('Matching users:', matches); // ["Alice", "Charlie"] ``` ## Next Steps Now that you understand the basics: 1. **Explore the Language** - Read the [JEXL Language Guide](../language/) to understand syntax, operators, and expressions in detail 2. **Learn the Functions** - Browse the [Function Reference](../reference/) to discover all 80+ available functions 3. **Build Something** - Check out [Basic Usage](./basic-usage) for practical integration examples 4. **Add Rich Editing** - Set up [Monaco Editor Integration](./monaco-integration) for a premium development experience 5. **Go Advanced** - Explore [Advanced Usage](./advanced-usage) for performance tips and custom extensions ## Need Help? * **Function Reference** - Complete documentation for all functions * **Language Guide** - Detailed syntax and language features * **Usage Examples** - Real-world integration patterns * **Online Playground** - Interactive expression testing Ready to dive deeper? Continue with [Basic Usage](./basic-usage) to learn how to integrate JEXL Extended into your applications! # Usage Guides # Usage Guides These guides show you how to use JEXL Extended in your projects, from basic expression evaluation to advanced Monaco Editor integration. ## Getting Started ### [Getting Started Guide](./getting-started) Learn the basics of JEXL Extended, installation, and your first expressions. Perfect for newcomers to JEXL. ### [Basic Usage](./basic-usage) Comprehensive guide to using JEXL Extended as an expression evaluator in your JavaScript/TypeScript applications. ## Integration Guides ### [Monaco Editor Integration](./monaco-integration) Complete guide to integrating JEXL with Monaco Editor for rich IDE experience with syntax highlighting, IntelliSense, and hover documentation. ### [Advanced Usage](./advanced-usage) Advanced patterns, performance optimization, error handling, and extending JEXL Extended with custom functionality. ## Quick Examples ### Expression Evaluation ```javascript import jexl from 'jexl-extended'; const data = { users: [{ name: "Alice", age: 28 }, { name: "Bob", age: 32 }] }; const result = jexl.evalSync('users|filter("value.age > 30")|map("value.name")', data); // ["Bob"] ``` ### Monaco Editor Setup ```typescript import * as monaco from "monaco-editor"; import { Monaco } from "jexl-extended"; Monaco.registerJexlLanguage(monaco); const editor = Monaco.createJexlEditor(monaco, container, { value: 'users|filter("value.active")|map("value.name")', theme: "vs-dark" }); ``` ### String Processing ```javascript const text = " Hello World "; const result = jexl.evalSync('text | trim | lowercase | split(" ") | join("-")', { text }); // "hello-world" ``` ### Mathematical Operations ```javascript const numbers = [1, 2, 3, 4, 5]; const stats = jexl.evalSync('{ sum: numbers | sum, average: numbers | average, max: numbers | max, count: numbers | length }', { numbers }); // { sum: 15, average: 3, max: 5, count: 5 } ``` ## Common Use Cases ### Data Transformation Transform API responses, filter arrays, and manipulate objects with powerful expression chains. ### Form Validation Create dynamic validation rules using JEXL expressions that can be stored and evaluated at runtime. ### Configuration Logic Build flexible configuration systems where business rules are expressed as JEXL expressions. ### Template Processing Process templates with dynamic content using JEXL expressions for calculations and formatting. ### Query Building Create dynamic queries and filters using JEXL expressions that can be safely evaluated. ## Choose Your Guide * **New to JEXL?** Start with [Getting Started](./getting-started) * **Adding to existing project?** Check [Basic Usage](./basic-usage) * **Want rich editor experience?** See [Monaco Integration](./monaco-integration) * **Need advanced features?** Explore [Advanced Usage](./advanced-usage) Each guide builds on the previous ones, so feel free to jump around based on your needs! # JavaScript Implementation # JavaScript Implementation JEXL Extended is the original JavaScript/TypeScript implementation with full type support and Monaco Editor integration. ## Installation ```bash # npm npm install jexl-extended # yarn yarn add jexl-extended # pnpm pnpm add jexl-extended ``` ## Basic Usage ### ES6 Modules ```javascript import jexl from 'jexl-extended'; // Simple expression evaluation const result = jexl.evalSync('5 + 3 * 2'); // 11 // With context data const context = { name: "Alice", scores: [85, 92, 78] }; const greeting = jexl.evalSync('"Hello " + name', context); // "Hello Alice" const average = jexl.evalSync('scores | average', context); // 85 ``` ### CommonJS ```javascript const jexl = require('jexl-extended'); const result = jexl.evalSync('[1, 2, 3] | sum'); // 6 ``` ### TypeScript Support Full TypeScript definitions are included: ```typescript import jexl from 'jexl-extended'; import type { JexlExpression, Context } from 'jexl-extended'; interface UserContext { user: { name: string; age: number; active: boolean; }; settings: Record; } const context: UserContext = { user: { name: "John", age: 30, active: true }, settings: { theme: "dark" } }; // Type-safe evaluation const userName: string = jexl.evalSync('user.name | uppercase', context); const isEligible: boolean = jexl.evalSync('user.age >= 18 && user.active', context); ``` ## Expression Examples Based on the actual test suite, here are working examples: ### String Operations ```javascript // String conversion and manipulation jexl.evalSync('string(123)'); // "123" jexl.evalSync('123456|string'); // "123456" jexl.evalSync('{a:123456}|string'); // '{"a":123456}' // Case conversion jexl.evalSync('"hello world"|uppercase'); // "HELLO WORLD" jexl.evalSync('"HELLO WORLD"|lowercase'); // "hello world" jexl.evalSync('"FOObar"|lower'); // "foobar" // camelCase and PascalCase jexl.evalSync('"foo bar"|camelCase'); // "fooBar" jexl.evalSync('"Foo_bar"|camelCase'); // "fooBar" jexl.evalSync('"foo bar"|toPascalCase'); // "FooBar" jexl.evalSync('"fooBar"|toPascalCase'); // "FooBar" // Substring operations jexl.evalSync('substring(123456,2,2)'); // "34" jexl.evalSync('substring("test",(-2))'); // "st" jexl.evalSync('"hello world"|substringBefore(" ")'); // "hello" jexl.evalSync('"hello world"|substringAfter(" ")'); // "world" // String utilities jexl.evalSync('trim(" baz ")'); // "baz" jexl.evalSync('pad("foo",5)'); // "foo " jexl.evalSync('pad("foo",(-5),0)'); // "00foo" jexl.evalSync('"foo-bar"|contains("bar")'); // true jexl.evalSync('"foo-bar"|startsWith("foo")'); // true jexl.evalSync('"foo-bar"|endsWith("bar")'); // true ``` ### Array Operations ```javascript // Array manipulation jexl.evalSync('["foo", "bar", "baz"]|append("tek")'); // ['foo', 'bar', 'baz', 'tek'] jexl.evalSync('["foo", "bar"]|append(["baz","tek"])'); // ['foo', 'bar', 'baz', 'tek'] jexl.evalSync('["tek", "baz", "bar", "foo"]|reverse'); // ['foo', 'bar', 'baz', 'tek'] jexl.evalSync('["tek", "baz", "bar", "foo", "foo"]|reverse|distinct'); // ['foo', 'bar', 'baz', 'tek'] // Array splitting and joining jexl.evalSync('split("foo-bar", "-")'); // ['foo', 'bar'] jexl.evalSync('join(["foo", "bar"], "-")'); // "foo-bar" jexl.evalSync('"f,b,a,d,e,c"|split(",")|sort|join'); // "a,b,c,d,e,f" jexl.evalSync('"f,b,a,d,e,c"|split(",")|sort|join("")'); // "abcdef" // Object operations jexl.evalSync('{foo:0, bar:1, baz:2, tek:3}|keys'); // ['foo', 'bar', 'baz', 'tek'] jexl.evalSync('{a:"foo", b:"bar", c:"baz", d:"tek"}|values'); // ['foo', 'bar', 'baz', 'tek'] ``` ### Mathematical Operations ```javascript // Number operations jexl.evalSync('number("1.1")'); // 1.1 jexl.evalSync('number(-1.1)|floor'); // -2 jexl.evalSync('number("10.6")|ceil'); // 11 jexl.evalSync('10.123456|round(2)'); // 10.12 jexl.evalSync('10.123456|toInt'); // 10 jexl.evalSync('"10.123456"|toInt'); // 10 jexl.evalSync('3|power(2)'); // 9 jexl.evalSync('3|power'); // 9 (defaults to power of 2) jexl.evalSync('9|sqrt'); // 3 jexl.evalSync('random() < 1'); // true // Formatting jexl.evalSync('16325.62|formatNumber("0,0.000")'); // "16,325.620" jexl.evalSync('16325.62|formatNumber("0.000")'); // "16325.620" jexl.evalSync('12|formatBase(16)'); // "c" jexl.evalSync('16325.62|formatInteger("0000000")'); // "0016325" // Aggregations jexl.evalSync('[1,2,3]|sum'); // 6 jexl.evalSync('sum(1,2,3,4,5)'); // 15 jexl.evalSync('[1,3]|sum(1,2,3,4,5)'); // 19 jexl.evalSync('[1,3]|max([1,2,3,4,5])'); // 5 jexl.evalSync('[2,3]|min([1,2,3,4,5])'); // 1 jexl.evalSync('[4,5,6]|avg'); // 5 ``` ### Boolean and Logic Operations ```javascript // Boolean conversion jexl.evalSync('1|toBoolean'); // true jexl.evalSync('3|toBoolean'); // true jexl.evalSync('"1"|toBoolean'); // true jexl.evalSync('0|toBool'); // false jexl.evalSync('"false"|toBool'); // false jexl.evalSync('"True"|toBool'); // true jexl.evalSync('"tRUE "|toBoolean'); // true // Case statements jexl.evalSync('2|case(1,"a",2,"b",3,"c")'); // "b" jexl.evalSync('case("bar","foo","a","bar","b","baz","c")'); // "b" jexl.evalSync('"notfound"|case("bar","foo","a","bar","b","baz","c","b","b")'); // "b" (default) // Logical operations jexl.evalSync('"False"|toBool|not'); // true jexl.evalSync('"TRUE"|toBool|not'); // false ``` ### Encoding and Conversion ```javascript // Base64 encoding jexl.evalSync('"foobar"|base64Encode'); // "Zm9vYmFy" jexl.evalSync('"Zm9vYmFy"|base64Decode'); // "foobar" jexl.evalSync('"hello⛳❤️🧀"|base64Encode|base64Decode'); // "hello⛳❤️🧀" // URL encoding jexl.evalSync('{foo:"bar",baz:"tek"}|formUrlEncoded'); // "foo=bar&baz=tek" // Text replacement jexl.evalSync('replace("foo-bar", "-", "_")'); // "foo_bar" jexl.evalSync('replace("foo-bar---", "-", "")'); // "foobar" jexl.evalSync('"123ab123ab123ab"|replace("123")'); // "ababab" ``` ## Async vs Sync Evaluation ### Synchronous (Recommended for most cases) ```javascript const result = jexl.evalSync('expression', context); ``` ### Asynchronous ```javascript const result = await jexl.eval('expression', context); // Multiple expressions in parallel const expressions = ['expr1', 'expr2', 'expr3']; const results = await Promise.all( expressions.map(expr => jexl.eval(expr, context)) ); ``` ## Expression Compilation For repeated evaluations, compile expressions once: ```javascript // Compile once const compiled = jexl.compile('user.name | uppercase'); // Evaluate multiple times const result1 = compiled.evalSync({ user: { name: 'Alice' } }); // "ALICE" const result2 = compiled.evalSync({ user: { name: 'Bob' } }); // "BOB" ``` ## Integration Patterns ### React Component ```javascript import React, { useMemo } from 'react'; import jexl from 'jexl-extended'; function DataDisplay({ data, expression }) { const result = useMemo(() => { try { return jexl.evalSync(expression, data); } catch (error) { return `Error: ${error.message}`; } }, [data, expression]); return
{JSON.stringify(result)}
; } // Usage ``` ### Express.js Middleware ```javascript function jexlMiddleware() { return (req, res, next) => { req.jexlEval = (expression, additionalContext = {}) => { const context = { req: { params: req.params, query: req.query, body: req.body, user: req.user }, ...additionalContext }; return jexl.evalSync(expression, context); }; next(); }; } // Usage app.use(jexlMiddleware()); app.get('/api/data', (req, res) => { const filtered = req.jexlEval('data | filter("value.visible")', { data: [...] }); res.json(filtered); }); ``` ### Form Validation ```javascript const validationRules = { email: 'email | contains("@") && email | contains(".")', age: 'age >= 18 && age <= 120', password: 'password | length >= 8' }; function validateForm(formData) { const results = {}; for (const [field, rule] of Object.entries(validationRules)) { try { results[field] = jexl.evalSync(rule, formData); } catch (error) { results[field] = false; } } return results; } ``` ## Error Handling ```javascript function safeEval(expression, context, defaultValue = null) { try { return jexl.evalSync(expression, context); } catch (error) { console.warn(`Expression evaluation failed: ${error.message}`); return defaultValue; } } // Usage const result = safeEval('user.profile.name', context, 'Unknown User'); ``` ## Custom Functions and Transforms ```javascript // Add custom transform jexl.addTransform('slugify', (value) => { return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }); // Add custom function jexl.addFunction('distance', (lat1, lon1, lat2, lon2) => { // Haversine formula implementation const R = 6371; // Earth's radius in kilometers const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); }); // Usage const slug = jexl.evalSync('title | slugify', { title: "Hello World!" }); // "hello-world" const km = jexl.evalSync('distance(40.7128, -74.0060, 34.0522, -118.2437)'); // ~3944 ``` ## Monaco Editor Integration See the dedicated [Monaco Editor Integration](./monaco-integration) guide for setting up rich IDE features like: * Syntax highlighting * IntelliSense auto-completion * Error detection and hints * Hover documentation * Go to definition ## Performance Tips 1. **Compile frequently used expressions**: ```javascript const compiled = jexl.compile('complex | expression | here'); // Reuse compiled expression multiple times ``` 2. **Use synchronous evaluation when possible**: ```javascript // Faster for simple expressions const result = jexl.evalSync(expression, context); ``` 3. **Optimize context objects**: ```javascript // Only include necessary data in context const minimalContext = { user: data.user, settings: data.settings }; ``` ## Next Steps * **Language Guide** - Learn [JEXL syntax](../language/) that works across all implementations * **Function Reference** - Browse all [built-in functions](../reference/) * **Monaco Integration** - Set up [rich editor experience](./monaco-integration) * **Examples** - Check the [test files](https://github.com/konnektr-io/jexl-extended/tree/main/test) for more examples # Monaco Editor Integration # Monaco Editor Integration JEXL Extended provides built-in Monaco Editor support with syntax highlighting, IntelliSense completion, hover documentation, and error detection. ## Installation ```bash npm install jexl-extended monaco-editor ``` ## Basic Setup ```typescript import * as monaco from "monaco-editor"; import { Monaco } from "jexl-extended"; // Register JEXL language support Monaco.registerJexlLanguage(monaco); // Create an editor with JEXL support const editor = Monaco.createJexlEditor( monaco, document.getElementById("editor"), { value: 'data.users | filter(.age > 18) | map(.name)', height: 400, theme: 'vs-dark' } ); ``` ## React Integration ```tsx import React, { useRef, useEffect } from 'react'; import * as monaco from 'monaco-editor'; import { Monaco } from 'jexl-extended'; const JexlEditor: React.FC<{ value: string; onChange: (value: string) => void }> = ({ value, onChange }) => { const editorRef = useRef(null); const monacoRef = useRef(null); useEffect(() => { if (editorRef.current) { // Register JEXL language Monaco.registerJexlLanguage(monaco); // Create editor monacoRef.current = Monaco.createJexlEditor(monaco, editorRef.current, { value, height: 300, theme: 'vs-dark' }); // Listen for changes monacoRef.current.onDidChangeModelContent(() => { onChange(monacoRef.current!.getValue()); }); } return () => { monacoRef.current?.dispose(); }; }, []); return
; }; ``` ## Vue Integration ```vue ``` ## Features The Monaco integration provides: * **Syntax Highlighting** - Color coding for JEXL expressions * **IntelliSense** - Auto-completion for all 80+ functions * **Hover Documentation** - Function descriptions on hover * **Error Detection** - Real-time syntax validation * **Code Folding** - Collapse arrays and objects ## Configuration Options ```typescript const editor = Monaco.createJexlEditor(monaco, container, { value: '', // Initial expression height: 400, // Editor height theme: 'vs-dark', // vs, vs-dark, or vs-light fontSize: 14, // Font size minimap: { enabled: false }, // Show/hide minimap lineNumbers: 'on', // Show line numbers wordWrap: 'on', // Enable word wrapping automaticLayout: true // Auto-resize with container }); ``` ## Getting Values ```typescript // Get current expression const expression = editor.getValue(); // Set new expression editor.setValue('new.expression | here'); // Listen for changes editor.onDidChangeModelContent(() => { const currentValue = editor.getValue(); console.log('Expression changed:', currentValue); }); ``` That's it! The Monaco integration handles all the complex setup internally, so you can focus on providing a great expression editing experience for your users. # Python Implementation # Python Implementation The Python implementation of JEXL Extended provides identical functionality to the JavaScript version with Pythonic APIs. ## Installation ```bash pip install pyjexl-extended ``` ## Basic Usage ```python from pyjexl_extended import jexl # Simple expression evaluation result = jexl.eval('5 + 3 * 2') # 11 # With context data context = {'name': 'Alice', 'scores': [85, 92, 78]} greeting = jexl.eval('"Hello " + name', context) # "Hello Alice" average = jexl.eval('scores | average', context) # 85 ``` ## Expression Examples Based on the actual test suite, here are working examples: ### String Operations ```python # String conversion and manipulation jexl.eval('string(123)') # "123" jexl.eval('123456|string') # "123456" jexl.eval('{a:123456}|string') # '{"a":123456}' # Case conversion jexl.eval('uppercase("hello world")') # "HELLO WORLD" jexl.eval('lowercase("HELLO WORLD")') # "hello world" jexl.eval('"FOObar"|lower') # "foobar" # camelCase and PascalCase jexl.eval('"foo bar "|camelCase') # "fooBar" jexl.eval('$camelCase("Foo_bar")') # "fooBar" jexl.eval('"FooBar"|toCamelCase') # "fooBar" jexl.eval('"foo bar"|toPascalCase') # "FooBar" jexl.eval('"fooBar"|toPascalCase') # "FooBar" # Substring operations jexl.eval('substring(123456,2,2)') # "34" jexl.eval('$substring("test",(-2))') # "st" jexl.eval('"hello world"|substringBefore(" ")') # "hello" jexl.eval('substringBefore("hello world", "o")') # "hell" jexl.eval('"hello world"|substringAfter(" ")') # "world" jexl.eval('substringAfter("hello world", "x")') # "" # String utilities jexl.eval('trim(" baz ")') # "baz" jexl.eval('trim("__baz--","--")') # "__baz" jexl.eval('pad("foo",5)') # "foo " jexl.eval('pad("foo",(-5),0)') # "00foo" jexl.eval('"foo-bar"|contains("bar")') # True jexl.eval('"foo-bar"|contains("baz")') # False ``` ### Array Operations ```python # Array manipulation jexl.eval('["foo", "bar", "baz"]|append("tek")') # ['foo', 'bar', 'baz', 'tek'] jexl.eval('["tek", "baz", "bar", "foo"]|reverse') # ['foo', 'bar', 'baz', 'tek'] jexl.eval('["tek", "baz", "bar", "foo", "foo"]|reverse|distinct') # ['foo', 'bar', 'baz', 'tek'] # Array splitting and joining jexl.eval('split("foo-bar", "-")') # ['foo', 'bar'] jexl.eval('split("foo-bar", "-")[1]') # "bar" jexl.eval('join(["foo", "bar"], "-")') # "foo-bar" jexl.eval('["foo", "bar"]|join') # "foo,bar" # Contains operations jexl.eval('"foo-bar"|contains("bar")') # True jexl.eval('["foo-bar"]|contains("bar")') # False jexl.eval('["foo-bar"]|contains("foo-bar")') # True jexl.eval('["baz", "foo", "bar"]|contains("bar")') # True # Object operations jexl.eval('{foo:0, bar:1, baz:2, tek:3}|keys') # ['foo', 'bar', 'baz', 'tek'] jexl.eval('{a:"foo", b:"bar", c:"baz", d:"tek"}|values') # ['foo', 'bar', 'baz', 'tek'] ``` ### Mathematical Operations ```python # Number operations jexl.eval('$number("1")') # 1 jexl.eval('$number("1.1")') # 1.1 jexl.eval('$number("-1.1")') # -1.1 jexl.eval('$number(-1.1)|floor') # -2 jexl.eval('$number("10.6")|ceil') # 11 jexl.eval('10.123456|round(2)') # 10.12 jexl.eval('10.123456|toInt') # 10 jexl.eval('"10.123456"|toInt') # 10 jexl.eval('3|power(2)') # 9 jexl.eval('3|power') # 9 jexl.eval('9|sqrt') # 3 jexl.eval('random() < 1') # True # Formatting jexl.eval('16325.62|formatNumber("0,0.000")') # "16,325.620" jexl.eval('12|formatBase(16)') # "c" jexl.eval('16325.62|formatInteger("0000000")') # "0016325" # Aggregations jexl.eval('[1,2,3]|sum') # 6 jexl.eval('sum(1,2,3,4,5)') # 15 jexl.eval('[1,3]|sum(1,2,3,4,5)') # 19 jexl.eval('[1,3]|sum([1,2,3,4,5])') # 19 jexl.eval('[1,3]|max([1,2,3,4,5])') # 5 jexl.eval('[2,3]|min([1,2,3,4,5])') # 1 jexl.eval('[4,5,6]|avg') # 5 ``` ### Boolean Operations ```python # Boolean conversion jexl.eval('1|toBoolean') # True jexl.eval('3|toBoolean') # True jexl.eval('"1"|toBoolean') # True jexl.eval('0|toBool') # False jexl.eval('"false"|toBool') # False jexl.eval('"False"|toBool') # False jexl.eval('"fALSE"|toBool') # False jexl.eval('"tRUE "|toBoolean') # True # Logical operations jexl.eval('"False"|toBool|not') # True jexl.eval('"TRUE"|toBool|not') # False ``` ### Encoding and Conversion ```python # Base64 encoding jexl.eval('base64Encode("foobar")') # "Zm9vYmFy" jexl.eval('base64Decode("Zm9vYmFy")') # "foobar" # URL encoding jexl.eval('{foo:"bar",baz:"tek"}|formUrlEncoded') # "foo=bar&baz=tek" # Text replacement jexl.eval('replace("foo-bar", "-", "_")') # "foo_bar" jexl.eval('replace("foo-bar---", "-", "")') # "foobar" jexl.eval('"123ab123ab123ab"|replace("123")') # "ababab" ``` ## Integration Patterns ### Flask Application ```python from flask import Flask, request, jsonify from pyjexl_extended import jexl app = Flask(__name__) @app.route('/api/filter', methods=['POST']) def filter_data(): data = request.json expression = data.get('expression', '') context = data.get('context', {}) try: result = jexl.eval(expression, context) return jsonify({'result': result}) except Exception as e: return jsonify({'error': str(e)}), 400 # Usage: # POST /api/filter # { # "expression": "users | filter(\"value.active\") | length", # "context": {"users": [...]} # } ``` ### Django Integration ```python from django.http import JsonResponse from pyjexl_extended import jexl import json def evaluate_expression(request): if request.method == 'POST': data = json.loads(request.body) expression = data.get('expression') context = data.get('context', {}) try: result = jexl.eval(expression, context) return JsonResponse({'result': result}) except Exception as e: return JsonResponse({'error': str(e)}, status=400) return JsonResponse({'error': 'Method not allowed'}, status=405) ``` ### Data Processing Pipeline ```python from pyjexl_extended import jexl def process_user_data(users): """Process user data using JEXL expressions""" # Filter active users active_users = jexl.eval('users | filter("value.active")', {'users': users}) # Calculate statistics stats = { 'total_users': jexl.eval('users | length', {'users': users}), 'active_users': jexl.eval('users | length', {'users': active_users}), 'average_age': jexl.eval('users | average("value.age")', {'users': active_users}), 'departments': jexl.eval('users | map("value.department") | distinct', {'users': active_users}) } return stats # Usage users = [ {'name': 'Alice', 'age': 30, 'active': True, 'department': 'Engineering'}, {'name': 'Bob', 'age': 25, 'active': False, 'department': 'Marketing'}, {'name': 'Carol', 'age': 35, 'active': True, 'department': 'Engineering'} ] stats = process_user_data(users) print(stats) ``` ### Configuration System ```python from pyjexl_extended import jexl class ConfigManager: def __init__(self, config_data, user_context): self.config = config_data self.user_context = user_context def get_feature_flag(self, feature_name): """Evaluate feature flag based on user context""" if feature_name not in self.config['features']: return False feature = self.config['features'][feature_name] condition = feature.get('condition', 'true') context = { **self.user_context, 'config': self.config } try: return jexl.eval(condition, context) except: return False def get_setting(self, setting_name, default=None): """Get configuration setting with conditional logic""" if setting_name not in self.config['settings']: return default setting = self.config['settings'][setting_name] if isinstance(setting, dict) and 'condition' in setting: context = {**self.user_context, 'config': self.config} if jexl.eval(setting['condition'], context): return setting['value'] else: return setting.get('default', default) return setting # Usage config = { 'features': { 'premium_feature': { 'condition': 'user.subscription == "premium" || user.role == "admin"' }, 'beta_features': { 'condition': 'user.beta_tester == true' } }, 'settings': { 'max_file_size': { 'condition': 'user.subscription == "premium"', 'value': '100MB', 'default': '10MB' } } } user_context = { 'user': { 'id': 123, 'subscription': 'premium', 'role': 'user', 'beta_tester': False } } config_manager = ConfigManager(config, user_context) has_premium = config_manager.get_feature_flag('premium_feature') # True max_size = config_manager.get_setting('max_file_size', '5MB') # "100MB" ``` ## Error Handling ```python from pyjexl_extended import jexl def safe_eval(expression, context, default_value=None): """Safely evaluate JEXL expression with error handling""" try: return jexl.eval(expression, context) except Exception as e: print(f"Expression evaluation failed: {e}") return default_value # Usage context = {'user': {'name': 'John'}} result = safe_eval('user.profile.email', context, 'No email') # "No email" name = safe_eval('user.name | uppercase', context, 'Unknown') # "JOHN" ``` ## Validation Example ```python from pyjexl_extended import jexl def validate_form(form_data): """Validate form using JEXL expressions""" rules = { 'email': 'email && email | contains("@") && email | contains(".")', 'age': 'age >= 18 && age <= 120', 'password': 'password && password | length >= 8', 'confirm_password': 'password == confirm_password' } results = {} for field, rule in rules.items(): try: results[field] = jexl.eval(rule, form_data) except: results[field] = False return results # Usage form_data = { 'email': 'user@example.com', 'age': 25, 'password': 'securepass123', 'confirm_password': 'securepass123' } validation_results = validate_form(form_data) all_valid = all(validation_results.values()) ``` ## Testing JEXL Expressions ```python import unittest from pyjexl_extended import jexl class TestJexlExpressions(unittest.TestCase): def setUp(self): self.context = { 'users': [ {'name': 'Alice', 'age': 30, 'active': True}, {'name': 'Bob', 'age': 25, 'active': False} ] } def test_string_operations(self): self.assertEqual(jexl.eval('string(123)'), "123") self.assertEqual(jexl.eval('"hello"|uppercase'), "HELLO") self.assertEqual(jexl.eval('"WORLD"|lowercase'), "world") def test_array_operations(self): result = jexl.eval('users | filter("value.active") | length', self.context) self.assertEqual(result, 1) names = jexl.eval('users | map("value.name")', self.context) self.assertEqual(names, ['Alice', 'Bob']) def test_math_operations(self): self.assertEqual(jexl.eval('[1,2,3] | sum'), 6) self.assertEqual(jexl.eval('3 | power(2)'), 9) self.assertEqual(jexl.eval('9 | sqrt'), 3) if __name__ == '__main__': unittest.main() ``` ## Performance Considerations 1. **Reuse the JEXL instance**: The `jexl` object can be reused across evaluations 2. **Minimize context size**: Only include necessary data in the context 3. **Cache results**: For expensive operations, consider caching results 4. **Use appropriate data types**: Python lists and dicts work efficiently with JEXL ```python # Efficient batch processing expressions = [ 'users | length', 'users | filter("value.active") | length', 'users | average("value.age")' ] results = {} for i, expr in enumerate(expressions): results[f'metric_{i}'] = jexl.eval(expr, context) ``` ## Common Patterns ### Data Transformation ```python # Transform API response api_data = { 'users': [ {'first_name': 'John', 'last_name': 'Doe', 'email': 'john@example.com'}, {'first_name': 'Jane', 'last_name': 'Smith', 'email': 'jane@example.com'} ] } transformed = jexl.eval(''' users | map("{ fullName: value.first_name + ' ' + value.last_name, email: value.email, domain: value.email | substringAfter('@') }") ''', api_data) print(transformed) # [ # {'fullName': 'John Doe', 'email': 'john@example.com', 'domain': 'example.com'}, # {'fullName': 'Jane Smith', 'email': 'jane@example.com', 'domain': 'example.com'} # ] ``` ### Conditional Logic ```python # Business rules evaluation user = {'age': 25, 'premium': True, 'country': 'US'} discount = jexl.eval(''' user.premium ? 0.2 : ( user.age < 25 ? 0.1 : ( user.country == "US" ? 0.05 : 0 ) ) ''', {'user': user}) print(f"Discount: {discount * 100}%") # Discount: 20.0% ``` ## Next Steps * **Language Guide** - Learn [JEXL syntax](../language/) that works across all implementations * **Function Reference** - Browse all [built-in functions](../reference/) * **GitHub Repository** - Check [pyjexl-extended](https://github.com/konnektr-io/pyjexl-extended) for latest updates * **Test Examples** - See the [test files](https://github.com/konnektr-io/pyjexl-extended/tree/main/tests) for more examples # Function Reference # Function Reference JEXL Extended provides over 80 built-in functions and transforms organized into the following categories: * [**Math**](./math): Mathematical operations and calculations (11 functions, 10 transforms) * [**Array**](./array): Array operations and transformations (17 functions, 17 transforms) * [**Encoding**](./encoding): Data encoding and formatting utilities (3 functions, 3 transforms) * [**Conversion**](./conversion): Convert between different data types (8 functions, 8 transforms) * [**String**](./string): String manipulation and formatting functions (14 functions, 14 transforms) * [**Utility**](./utility): General utility functions (6 functions, 5 transforms) * [**DateTime**](./datetime): Date and time operations (8 functions, 6 transforms) * [**Object**](./object): Object manipulation and inspection (3 functions, 3 transforms) ## Usage Functions are called directly: ```javascript abs(-5) // Returns: 5 max([1, 2, 3]) // Returns: 3 ``` Transforms are used with the pipe operator: ```javascript "hello world" | uppercase // Returns: "HELLO WORLD" [1, 2, 3] | map("value * 2") // Returns: [2, 4, 6] ``` Many functions can also be used as transforms when they have a single parameter. # Access Control # Access Control KtrlPlane uses a robust Role-Based Access Control (RBAC) system to ensure that users have exactly the permissions they need—no more, no less. Permissions are managed hierarchically across three scopes: **Organization**, **Project**, and **Resource**. ## Permission Scopes Access can be granted at three different levels. Permissions granted at a higher level automatically inherit down to lower levels. | Scope | Description | Example | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **Organization** | The top-level boundary. Roles assigned here apply to the entire organization and all projects within it. | granting `Owner` on the Organization gives full control over all projects. | | **Project** | A workspace for related resources. Roles assigned here apply to all resources within the project. | Granting `Viewer` on a Project allows a user to see all resources in that project. | | **Resource** | The individual deployable unit (e.g., a specific Graph or Secret). Roles assigned here apply ONLY to that specific resource. | Granting `Editor` on a specific Secret allows modifying that secret but nothing else. | ## System Roles KtrlPlane provides a set of pre-defined roles designed to cover common access patterns. These roles are categorized into **Platform Access**, **Data Access**, and **Resource-Specific Access**. ### Platform Access Roles These roles control access to the KtrlPlane control plane (configuration, deployment, settings). | Role | Permissions | Best For | | ---------- | ------------------------------------------------------------------------------------------------- | ------------------------------------- | | **Owner** | Full administrative access. Can manage billing, users, roles, and delete the scope (Project/Org). | Organization Admins, Project Leads | | **Editor** | Can create, update, and delete resources. Cannot manage user access or billing. | Developers, DevOps Engineers | | **Viewer** | Read-only access to configuration and status. Cannot make any changes. | Auditors, Support Staff, Stakeholders | ### Data Access Roles These roles control access to the *data plane*—the actual data stored within your resources. | Role | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **Konnektr.Data.Owner** | Grants full access to the data within resources. For example, allows querying all nodes in a Graph or reading values in a Secret. | **Control Plane vs. Data Plane**: An `Editor` can change the configuration of a database (Control Plane) but might not have permission to read the customer data inside it (Data Plane). Assign `Konnektr.Data.Owner` to grant data access. ### Resource-Specific Roles These roles are tailored for specific resource types and offer more granular control. * **Konnektr.Graph.Owner**, **Konnektr.Graph.Editor**, **Konnektr.Graph.Viewer** * **Konnektr.Flow\.Owner**, **Konnektr.Flow\.Editor**, **Konnektr.Flow\.Viewer** * **Konnektr.Secret.Owner**, **Konnektr.Secret.Editor**, **Konnektr.Secret.Viewer** Use these when you need to grant access to a specific *type* of resource without granting blanket access to the entire project. ## Inheritance Model Permissions are additive and hierarchical. * **Organization Owner** implies **Project Owner** for all projects. * **Project Viewer** implies **Resource Viewer** for all resources in that project. Best Practice: Assign roles at the **Project** level for teams to reduce management overhead. Use **Resource** level assignments only for exceptions (e.g., a specific secret that only one person should see). ## Managing Access To manage access: 1. Navigate to the scope you want to manage (Organization Settings, Project Settings, or Resource Details). 2. Click on the **"Access"** or **"Permissions"** tab. 3. Click **"Add Assignment"**. 4. Select the **User** (by email) and the **Role**. 5. Save the assignment. Effective permissions are calculated instantly, allowing users immediate access. # Organizations # Organizations Organizations are the highest-level grouping construct in KtrlPlane. They represent a company, business unit, or major team boundary. ## Purpose * Group related projects * Provide a shared billing context * Define a top-level RBAC boundary (org\_owner, org\_admin, org\_viewer) * Serve as a logical tenant anchor for multi-tenancy ## Key Characteristics | Attribute | Description | | ------------- | -------------------------------------------------------------- | | Multi-tenancy | Each organization establishes isolation across data and access | | Billing | Can own billing settings or allow project-specific overrides | | Membership | Users may belong to multiple organizations | | Governance | Central place to enforce policies and audit access | ## Lifecycle 1. Creation: An initial owner establishes the organization 2. Project Provisioning: Projects are added under the organization 3. Growth: Roles and billing are refined as usage scales 4. Decommissioning: Resources and projects archived prior to deletion ## RBAC Overview Organization-level roles often influence which projects are visible and manageable. ## Best Practices * Use descriptive names (e.g., "Acme Data Platform") * Centralize billing at the organization unless specific chargeback is required * Periodically audit role assignments ## Next Steps * [Projects](/concepts/projects) * [Access Control](/concepts/access-control) # Projects # Projects Projects are operational workspaces that group resources and define an RBAC boundary for day-to-day execution. ## Purpose * Provide a focused context for an application or workload * Group related resources (Graph, Flow, Assembler, Compass) * Act as a billing scope (inherit or override organization billing) * Enable scoped role assignments (project\_owner, project\_editor, project\_viewer) ## When to Create a New Project Create a new project when: * You have a logical application boundary * Different teams manage separate workloads * You require isolated access control for a feature set * You want to segment billing or usage reporting ## Project Lifecycle 1. Initialization: Project defined with name and organization association 2. Resource Provisioning: Platform resources created inside the project 3. Expansion: Additional roles and monitoring added 4. Evolution: Configuration updated based on scaling needs 5. Archival: Project retired and resources terminated ## RBAC Scope Project-level roles govern access to all resources within the project unless overridden by resource-specific roles. ## Billing Inheritance Projects can inherit billing from the parent organization or specify their own subscription context. ## Best Practices * Use consistent naming patterns (e.g., `data-analytics-prod`) * Keep resource count manageable for clarity * Regularly review resource health and RBAC assignments ## Next Steps * [Resources](/concepts/resources) * [Access Control](/concepts/access-control) # Resources # Resources Resources are the core deployable services and applications within the Konnektr Platform. They represent the actual functionality you're building and deploying - from graph databases to AI-powered builders. ## What Are Resources? Resources in KtrlPlane are: * **Deployable services** that provide specific functionality * **Managed instances** of Konnektr products (Graph, Flow, Assembler, Compass) * **Configurable applications** with specific settings and tiers * **Billable entities** that consume compute, storage, and network resources ## Available Resource Types ### Konnektr.Graph **High-performance graph database and API layer** * **Purpose**: Store and query connected data for digital twin models * **Use Cases**: * IoT device networks and relationships * Digital twin data models * Knowledge graphs * Social networks and organizational structures * **Key Features**: * CRUD operations for nodes and relationships * Advanced graph query capabilities * Real-time data synchronization * Built-in analytics and reporting **Configuration Options:** ```json { "database_name": "my-digital-twins", "enable_analytics": true, "backup_retention_days": 7, "query_timeout_seconds": 30, "max_concurrent_connections": 100, "enable_audit_logging": false } ``` **Available Tiers:** * **Free**: Development and testing usage * **Pro**: Production workloads with higher limits * **Enterprise**: Custom limits and dedicated support ### Konnektr.Flow **Real-time data and event processing** * **Purpose**: Process streaming data and handle events in real-time * **Use Cases**: * Sensor data processing and transformation * Event-driven workflows and automation * Real-time analytics and alerting * Data pipeline orchestration * **Key Features**: * Visual flow design interface * Pre-built connectors and transformations * Error handling and retry mechanisms * Scalable event processing **Configuration Options:** ```json { "max_concurrent_flows": 10, "retention_days": 30, "enable_dead_letter_queue": true, "processing_mode": "streaming", "auto_scaling": { "enabled": true, "min_instances": 1, "max_instances": 5 } } ``` **Available Tiers:** * **Free**: Development and testing usage * **Pro**: Production workloads with higher limits * **Enterprise**: Custom limits and dedicated support Tier limits and enforcement are planned features. Currently, all tiers provide full functionality for development and testing. ### Konnektr.Assembler **AI-powered digital twin builder** * **Purpose**: Automatically generate digital twin models using AI * **Use Cases**: * Automated DTDL model generation * Data source integration and mapping * Intelligent schema inference * Model validation and optimization * **Key Features**: * AI-powered model generation * Multiple data source connectors * Visual model editor * Validation and testing tools **Configuration Options:** ```json { "model_complexity": "standard", "output_format": "dtdl_v2", "confidence_threshold": 0.8 } ``` **Available Tiers:** * **Free**: Development and testing usage * **Pro**: Production workloads with higher limits * **Enterprise**: Custom limits and dedicated support ### Konnektr.Compass (Coming Soon) **Navigation and discovery tools** * **Purpose**: Intelligent navigation and discovery for digital twin networks * **Use Cases**: * Intelligent search and discovery * Automated relationship mapping * Navigation optimization * Recommendation systems ## Resource Lifecycle ### 1. Creation When you create a resource: 1. **Selection**: Choose the resource type and tier 2. **Configuration**: Set up resource-specific settings 3. **Validation**: KtrlPlane validates your configuration 4. **Provisioning**: Infrastructure is allocated and configured 5. **Deployment**: The resource is deployed and becomes available ### 2. Running State Once deployed, resources provide: * **Service endpoints** for accessing functionality * **Configuration management** for updating settings * **Monitoring and logging** for operational visibility * **Scaling capabilities** based on usage patterns ### 3. Management Throughout their lifecycle, you can: * **Update configuration** to modify behavior * **Scale up or down** to handle changing demands * **Monitor performance** and usage metrics * **Access logs and diagnostics** for troubleshooting ### 4. Termination When you no longer need a resource: 1. **Backup data** if needed (some services have automatic backups) 2. **Remove dependencies** from other resources 3. **Delete the resource** through the KtrlPlane interface 4. **Billing stops** when the resource is fully terminated ## Resource Configuration ### Settings Schema Each resource type has a specific JSON schema for configuration: ```typescript // Example: Graph Resource Settings interface GraphSettings { database_name: string; enable_analytics: boolean; backup_retention_days: number; query_timeout_seconds: number; max_concurrent_connections: number; enable_audit_logging: boolean; } ``` ### Configuration Best Practices 1. **Start Conservative**: Begin with modest resource limits and scale up as needed 2. **Use Descriptive Names**: Choose clear, descriptive names for your resources 3. **Plan for Growth**: Consider future scaling needs when setting initial configuration 4. **Security First**: Enable security features like audit logging when available 5. **Regular Reviews**: Periodically review and optimize your resource configurations ### Dynamic Configuration Many settings can be updated without recreating the resource: * **Scaling parameters** (memory, CPU, connections) * **Retention policies** (backup, logging, data retention) * **Security settings** (access control, audit logging) * **Performance tuning** (timeouts, caching, optimization) ## Resource Tiers and Billing ### Understanding Tiers Each resource type offers multiple tiers: * **Free Tier**: Perfect for development, testing, and small projects * **Pro Tier**: Production-ready with higher limits and additional features * **Enterprise Tier**: Maximum performance, dedicated infrastructure, and SLA ### Billing Model Resources are billed based on: 1. **Base tier cost**: Monthly subscription for the selected tier 2. **Usage-based charges**: Additional costs for usage beyond tier limits 3. **Storage costs**: Data storage charges for persistent data 4. **Network egress**: Charges for data transfer out of the platform ### Cost Optimization * **Right-sizing**: Choose appropriate tiers based on actual usage * **Monitoring**: Use built-in monitoring to track resource utilization * **Scaling**: Configure auto-scaling to optimize costs * **Cleanup**: Remove unused resources to avoid ongoing charges ## Resource Access and Security ### Access URLs Each resource provides secure access endpoints: ``` https://graph-abc123.konnektr.io/api/v1 https://flow-def456.konnektr.io/webhooks https://assembler-ghi789.konnektr.io/models ``` ### Authentication Resources use multiple authentication methods: * **API Keys**: For programmatic access * **JWT Tokens**: For user-based authentication * **OAuth 2.0**: For third-party integrations * **Service Accounts**: For service-to-service communication ### Network Security * **TLS Encryption**: All traffic is encrypted in transit * **VPC Isolation**: Enterprise tiers offer network isolation * **IP Whitelisting**: Restrict access to specific IP ranges * **Private Endpoints**: Direct private connectivity options ## Resource Integration ### Inter-Resource Communication Resources within the same project can communicate securely: ```json { "flow_config": { "graph_endpoint": "graph://my-graph-resource/api/v1", "assembler_endpoint": "assembler://my-assembler-resource/models" } } ``` ### External Integrations Connect resources to external services: * **Webhooks**: Receive notifications from external systems * **API Connectors**: Pull data from external APIs * **Database Connections**: Connect to external databases * **Message Queues**: Integrate with messaging systems ### SDK and Libraries Each resource type provides: * **REST APIs**: Standard HTTP-based interfaces * **SDKs**: Language-specific client libraries * **GraphQL**: Query-based APIs where applicable * **WebSocket**: Real-time bidirectional communication ## Monitoring and Observability ### Built-in Metrics All resources provide standard metrics: * **Performance**: Response times, throughput, error rates * **Resource Utilization**: CPU, memory, storage usage * **Business Metrics**: Custom metrics specific to each resource type * **Cost Metrics**: Resource consumption and billing information ### Logging Comprehensive logging capabilities: * **Application Logs**: Resource-specific operational logs * **Access Logs**: All API calls and user interactions * **Audit Logs**: Security and configuration changes * **Performance Logs**: Detailed performance and debugging information ### Alerting Set up alerts for: * **Performance Issues**: High response times or error rates * **Resource Limits**: Approaching tier limits or quotas * **Security Events**: Unauthorized access attempts * **Cost Thresholds**: Unexpected billing increases ## Next Steps Now that you understand resources: 1. **[Create Your First Resource](/getting-started/first-project)**: Deploy a resource in your project 2. **[Resource Management Guide](/guides/resources)**: Learn advanced resource management 3. **[Billing and Optimization](/guides/billing)**: Understand costs and optimization strategies 4. **[API Integration](/api/resources)**: Learn how to integrate with resource APIs Start with free tiers to experiment and learn. You can always upgrade to higher tiers as your needs grow and you become more familiar with each resource type. # Creating Your First Project # Creating Your First Project Projects are the core workspaces in KtrlPlane where you organize and deploy your resources. This guide will walk you through creating, configuring, and managing your first project. ## Understanding Projects A project in KtrlPlane is: * **A workspace** for related resources and applications * **A billing boundary** where costs can be tracked separately * **An access control scope** where you can grant team members specific permissions * **A deployment environment** for your Konnektr resources Projects belong to organizations. If you haven't created an organization yet, follow the [Quick Start Guide](/getting-started/quick-start) first. ## Step 1: Navigate to Project Creation 1. Log in to KtrlPlane and select your organization 2. From the organization dashboard, you'll see your projects (if any) 3. Click the **"New Project"** button or **"Create Your First Project"** if this is your first one ## Step 2: Basic Project Information Fill out the project creation form: ### Project Name * Choose a descriptive name that reflects the project's purpose * Examples: "IoT Sensor Network", "Customer Analytics Platform", "Smart Building Management" * Names must be unique within your organization ### Description (Optional) * Add a brief description explaining the project's goals * This helps team members understand the project's purpose * Examples: "Real-time monitoring system for manufacturing equipment", "Customer behavior analysis for e-commerce platform" ### Project ID * Automatically generated based on your project name * Used in URLs and API calls * Can be customized if needed (must be unique) ## Step 3: Advanced Configuration ### Billing Configuration You have two options for project billing: #### Option 1: Inherit from Organization (Recommended) * Uses your organization's billing account and payment method * Simplifies billing management * All project costs appear on the organization's invoice * **Choose this if**: You want centralized billing management #### Option 2: Separate Project Billing * Set up dedicated billing for this project * Requires separate payment method configuration * Useful for client projects or cost center tracking * **Choose this if**: You need separate invoicing or charge-backs ### Access Control Template Choose who can access your project initially: * **Private**: Only you have access (you can invite others later) * **Organization Members**: All organization members get viewer access * **Custom**: Define specific roles for organization members You can always modify access permissions later through the project's Access Control settings. ## Step 4: Create the Project 1. Review your configuration 2. Click **"Create Project"** 3. Wait for the project to be created (usually takes a few seconds) 4. You'll be redirected to your new project dashboard ## Step 5: Project Dashboard Overview Your project dashboard provides: ### Quick Stats * Number of resources deployed * Current monthly usage and costs * Active team members * Project health status ### Recent Activity * Recent resource deployments * Team member access changes * Configuration updates * Billing events ### Quick Actions * **Add Resource**: Deploy a new Konnektr service * **Invite Team Member**: Grant project access to someone * **View Billing**: Check usage and costs * **Project Settings**: Modify project configuration ## Step 6: Deploy Your First Resource Now that your project is created, let's deploy your first resource: 1. Click **"Add Resource"** from the project dashboard 2. Choose a resource type based on your needs: ### Konnektr.Graph * **Best for**: Storing and querying connected data * **Use cases**: Digital twin data models, relationship mapping, IoT device networks * **Free tier**: Up to 1,000 nodes and 10,000 relationships ### Konnektr.Flow * **Best for**: Real-time data processing and event handling * **Use cases**: Sensor data processing, alert systems, data transformation pipelines * **Free tier**: Up to 1,000 events per hour ### Konnektr.Assembler * **Best for**: AI-powered digital twin creation * **Use cases**: Automated model generation, data source integration, intelligent mapping * **Free tier**: 5 AI generations per month 3. Select your preferred tier (Free, Pro, or Enterprise) 4. Configure the resource settings 5. Click **"Create Resource"** ## Step 7: Configure Resource Settings Each resource type has specific configuration options: ### Graph Database Settings ```json { "database_name": "my-digital-twins", "enable_analytics": true, "backup_retention_days": 7, "access_control": { "enable_row_level_security": false } } ``` ### Flow Processing Settings ```json { "max_concurrent_flows": 10, "retention_days": 30, "enable_dead_letter_queue": true, "auto_scaling": { "enabled": true, "max_instances": 5 } } ``` ### Assembler AI Settings ```json { "model_complexity": "standard", "auto_validation": true, "output_format": "dtdl_v2", "training_data_retention": "30_days" } ``` Don't worry about getting all settings perfect initially. You can modify most configuration options after the resource is deployed. ## Step 8: Monitor Deployment After creating a resource: 1. You'll see the deployment progress in real-time 2. Deployment typically takes 2-5 minutes depending on the resource type 3. You'll receive an email notification when deployment completes 4. The resource will appear in your project dashboard with a "Running" status ## Managing Your Project ### Project Settings Access project settings to: * Change project name and description * Modify billing configuration * Update access control policies * Configure integrations and webhooks ### Team Management Invite team members and manage permissions: * **Viewer**: Can see resources and their configurations * **Editor**: Can create, modify, and delete resources * **Admin**: Can manage billing and team access * **Owner**: Full project control (transferable) ### Resource Management From your project dashboard you can: * View all deployed resources * Check resource health and status * Access resource configuration * Monitor usage and performance * Scale resources up or down ## Next Steps Now that you have your first project set up: 1. **[Explore Resource Management](/guides/resources)**: Learn advanced resource configuration 2. **[Set Up Billing Alerts](/guides/billing)**: Monitor costs and set up notifications 3. **[Configure Access Control](/guides/access-control)**: Fine-tune team permissions 4. **[Connect External Services](/guides/integrations)**: Integrate with your existing tools ## Project Best Practices ### Naming Conventions * Use descriptive, consistent names * Include environment indicators (dev, staging, prod) * Consider using prefixes for different project types ### Resource Organization * Group related resources in the same project * Use separate projects for different environments * Consider project size limits for better management ### Access Control * Follow the principle of least privilege * Regularly review team access * Use temporary access for contractors or external collaborators ### Cost Management * Set up billing alerts for your projects * Monitor resource usage regularly * Use free tiers for development and testing 🎉 You've successfully created and configured your first project! You're now ready to build and deploy powerful digital twin solutions using the Konnektr Platform. # Understanding Organizations # Understanding Organizations Organizations are the foundational structure in KtrlPlane. They represent your company, team, or personal workspace and serve as the top-level container for all your projects and resources. ## What is an Organization? An organization in KtrlPlane is: * **The top-level entity** that contains all your projects and resources * **A billing boundary** where payment methods and subscriptions are managed * **A team workspace** where you collaborate with colleagues * **An administrative scope** for managing company-wide policies and settings Think of an organization like a company account on other platforms - it's where your team collaborates and where billing is centralized. ## Organization Structure ``` Organization (Acme Corp) ├── Project A (IoT Platform) │ ├── Resource 1 (Graph Database) │ ├── Resource 2 (Flow Processor) │ └── Resource 3 (Assembler AI) ├── Project B (Analytics Dashboard) │ ├── Resource 1 (Graph Database) │ └── Resource 2 (Flow Processor) └── Project C (Mobile App Backend) └── Resource 1 (Graph Database) ``` ## Creating Your Organization ### First Organization When you first sign up for KtrlPlane, you'll be prompted to create your initial organization: 1. **Organization Name**: Choose a name that represents your team or company 2. **Description**: Optional description of your organization's purpose 3. **Billing Email**: Email address for billing notifications (can be different from your account email) ### Additional Organizations You can create multiple organizations if you: * Work with different companies or teams * Need separate billing for different business units * Manage client projects that require isolation To create additional organizations: 1. Click your profile menu in the top right 2. Select **"Switch Organization"** 3. Click **"Create New Organization"** 4. Fill out the organization details ## Organization Settings ### Basic Information Manage your organization's core details: * **Name**: The display name for your organization * **Description**: Brief description of your organization's purpose * **Organization ID**: Unique identifier used in URLs and APIs * **Contact Information**: Primary contact details ### Billing Configuration Organizations handle billing for all contained projects (unless projects have separate billing): * **Payment Methods**: Credit cards, bank accounts, or invoicing * **Billing Address**: Address for tax calculations and invoices * **Tax Information**: VAT numbers, tax exemption certificates * **Billing Contacts**: Who receives billing notifications and invoices ### Default Settings Set organization-wide defaults that apply to new projects: * **Default Resource Tiers**: Which tiers to suggest for new resources * **Access Control Policies**: Default permissions for organization members * **Compliance Settings**: Security and compliance requirements * **Integration Defaults**: Default connections to external services ## Team Management ### Organization Roles There are several roles available at the organization level: #### Owner * **Full control** over the organization * Can delete the organization * Manage billing and payment methods * Transfer ownership to another member * **Limit**: Only one owner per organization #### Admin * **Administrative access** to organization settings * Create and manage projects * Invite and remove team members * Configure billing (but not delete payment methods) * **Cannot**: Delete organization or transfer ownership #### Member * **Standard user access** * Can view projects they have access to * Can create new projects (if allowed by org policy) * Cannot modify organization settings * **Access**: Determined by individual project permissions #### Viewer * **Read-only access** to organization information * Can see organization projects (but not access them unless specifically granted) * Cannot create projects or modify settings * Useful for stakeholders who need visibility but not control ### Inviting Team Members 1. Go to **Organization Settings > Team Management** 2. Click **"Invite Team Member"** 3. Enter their email address 4. Select their organization role 5. Optionally set an expiration date for the invitation 6. Click **"Send Invitation"** The invited user will receive an email with instructions to join your organization. ### Managing Existing Members From the team management page, you can: * **Change roles**: Promote or demote team members * **Remove members**: Remove access to the organization * **View activity**: See when members last accessed the organization * **Manage invitations**: Cancel pending invitations or resend them ## Billing and Subscriptions ### Organization-Level Billing Organizations can have their own billing configuration: * **Primary payment method** for all projects in the organization * **Consolidated invoicing** for all resource usage * **Usage-based billing** across all projects * **Subscription management** for organization-wide features ### Project Billing Inheritance Projects can either: 1. **Inherit billing** from the organization (recommended) 2. **Use separate billing** with their own payment methods Benefits of inheritance: * Simplified billing management * Consolidated invoices * Easier cost tracking across projects * Reduced administrative overhead ### Setting Up Billing 1. Go to **Organization Settings > Billing** 2. Add a payment method (credit card, bank account, or request invoicing) 3. Configure your billing address and tax information 4. Set up billing notifications and contacts 5. Choose your subscription plan (if applicable) ## Organization Best Practices ### Naming Strategy * Use your actual company or team name * Keep it professional and recognizable * Avoid abbreviations that might be confusing to team members ### Team Structure * **Start small**: Begin with just essential team members * **Use appropriate roles**: Don't give everyone admin access * **Regular reviews**: Periodically review team access and roles * **Clear policies**: Document who can invite new members and create projects ### Billing Management * **Centralized approach**: Use organization-level billing for most use cases * **Set up alerts**: Configure billing notifications to avoid surprises * **Regular monitoring**: Review usage and costs monthly * **Budget planning**: Use project-level cost tracking for budgeting ### Security Considerations * **Enable two-factor authentication** for all team members * **Regular access reviews** to ensure only current team members have access * **Audit logs** to track important changes to the organization * **Compliance settings** based on your industry requirements ## Multiple Organizations ### When to Use Multiple Organizations Create separate organizations when you need: 1. **Complete isolation** between different business units 2. **Separate billing** for different companies or clients 3. **Different compliance requirements** (e.g., HIPAA vs. non-HIPAA projects) 4. **Distinct team structures** with no overlap ### Managing Multiple Organizations * **Organization switching**: Use the organization switcher in the top navigation * **Unified billing**: Set up consolidated billing across organizations if needed * **Cross-organization collaboration**: Invite the same users to multiple organizations * **Consistent policies**: Apply similar security and compliance settings ### Organization Consolidation If you find you have too many organizations: 1. **Evaluate necessity**: Determine if separation is still needed 2. **Plan migration**: Move projects between organizations if possible 3. **Billing consideration**: Understand billing implications of consolidation 4. **Team communication**: Inform all stakeholders about changes ## Next Steps Now that you understand organizations: 1. **[Create Your First Project](/getting-started/first-project)**: Set up a project within your organization 2. **[Configure Team Access](/guides/access-control)**: Learn advanced permission management 3. **[Set Up Billing](/guides/billing)**: Configure payment methods and billing policies 4. **[Explore Projects](/concepts/projects)**: Deep dive into project management Most teams should start with a single organization and create additional ones only when there's a clear business need for separation. # Quick Start Guide # Quick Start Guide Welcome to KtrlPlane! This guide will help you get started with the Control Plane for the Konnektr Platform in just a few minutes. ## What is KtrlPlane? KtrlPlane is the centralized management system for the Konnektr Platform. It provides: * **Organization Management**: Create and manage teams and workspaces * **Project Management**: Organize your applications and resources * **Resource Lifecycle**: Deploy and manage Konnektr products (Graph, Flow, Assembler, Compass) * **Access Control**: Role-based permissions for secure collaboration * **Billing**: Integrated subscription and usage-based billing ## Prerequisites To use KtrlPlane, you need: * A modern web browser (Chrome, Firefox, Safari, Edge) * An internet connection * An email address for account creation ## Step 1: Create Your Account 1. Visit the KtrlPlane login page 2. Click **"Sign Up"** to create a new account 3. Enter your email address and create a secure password 4. Verify your email address by clicking the link sent to your inbox 5. Complete your profile setup ## Step 2: Create Your First Organization Organizations are the top-level containers in KtrlPlane. They represent your company, team, or personal workspace. 1. After logging in, you'll be prompted to create your first organization 2. Enter your organization name (e.g., "Acme Corp", "Personal Projects") 3. Add an optional description 4. Click **"Create Organization"** You can create multiple organizations if you work with different teams or manage separate businesses. ## Step 3: Create Your First Project Projects are workspaces within your organization where you'll deploy and manage your resources. 1. From your organization dashboard, click **"Create Project"** 2. Enter a project name (e.g., "IoT Dashboard", "Customer Analytics") 3. Add a description explaining what this project is for 4. Click **"Create Project"** ## Step 4: Deploy Your First Resource Resources are the actual Konnektr products and services you'll use in your project. 1. Navigate to your project dashboard 2. Click **"Add Resource"** 3. Choose from available resource types: * **Graph**: High-performance graph database for digital twin data * **Flow**: Real-time data and event processing * **Assembler**: AI-powered digital twin builder * **Compass**: Navigation and discovery tools (coming soon) 4. Select a resource tier (Free, Pro, Enterprise) 5. Configure your resource settings 6. Click **"Create Resource"** Start with the Free tier to explore the platform without any cost. You can always upgrade later as your needs grow. ## Step 5: Invite Team Members If you're working with a team, you can invite collaborators to your organization or specific projects. 1. Go to your organization or project settings 2. Click **"Access Control"** 3. Click **"Invite User"** 4. Enter their email address 5. Select their role: * **Viewer**: Can view resources but not modify them * **Editor**: Can create and modify resources * **Admin**: Full access including billing and user management * **Owner**: Complete control (only one per organization/project) 6. Click **"Send Invitation"** ## What's Next? Now that you have KtrlPlane set up, you can: * **[Create More Projects](/getting-started/first-project)**: Learn advanced project management * **[Explore Resource Types](/concepts/resources)**: Understand what each Konnektr product offers * **[Set Up Billing](/guides/billing)**: Configure payment methods and subscriptions * **[Manage Access Control](/guides/access-control)**: Fine-tune permissions for your team ## Need Help? If you run into any issues: * Check our **[FAQ](/support/faq)** for common questions * Visit our **[Troubleshooting Guide](/support/troubleshooting)** * Reach out to our **[Community](/support/community)** for help 🎉 Congratulations! You've successfully set up KtrlPlane and deployed your first resource. You're now ready to build amazing digital twin solutions with the Konnektr Platform. # Development Setup # Development Setup This guide will help you set up a local development environment for contributing to KtrlPlane. Whether you're fixing bugs, adding features, or improving documentation, this guide will get you up and running quickly. ## Prerequisites Before you begin, ensure you have the following installed: ### Required Software * **Go 1.21+**: Backend development * **Node.js 18+**: Frontend development * **pnpm**: Package management for frontend * **PostgreSQL 14+**: Local database * **Git**: Version control * **Docker** (optional): For containerized development ### Development Tools (Recommended) * **VS Code**: With Go and TypeScript extensions * **Postman/Insomnia**: API testing * **pgAdmin/DBeaver**: Database administration * **GitHub CLI**: For easier contribution workflow ## Quick Setup ### 1. Clone the Repository ```bash git clone https://github.com/konnektr-io/ktrlplane.git cd ktrlplane ``` ### 2. Database Setup **Option A: Local PostgreSQL** ```bash # Create database and user sudo -u postgres psql CREATE USER ktrlplane_dev WITH ENCRYPTED PASSWORD 'dev_password'; CREATE DATABASE ktrlplane_dev OWNER ktrlplane_dev; GRANT ALL PRIVILEGES ON DATABASE ktrlplane_dev TO ktrlplane_dev; \q ``` **Option B: Docker PostgreSQL** ```bash cd deployments/docker docker-compose up -d postgres ``` ### 3. Backend Setup ```bash # Install Go dependencies go mod tidy # Copy example configuration cp config.yaml.example config.yaml # Edit config.yaml with your database settings nano config.yaml # Run database migrations go run cmd/migrate/main.go # Start the backend server go run cmd/server/main.go ``` The backend will be available at `http://localhost:8080` ### 4. Frontend Setup ```bash cd web # Install dependencies pnpm install # Start development server pnpm dev ``` The frontend will be available at `http://localhost:5173` ### 5. Verify Setup 1. Open your browser to `http://localhost:5173` 2. You should see the KtrlPlane login page 3. Backend health check: `curl http://localhost:8080/health` ## Detailed Setup ### Configuration The `config.yaml` file contains all application configuration: ```yaml # config.yaml database: host: "localhost" port: 5432 username: "ktrlplane_dev" password: "dev_password" database: "ktrlplane_dev" ssl_mode: "disable" auth: domain: "your-dev-auth0-domain.auth0.com" audience: "https://api.ktrlplane.localhost" client_id: "your-auth0-client-id" server: port: 8080 cors: allowed_origins: - "http://localhost:5173" - "http://localhost:3000" logging: level: "debug" format: "text" ``` ### Environment Variables For local development, you can also use environment variables: ```bash # Create .env file cat > .env << EOF DB_HOST=localhost DB_PORT=5432 DB_USER=ktrlplane_dev DB_PASSWORD=dev_password DB_NAME=ktrlplane_dev DB_SSL_MODE=disable AUTH_DOMAIN=your-dev-auth0-domain.auth0.com AUTH_AUDIENCE=https://api.ktrlplane.localhost AUTH_CLIENT_ID=your-auth0-client-id LOG_LEVEL=debug EOF # Source environment variables source .env ``` ### Auth0 Development Setup For authentication to work, you'll need an Auth0 development account: 1. Create a free Auth0 account at [auth0.com](https://auth0.com) 2. Create a new Application (Single Page Application) 3. Configure callback URLs: * `http://localhost:5173/callback` * `http://localhost:5173/silent-callback` 4. Configure logout URLs: `http://localhost:5173` 5. Create an API with identifier: `https://api.ktrlplane.localhost` 6. Update your `config.yaml` with Auth0 settings ## Project Structure Understanding the codebase structure: ``` ktrlplane/ ├── cmd/ # Application entry points │ ├── migrate/ # Database migration tool │ └── server/ # HTTP server ├── internal/ # Private application code │ ├── api/ # HTTP handlers and routes │ ├── auth/ # Authentication logic │ ├── config/ # Configuration management │ ├── db/ # Database layer │ ├── models/ # Data models │ └── service/ # Business logic ├── migrations/ # SQL migration files ├── web/ # React frontend │ ├── src/ │ │ ├── components/ # Reusable UI components │ │ ├── features/ # Feature-specific code │ │ ├── hooks/ # Custom React hooks │ │ ├── lib/ # Utility libraries │ │ ├── pages/ # Page components │ │ └── store/ # State management ├── deployments/ # Deployment configurations ├── docs/ # Documentation └── README.md ``` ## Development Workflow ### 1. Backend Development **Starting the backend:** ```bash # Development with auto-reload (install air first) go install github.com/cosmtrek/air@latest air # Or run directly go run cmd/server/main.go ``` **Running tests:** ```bash # All tests go test ./... # Specific package go test ./internal/service/... # With coverage go test -cover ./... ``` **Database migrations:** ```bash # Create new migration migrate create -ext sql -dir migrations -seq add_new_feature # Run migrations go run cmd/migrate/main.go # Or use make make migrate ``` ### 2. Frontend Development **Starting the frontend:** ```bash cd web pnpm dev ``` **Running tests:** ```bash # Unit tests pnpm test # E2E tests pnpm test:e2e # Test coverage pnpm test:coverage ``` **Linting and formatting:** ```bash # Lint pnpm lint # Fix linting issues pnpm lint:fix # Format code pnpm format ``` ### 3. Full Stack Development Use the provided VS Code tasks for efficient development: 1. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) 2. Type "Tasks: Run Task" 3. Choose from: * `go: run backend` - Start backend server * `vite: dev` - Start frontend development server ## Debugging ### Backend Debugging **Using VS Code:** 1. Install the Go extension 2. Set breakpoints in your code 3. Press F5 to start debugging 4. The debugger will attach to the running process **Using Delve directly:** ```bash # Install delve go install github.com/go-delve/delve/cmd/dlv@latest # Start debugging dlv debug cmd/server/main.go ``` ### Frontend Debugging **Browser DevTools:** * Use Chrome DevTools or Firefox Developer Tools * React components are visible in React DevTools extension * Zustand store can be inspected with Redux DevTools **VS Code Debugging:** 1. Install the "Debugger for Chrome" extension 2. Set breakpoints in TypeScript/JavaScript files 3. Use the debug configuration in `.vscode/launch.json` ## Testing ### Backend Tests **Unit Tests:** ```bash # Run all tests go test ./... # Run tests with verbose output go test -v ./... # Run tests for specific package go test -v ./internal/service/ ``` **Integration Tests:** ```bash # Start test database cd deployments/docker docker-compose -f docker-compose.test.yml up -d # Run integration tests go test -tags=integration ./... ``` **Test Coverage:** ```bash # Generate coverage report go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html open coverage.html ``` ### Frontend Tests **Unit Tests:** ```bash cd web # Run all tests pnpm test # Watch mode pnpm test:watch # Coverage pnpm test:coverage ``` **E2E Tests:** ```bash # Install Playwright browsers pnpm exec playwright install # Run E2E tests pnpm test:e2e # Run in UI mode pnpm test:e2e:ui ``` ## Database Development ### Migrations Create new migrations for schema changes: ```bash # Create migration file migrate create -ext sql -dir migrations -seq add_user_preferences # This creates: # migrations/001_add_user_preferences.up.sql # migrations/001_add_user_preferences.down.sql ``` **Migration best practices:** * Always create both up and down migrations * Test migrations on a copy of production data * Keep migrations small and focused * Never modify existing migrations in production ### Schema Changes ```sql -- Example migration: 005_add_user_preferences.up.sql CREATE TABLE user_preferences ( user_id VARCHAR(255) PRIMARY KEY, preferences JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_user_preferences_user_id ON user_preferences(user_id); ``` ```sql -- Example rollback: 005_add_user_preferences.down.sql DROP TABLE IF EXISTS user_preferences; ``` ## API Development ### Adding New Endpoints 1. **Define the model** in `internal/models/models.go` 2. **Add service methods** in `internal/service/` 3. **Create handlers** in `internal/api/handlers.go` 4. **Add routes** in `internal/api/routes.go` 5. **Write tests** for all layers **Example:** ```go // 1. Model (internal/models/models.go) type UserPreference struct { UserID string `json:"user_id" db:"user_id"` Preferences json.RawMessage `json:"preferences" db:"preferences"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // 2. Service (internal/service/user_service.go) func (s *UserService) GetPreferences(userID string) (*UserPreference, error) { // Implementation } // 3. Handler (internal/api/handlers.go) func (h *APIHandler) GetUserPreferences(c *gin.Context) { // Implementation } // 4. Route (internal/api/routes.go) apiV1.GET("/users/:userId/preferences", handler.GetUserPreferences) ``` ## Code Quality ### Go Standards Follow Go best practices: ```bash # Format code go fmt ./... # Vet code go vet ./... # Run linters (install golangci-lint first) golangci-lint run ``` ### TypeScript Standards Follow TypeScript and React best practices: ```bash cd web # Type checking pnpm type-check # Linting pnpm lint # Formatting pnpm format ``` ## Git Workflow ### Branch Naming * `feature/add-user-preferences` - New features * `bugfix/fix-login-issue` - Bug fixes * `docs/update-api-docs` - Documentation updates * `refactor/optimize-database-queries` - Code refactoring ### Commit Messages Use conventional commits: ``` feat: add user preferences endpoint fix: resolve authentication timeout issue docs: update API documentation refactor: optimize database connection pooling test: add unit tests for billing service ``` ### Pull Request Process 1. Create feature branch from `main` 2. Make your changes 3. Add tests for new functionality 4. Update documentation if needed 5. Ensure all tests pass 6. Create pull request with descriptive title and description 7. Request review from maintainers ## Troubleshooting Development Issues ### Common Backend Issues **Database connection errors:** ```bash # Check PostgreSQL is running sudo systemctl status postgresql # Check connection psql -h localhost -U ktrlplane_dev -d ktrlplane_dev -c "SELECT version();" ``` **Port conflicts:** ```bash # Check what's using port 8080 lsof -i :8080 sudo netstat -tulpn | grep :8080 ``` ### Common Frontend Issues **Node/pnpm version conflicts:** ```bash # Check versions node --version pnpm --version # Use Node Version Manager if needed nvm use 18 ``` **Module resolution issues:** ```bash # Clear cache and reinstall rm -rf node_modules pnpm-lock.yaml pnpm install ``` ## VS Code Configuration Recommended VS Code settings for the project: ```json // .vscode/settings.json { "go.toolsManagement.checkForUpdates": "local", "go.useLanguageServer": true, "go.lintOnSave": "package", "go.formatTool": "goimports", "typescript.preferences.importModuleSpecifier": "relative", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } } ``` Recommended extensions: * Go (Google) * TypeScript and JavaScript (Microsoft) * ES7+ React/Redux/React-Native snippets * Prettier - Code formatter * ESLint * GitLens ## Next Steps After setting up your development environment: 1. **[Read the Architecture Guide](/development/architecture)** - Understand the system design 2. **[Review Contributing Guidelines](/development/contributing)** - Learn how to contribute effectively 3. **[Explore the API](/development/api)** - Understand the API design patterns 4. **[Write Tests](/development/testing)** - Learn the testing strategy Join our developer community on Discord to get help, share ideas, and collaborate with other contributors! # Creating Resources # Creating Resources This guide walks you through the process of creating new resources in your KtrlPlane projects. KtrlPlane uses a unified wizard to make resource creation simple and consistent across different types. ## The Creation Wizard To start creating a resource: 1. Navigate to your **Project** dashboard. 2. Click the **"Add Resource"** button in the top right or the **"Create Resource"** card in the dashboard. ### Step 1: Select Resource Type Choose the type of resource you need from the catalog. * **Konnektr.Graph**: A high-performance graph database for digital twins. * **Konnektr.Flow**: A real-time event processing and routing engine. * **Konnektr.Assembler**: An AI-powered tool for generating data models. ### Step 2: Configure Details 1. **Name**: Enter a unique display name for your resource. 2. **ID**: A DNS-compliant ID will be automatically generated from your name. You can customize this if needed, but it must be unique within the project. 3. **Tier**: Select a performance tier. * **Free**: Good for development and testing. Shared infrastructure. * **Pro**: For production workloads requiring guaranteed performance. * **Enterprise**: Dedicated infrastructure with custom SLAs. ### Step 3: Billing (If Required) If you select a paid tier (Pro or Enterprise) and haven't set up billing for your project yet, you will be prompted to: 1. Enter your billing details. 2. Add a payment method via the secure Stripe portal. *Note: You only need to do this once per project.* ### Step 4: Resource Configuration Different resources require different configuration settings. * **Graphs**: configuring database names, backup retention, and scaling options. * **Flows**: configuring processing modes (streaming/batch), retention policies, and instance counts. * **Secrets**: Add key-value pairs for sensitive data. Refer to the specific product documentation for detailed configuration parameters for each resource type. ### Step 5: Creation and Deployment Once configured, click **Create Resource**. KtrlPlane will provision your dedicated infrastructure. This process typically takes 1-3 minutes. You will be redirected to the resource details page automatically. ## Next Steps After your resource is created: 1. **View Details**: Check the resource overview for status and connection endpoints. 2. **Manage Access**: Use the **Access** tab to grant permissions to other users or service accounts. 3. **Check Logs**: Use the **Logs** tab to monitor initialization and runtime events. # Resource Management Guide # Resource Management Guide This comprehensive guide covers everything you need to know about managing resources in KtrlPlane, from initial deployment to optimization and troubleshooting. ## Creating Resources For a detailed step-by-step walkthrough of the creation process, please refer to the **[Creating Resources](/guides/creating-resources)** guide. ## Secret Management KtrlPlane employs a modern, secure approach to secret management that minimizes the need for handling long-lived static credentials. ### Federated Identity For integrations with external services like Kafka or Azure Data Explorer, KtrlPlane utilizes **Federated Identity** (Workload Identity). This means you don't need to manually create or store secrets for these connections. Instead, you configure the connection details (like endpoints or cluster URIs), and authentication is handled securely and automatically via OAUTHBEARER tokens. ### Project Credentials (M2M) For programmatic access to the KtrlPlane API (e.g., for CI/CD pipelines or external scripts), you should use **Service Accounts**. 1. Navigate to your **Project Dashboard**. 2. Locate the **API Authentication** card. 3. You will see your **Client ID** and **Client Secret**. 4. Use these credentials to obtain an access token via the standard OAuth2 Client Credentials flow. Store your Client Secret securely. It is only visible to Project Admins and allows full programmatic access to your project's resources. For more details on using these credentials, see the [Service Accounts](/guides/service-accounts) guide. ## Managing Existing Resources ### Resource Dashboard Your resource dashboard provides: **Status Overview** * Current operational status * Health and performance metrics * Recent activity and events * Resource utilization **Quick Actions** * Access resource endpoints * View configuration * Update settings * Scale resource * Access logs ### Updating Resource Configuration Most settings can be updated without recreating the resource: 1. Navigate to your resource 2. Click **"Settings"** 3. Modify the configuration 4. Click **"Save Changes"** 5. Monitor the update process Some configuration changes may cause brief service interruptions. Plan updates during maintenance windows when possible. ### Scaling Resources #### Manual Scaling Adjust resource capacity based on current needs: 1. Go to resource settings 2. Find the scaling section 3. Adjust capacity limits or performance tiers 4. Apply changes #### Auto-Scaling (Future Feature) Automatic scaling based on resource metrics is planned for future releases. Currently, you can manually adjust resource configuration through the dashboard or API. ## Resource Integration ### Inter-Resource Communication Resources within the same project can communicate using internal service discovery: ```javascript // Graph to Flow integration const flowClient = new FlowClient({ endpoint: process.env.FLOW_INTERNAL_ENDPOINT, authentication: 'service_account' }); // Assembler to Graph integration const graphClient = new GraphClient({ endpoint: process.env.GRAPH_INTERNAL_ENDPOINT, authentication: 'service_account' }); ``` ### External Integrations Connect your resources to external services: #### API Integration ```javascript // Configure external API endpoint in resource settings { "external_apis": { "weather_service": { "endpoint": "https://api.weather.com/v1", "authentication": "api_key", "rate_limit": 1000 } } } ``` #### Webhook Configuration ```json { "webhooks": { "incoming": { "endpoint": "/webhooks/data-ingestion", "authentication": "hmac_sha256", "payload_validation": true }, "outgoing": { "alerts": "https://your-system.com/alerts", "events": "https://your-system.com/events" } } } ``` ## Security and Access Control ### API Key Management Each resource provides API keys for programmatic access: 1. Navigate to resource settings 2. Go to **"API Keys"** section 3. Click **"Generate New Key"** 4. Configure key permissions and expiration 5. Copy the key (it won't be shown again) ### Resource-Level Permissions Control who can access and modify resources: * **Viewer**: Read-only access to resource data and configuration * **Editor**: Can modify configuration and manage the resource * **Admin**: Full access including security settings and key management ### Network Security Configure network access controls: ```json { "network_security": { "ip_whitelist": [ "192.168.1.0/24", "10.0.0.0/8" ], "enable_private_endpoints": true, "require_tls": true, "min_tls_version": "1.2" } } ``` ## Monitoring and Observability ### Built-in Monitoring KtrlPlane provides comprehensive monitoring for all resources: **Performance Metrics** * Response time percentiles (p50, p95, p99) * Request throughput and error rates * Resource utilization (CPU, memory, storage) * Custom business metrics **Operational Metrics** * Service availability and uptime * Deployment and configuration changes * Error patterns and trends * Cost and billing information ### Custom Monitoring Add custom metrics to your resources: ```javascript // Graph resource custom metrics const metrics = { nodes_created_per_minute: nodeCreationRate, query_complexity_average: averageQueryComplexity, relationship_depth_max: maxRelationshipDepth }; await graphResource.recordMetrics(metrics); ``` ### Alerting Set up alerts for important events: 1. Go to resource monitoring 2. Click **"Create Alert"** 3. Choose alert conditions: * Performance thresholds * Error rate limits * Resource utilization * Cost thresholds 4. Configure notification channels (email, Slack, webhooks) 5. Save alert configuration ## Troubleshooting ### Common Issues and Solutions #### High Response Times **Symptoms**: Slow API responses, timeouts **Solutions**: * Scale up resource capacity * Optimize queries or processing logic * Enable caching * Check for resource bottlenecks #### Resource Unavailability **Symptoms**: Service unreachable, connection errors **Solutions**: * Check resource status in dashboard * Review recent configuration changes * Verify network connectivity * Check API key validity #### High Costs **Symptoms**: Unexpected billing increases **Solutions**: * Review resource utilization metrics * Optimize resource configuration * Implement auto-scaling policies * Consider tier adjustments ### Diagnostic Tools #### Resource Logs Access detailed logs for troubleshooting: ```bash # Using KtrlPlane CLI ktrlplane logs --resource-id res-abc123 --follow # Via API curl -H "Authorization: Bearer $API_KEY" \ https://ktrlplane.konnektr.io/v1/resources/res-abc123/logs ``` #### Health Checks Monitor resource health: ```json { "health_check": { "endpoint": "/health", "interval_seconds": 30, "timeout_seconds": 10, "healthy_threshold": 2, "unhealthy_threshold": 3 } } ``` ## Best Practices ### Configuration Management * **Use version control**: Track configuration changes * **Environment consistency**: Use similar configs across environments * **Gradual rollouts**: Test configuration changes in staging first * **Backup configurations**: Keep copies of working configurations ### Performance Optimization * **Right-sizing**: Choose appropriate resource sizes * **Caching strategies**: Implement appropriate caching * **Connection pooling**: Use connection pools for database resources * **Batch processing**: Use batch operations where possible ### Security Practices * **Rotate API keys**: Regularly rotate authentication keys * **Principle of least privilege**: Grant minimum required permissions * **Network segmentation**: Use private networks where possible * **Audit logging**: Enable comprehensive audit logs ### Cost Management * **Resource monitoring**: Regularly review resource usage * **Auto-scaling**: Use auto-scaling to optimize costs * **Cleanup unused resources**: Remove resources no longer needed * **Tier optimization**: Regularly review and optimize tier selections ## Advanced Features ### Blue-Green Deployments For critical resources, use blue-green deployment strategies: 1. Create a new resource with updated configuration 2. Test the new resource thoroughly 3. Gradually shift traffic from old to new resource 4. Remove the old resource once migration is complete ### Multi-Region Deployment Deploy resources across multiple regions for high availability: ```json { "deployment": { "strategy": "multi_region", "regions": ["us-east-1", "eu-west-1", "asia-southeast-1"], "replication": { "mode": "active_active", "consistency": "eventual" } } } ``` ### Disaster Recovery Implement disaster recovery for critical resources: ```json { "disaster_recovery": { "backup_frequency": "daily", "backup_retention": "30_days", "cross_region_backup": true, "automated_failover": { "enabled": true, "rto_minutes": 15, "rpo_minutes": 5 } } } ``` ## Next Steps Now that you understand resource management: 1. **[Explore API Integration](/api/resources)**: Learn how to integrate with your applications 2. **[Set Up Monitoring](/guides/monitoring)**: Implement comprehensive monitoring 3. **[Optimize Costs](/guides/billing)**: Learn advanced cost optimization strategies 4. **[Security Hardening](/guides/security)**: Implement enterprise security practices 🎉 You're now equipped to effectively manage resources in KtrlPlane! Remember to start simple and gradually add complexity as your needs grow. # Service Accounts (M2M) # Service Accounts for Permission Checking ## Overview Service accounts allow your backend services to authenticate with KtrlPlane and check user permissions on behalf of those users. This is useful when: * Your service has a different Auth0 audience than KtrlPlane * You need to verify user permissions without forwarding user tokens * Auth0's on-behalf-of flow isn't available for your use case ## How It Works 1. **M2M Application**: Create an Auth0 Machine-to-Machine application 2. **Client Credentials Flow**: Your service authenticates using client ID and secret 3. **Special Permission**: Grant the M2M app permission to check permissions on behalf of users 4. **API Call**: Call `/api/v1/permissions/check` with a `userId` parameter ## Setup Guide ### Step 1: Create Auth0 M2M Application 1. Go to your Auth0 Dashboard → Applications → Create Application 2. Choose "Machine to Machine Applications" 3. Name it (e.g., "My Service - KtrlPlane Integration") 4. Select the KtrlPlane API as the authorized API 5. Grant the necessary permissions (the API must be configured to allow M2M access) 6. Note the **Client ID** and **Client Secret** ### Step 2: Grant Service Account Permission The service account needs a special permission to check permissions on behalf of users. You'll need to create a role assignment at the global scope. #### Option A: Using SQL (Recommended for initial setup) ```sql -- 1. Find or create a role with the special permission INSERT INTO ktrlplane.roles (role_id, name, description, scope_type, display_order, created_at, updated_at) VALUES ( 'service-account-permission-checker', 'Service Account: Permission Checker', 'Allows service accounts to check permissions on behalf of users', 'global', 1000, NOW(), NOW() ) ON CONFLICT (role_id) DO NOTHING; -- 2. Add the check_permissions_on_behalf_of permission to the role INSERT INTO ktrlplane.role_permissions (role_id, permission_id) VALUES ('service-account-permission-checker', '00000000-0001-0000-0000-000000000006') ON CONFLICT DO NOTHING; -- 3. Assign the role to your service account (use the client ID as user_id) INSERT INTO ktrlplane.role_assignments ( assignment_id, user_id, -- This is the Auth0 client ID (the 'sub' from the M2M token) role_id, scope_type, scope_id, assigned_by, created_at, updated_at ) VALUES ( gen_random_uuid(), 'YOUR_M2M_CLIENT_ID_HERE', -- Replace with your M2M application's client ID 'service-account-permission-checker', 'global', 'global', 'system', -- Or use your admin user ID NOW(), NOW() ); ``` #### Option B: Using the API (Future Enhancement) Currently, service account role assignments must be created via SQL. In the future, an API endpoint will be available for this. ### Step 3: Authenticate Your Service Your service needs to obtain an access token using the client credentials flow: ```typescript // Example using Node.js import axios from 'axios'; async function getServiceAccountToken() { const response = await axios.post(`https://YOUR_AUTH0_DOMAIN/oauth/token`, { grant_type: 'client_credentials', client_id: process.env.M2M_CLIENT_ID, client_secret: process.env.M2M_CLIENT_SECRET, audience: process.env.KTRLPLANE_API_AUDIENCE, }); return response.data.access_token; } ``` ```go // Example using Go package main import ( "encoding/json" "fmt" "net/http" "net/url" "strings" ) type TokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } func getServiceAccountToken(auth0Domain, clientID, clientSecret, audience string) (string, error) { data := url.Values{} data.Set("grant_type", "client_credentials") data.Set("client_id", clientID) data.Set("client_secret", clientSecret) data.Set("audience", audience) req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/oauth/token", auth0Domain), strings.NewReader(data.Encode())) if err != nil { return "", err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() var tokenResp TokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { return "", err } return tokenResp.AccessToken, nil } ``` ### Step 4: Check Permissions on Behalf of Users Now your service can check permissions for any user: ```typescript // Example using Node.js async function checkUserPermissions( userId: string, scopeType: string, scopeId: string ) { const token = await getServiceAccountToken(); const response = await axios.get( `${process.env.KTRLPLANE_API_URL}/api/v1/permissions/check`, { params: { userId: userId, // The user you're checking permissions for scopeType: scopeType, // e.g., "project", "organization", "resource" scopeId: scopeId, // The specific project/org/resource ID }, headers: { Authorization: `Bearer ${token}`, }, } ); return response.data.permissions; // Array of permission strings } // Usage example const permissions = await checkUserPermissions( 'auth0|123456789', // User's Auth0 sub claim 'project', 'my-project-id' ); if (permissions.includes('read')) { // User can read the project } ``` ```go // Example using Go func checkUserPermissions(token, userID, scopeType, scopeID string) ([]string, error) { url := fmt.Sprintf("%s/api/v1/permissions/check?userId=%s&scopeType=%s&scopeId=%s", os.Getenv("KTRLPLANE_API_URL"), url.QueryEscape(userID), url.QueryEscape(scopeType), url.QueryEscape(scopeID), ) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result struct { Permissions []string `json:"permissions"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result.Permissions, nil } ``` ## API Reference ### Check Permissions **Endpoint:** `GET /api/v1/permissions/check` **Authentication:** Bearer token (user token or service account token) **Query Parameters:** | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------------- | | `scopeType` | string | Yes | Type of scope: "organization", "project", "resource" | | `scopeId` | string | Yes | ID of the specific scope | | `userId` | string | No\* | User ID to check permissions for (M2M only) | \*Required when using service account to check another user's permissions **Response:** ```json { "user_id": "auth0|123456789", "scope_type": "project", "scope_id": "my-project-id", "permissions": [ "read", "write", "manage_access" ] } ``` **Error Responses:** * `400 Bad Request`: Missing required parameters * `401 Unauthorized`: Invalid or missing token * `403 Forbidden`: Service account lacks permission to check on behalf of users * `500 Internal Server Error`: Server-side error ## Security Considerations ### Why This Is Safe 1. **Explicit Permission Required**: Service accounts must be explicitly granted the `check_permissions_on_behalf_of` permission 2. **Global Scope Only**: This permission is only checked at the global scope, making it easy to audit 3. **Read-Only Operation**: Service accounts can only *check* permissions, not grant or modify them 4. **Audit Trail**: All permission checks are logged (if logging is enabled) ### Best Practices 1. **Limit Service Accounts**: Only create service accounts when necessary 2. **Rotate Credentials**: Regularly rotate client secrets 3. **Use Environment Variables**: Never hardcode credentials 4. **Monitor Usage**: Set up alerts for unusual permission check patterns 5. **Principle of Least Privilege**: Only grant the specific permission needed ### What Service Accounts CANNOT Do Service accounts with `check_permissions_on_behalf_of` permission: * ❌ Cannot modify user permissions or role assignments * ❌ Cannot create, update, or delete resources * ❌ Cannot impersonate users for other API calls * ❌ Cannot access user data beyond permission information * ✅ Can only read what permissions a user has on a specific scope ## Troubleshooting ### "Only service accounts can check permissions on behalf of other users" **Cause**: You're using a regular user token, not an M2M token. **Solution**: Ensure you're authenticating with client credentials flow, not a user token. ### "Service account does not have permission to check permissions on behalf of users" **Cause**: The service account hasn't been granted the `check_permissions_on_behalf_of` permission. **Solution**: Follow Step 2 in the setup guide to create the role assignment. ### How to Find Your M2M Client ID The client ID becomes the `sub` claim in the M2M token. To verify: 1. Decode your M2M token at [jwt.io](https://jwt.io) 2. Look for the `sub` claim - this is your client ID 3. Verify the `gty` claim is `"client-credentials"` Example token payload: ```json { "iss": "https://your-tenant.auth0.com/", "sub": "AbC123xyz@clients", // This is your client ID "aud": "https://api.ktrlplane.io", "gty": "client-credentials", // Identifies this as M2M token "azp": "AbC123xyz", "exp": 1234567890, "iat": 1234567890 } ``` ## Example: Integration with Konnektr.Graph Here's a real-world example of how Konnektr.Graph uses service accounts to verify user permissions: ```typescript // In Konnektr.Graph API middleware async function verifyUserCanAccessGraph(userId: string, graphResourceId: string) { // Get service account token const token = await getServiceAccountToken(); // Check if user has read permission on the graph resource const permissions = await checkUserPermissions( userId, 'resource', graphResourceId ); if (!permissions.includes('read')) { throw new UnauthorizedError('User does not have access to this graph'); } // Permission verified, proceed with graph operation return true; } ``` ## Related Documentation * [Access Control Overview](/concepts/access-control) * [RBAC API Reference](/api/rbac) * [Authentication](/api/authentication) # Installation Guide # Self-Hosting Installation Guide This guide covers how to install and deploy KtrlPlane in your own environment, whether that's on-premises, in your own cloud account, or in a private cloud. ## Overview KtrlPlane can be deployed in several ways: * **Docker Compose** (Development/Testing) * **Kubernetes** (Production - Recommended) * **Helm Chart** (Kubernetes with simplified configuration) * **Manual Installation** (Advanced users) ## Prerequisites ### System Requirements **Minimum Requirements:** * 2 CPU cores * 4 GB RAM * 20 GB storage * PostgreSQL 14+ * Docker or Kubernetes cluster **Recommended for Production:** * 4+ CPU cores * 8+ GB RAM * 100+ GB storage (depending on usage) * PostgreSQL 14+ with high availability * Kubernetes cluster with multiple nodes ### Software Dependencies * **Container Runtime**: Docker 20.10+ or containerd 1.5+ * **Database**: PostgreSQL 14+ * **Authentication**: Auth0 account (or compatible OIDC provider) * **TLS Certificates**: For HTTPS in production * **Load Balancer**: For high availability deployments ## Quick Start with Docker Compose For development or testing environments: ### Step 1: Clone Repository ```bash git clone https://github.com/konnektr-io/ktrlplane.git cd ktrlplane ``` ### Step 2: Configure Environment Create a `.env` file: ```bash # Database Configuration DB_HOST=postgres DB_PORT=5432 DB_NAME=ktrlplane_dev DB_USER=ktrlplane DB_PASSWORD=change_this_password # Auth0 Configuration AUTH_DOMAIN=your-domain.auth0.com AUTH_AUDIENCE=https://api.ktrlplane.yourdomain.com # Application Configuration API_BASE_URL=http://localhost:8080 FRONTEND_URL=http://localhost:5173 ``` ### Step 3: Start Services ```bash cd deployments/docker docker-compose up -d ``` This will start: * PostgreSQL database * KtrlPlane backend (port 8080) * KtrlPlane frontend (port 5173) ### Step 4: Run Database Migrations ```bash docker-compose exec backend /app/migrate ``` ### Step 5: Access KtrlPlane Open your browser to `http://localhost:5173` to access the KtrlPlane interface. ## Production Kubernetes Deployment For production environments, we recommend using Kubernetes: ### Step 1: Prerequisites Ensure you have: * Kubernetes cluster 1.21+ * kubectl configured * Helm 3.0+ * PostgreSQL database (managed service recommended) ### Step 2: Add Helm Repository ```bash helm repo add konnektr https://charts.konnektr.io helm repo update ``` ### Step 3: Create Configuration Create a `values.yaml` file: ```yaml # values.yaml global: domain: "ktrlplane.yourdomain.com" # Database configuration (external managed database recommended) postgresql: enabled: false # Using external database database: external: true host: "your-postgres-host.amazonaws.com" port: 5432 name: "ktrlplane_production" username: "ktrlplane" password: "your-secure-password" # Auth0 configuration auth: domain: "your-domain.auth0.com" audience: "https://api.ktrlplane.yourdomain.com" clientId: "your-auth0-client-id" # Backend configuration backend: image: repository: ghcr.io/konnektr-io/ktrlplane-backend tag: "v1.0.0" resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2000m memory: 4Gi autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 # Frontend configuration frontend: image: repository: ghcr.io/konnektr-io/ktrlplane-frontend tag: "v1.0.0" resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi # Ingress configuration ingress: enabled: true className: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/force-ssl-redirect: "true" tls: - secretName: ktrlplane-tls hosts: - ktrlplane.yourdomain.com # Security configuration securityContext: runAsNonRoot: true runAsUser: 10001 fsGroup: 10001 ``` ### Step 4: Install KtrlPlane ```bash helm install ktrlplane konnektr/ktrlplane \ --namespace ktrlplane \ --create-namespace \ --values values.yaml ``` ### Step 5: Run Database Migrations ```bash kubectl run migrate --rm -i --tty \ --image=ghcr.io/konnektr-io/ktrlplane-backend:v1.0.0 \ --restart=Never \ --env="DATABASE_URL=postgresql://user:pass@host:5432/dbname" \ -- /app/migrate ``` ### Step 6: Verify Installation ```bash # Check pod status kubectl get pods -n ktrlplane # Check ingress kubectl get ingress -n ktrlplane # Check logs kubectl logs -f deployment/ktrlplane-backend -n ktrlplane ``` ## Manual Installation For environments where Docker/Kubernetes isn't available: ### Step 1: Install Dependencies **Ubuntu/Debian:** ```bash # Install PostgreSQL sudo apt update sudo apt install postgresql postgresql-contrib # Install Go 1.21+ wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc # Install Node.js 18+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs # Install pnpm npm install -g pnpm ``` **CentOS/RHEL:** ```bash # Install PostgreSQL sudo dnf install postgresql postgresql-server postgresql-contrib sudo postgresql-setup --initdb sudo systemctl enable --now postgresql # Install Go and Node.js (similar to above) ``` ### Step 2: Setup Database ```bash # Switch to postgres user sudo -u postgres psql # Create database and user CREATE USER ktrlplane WITH ENCRYPTED PASSWORD 'your-secure-password'; CREATE DATABASE ktrlplane_production OWNER ktrlplane; GRANT ALL PRIVILEGES ON DATABASE ktrlplane_production TO ktrlplane; \q ``` ### Step 3: Build Backend ```bash git clone https://github.com/konnektr-io/ktrlplane.git cd ktrlplane # Build backend go build -o ktrlplane-server ./cmd/server # Run migrations export DATABASE_URL="postgresql://ktrlplane:your-password@localhost/ktrlplane_production?sslmode=disable" go run ./cmd/migrate ``` ### Step 4: Build Frontend ```bash cd web pnpm install pnpm build ``` ### Step 5: Configure Systemd Services Create `/etc/systemd/system/ktrlplane-backend.service`: ```ini [Unit] Description=KtrlPlane Backend After=network.target postgresql.service [Service] Type=simple User=ktrlplane WorkingDirectory=/opt/ktrlplane ExecStart=/opt/ktrlplane/ktrlplane-server Restart=always RestartSec=10 Environment=DB_HOST=localhost Environment=DB_PORT=5432 Environment=DB_NAME=ktrlplane_production Environment=DB_USER=ktrlplane Environment=DB_PASSWORD=your-secure-password Environment=AUTH_DOMAIN=your-domain.auth0.com Environment=AUTH_AUDIENCE=https://api.ktrlplane.yourdomain.com [Install] WantedBy=multi-user.target ``` Start the service: ```bash sudo systemctl enable --now ktrlplane-backend sudo systemctl status ktrlplane-backend ``` ### Step 6: Setup Web Server Configure nginx to serve the frontend and proxy API requests: ```nginx server { listen 80; server_name ktrlplane.yourdomain.com; # Redirect to HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name ktrlplane.yourdomain.com; ssl_certificate /etc/letsencrypt/live/ktrlplane.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ktrlplane.yourdomain.com/privkey.pem; # Frontend location / { root /opt/ktrlplane/web/dist; try_files $uri $uri/ /index.html; } # API proxy location /api { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Health check location /health { proxy_pass http://localhost:8080; } } ``` ## Configuration ### Environment Variables | Variable | Required | Description | Default | | --------------- | -------- | ------------------- | ------- | | `DB_HOST` | Yes | PostgreSQL hostname | - | | `DB_PORT` | Yes | PostgreSQL port | 5432 | | `DB_NAME` | Yes | Database name | - | | `DB_USER` | Yes | Database username | - | | `DB_PASSWORD` | Yes | Database password | - | | `AUTH_DOMAIN` | Yes | Auth0 domain | - | | `AUTH_AUDIENCE` | Yes | Auth0 API audience | - | | `PORT` | No | Server port | 8080 | | `LOG_LEVEL` | No | Logging level | info | ### Security Configuration For production deployments: ```yaml security: # Enable HTTPS only tls: enabled: true cert_file: /etc/ssl/certs/ktrlplane.crt key_file: /etc/ssl/private/ktrlplane.key # CORS configuration cors: allowed_origins: - "https://ktrlplane.yourdomain.com" allowed_methods: - GET - POST - PUT - DELETE allowed_headers: - Authorization - Content-Type # Rate limiting rate_limit: enabled: true requests_per_minute: 100 burst_size: 20 ``` ## High Availability Setup For production environments requiring high availability: ### Database High Availability Use PostgreSQL with read replicas: ```yaml database: primary: host: "pg-primary.yourdomain.com" port: 5432 read_replicas: - host: "pg-replica1.yourdomain.com" port: 5432 - host: "pg-replica2.yourdomain.com" port: 5432 ``` ### Application High Availability Deploy multiple backend instances behind a load balancer: ```yaml backend: replicas: 3 antiAffinity: enabled: true readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 ``` ## Monitoring and Observability ### Health Checks KtrlPlane provides health check endpoints: * `/health` - Basic health check * `/health/ready` - Readiness check (database connectivity) * `/health/live` - Liveness check ### Metrics Enable Prometheus metrics: ```yaml monitoring: metrics: enabled: true port: 9090 path: /metrics ``` ### Logging Configure structured logging: ```yaml logging: level: info format: json output: stdout ``` ## Backup and Recovery ### Database Backups Set up automated PostgreSQL backups: ```bash # Daily backup script #!/bin/bash BACKUP_DIR="/backups/ktrlplane" DATE=$(date +%Y%m%d_%H%M%S) pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > "$BACKUP_DIR/ktrlplane_$DATE.sql" # Compress backup gzip "$BACKUP_DIR/ktrlplane_$DATE.sql" # Clean up old backups (keep 30 days) find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete ``` ### Configuration Backups Backup your configuration files: ```bash # Backup Kubernetes configs kubectl get secret,configmap -n ktrlplane -o yaml > ktrlplane-config-backup.yaml # Backup Helm values helm get values ktrlplane -n ktrlplane > values-backup.yaml ``` ## Troubleshooting Installation ### Common Issues **Database Connection Failed** ```bash # Check connectivity telnet $DB_HOST $DB_PORT # Verify credentials psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT version();" ``` **Auth0 Configuration Issues** ```bash # Verify Auth0 settings curl -H "Authorization: Bearer $JWT_TOKEN" \ https://$AUTH_DOMAIN/.well-known/jwks.json ``` **Port Conflicts** ```bash # Check what's using port 8080 sudo lsof -i :8080 sudo netstat -tulpn | grep :8080 ``` ### Getting Help If you encounter issues: 1. Check the [troubleshooting guide](/self-hosting/troubleshooting) 2. Review application logs 3. Verify all prerequisites are met 4. Check the [community forums](/support/community) ## Next Steps After successful installation: 1. **[Configure Authentication](/self-hosting/authentication)**: Set up Auth0 integration 2. **[Database Setup](/self-hosting/database)**: Optimize database configuration 3. **[Monitoring](/self-hosting/monitoring)**: Set up comprehensive monitoring 4. **[Security Hardening](/self-hosting/security)**: Implement security best practices 🎉 Congratulations! You have successfully installed KtrlPlane. You can now access the web interface and start creating organizations, projects, and resources. # Frequently Asked Questions # Frequently Asked Questions Find answers to common questions about KtrlPlane, the Control Plane for the Konnektr Platform. ## General Questions ### What is KtrlPlane? KtrlPlane is the centralized Control Plane for the Konnektr Platform. It manages organizations, projects, resources, billing, and access control for all Konnektr products (Graph, Flow, Assembler, and Compass). ### How does KtrlPlane relate to other Konnektr products? KtrlPlane serves as the management layer for the entire Konnektr ecosystem: * **KtrlPlane**: Control Plane - manages users, projects, billing, and deployments * **Konnektr.Graph**: Graph database and API layer for digital twin data * **Konnektr.Flow**: Real-time data and event processing * **Konnektr.Assembler**: AI-powered digital twin builder * **Konnektr.Compass**: Navigation and discovery tools (coming soon) ### Is KtrlPlane free to use? KtrlPlane itself is free - you only pay for the resources you deploy. Each resource type (Graph, Flow, Assembler) offers free tiers perfect for development and small projects. ## Account and Authentication ### How do I create an account? Simply visit the KtrlPlane web interface and click "Sign Up". You'll need to provide an email address and create a password. Email verification is required before you can start using the platform. ### Can I use single sign-on (SSO)? Yes, KtrlPlane supports Auth0-based authentication, which can be configured for various SSO providers including Google, Microsoft, SAML, and more. ### How do I reset my password? Click "Forgot Password" on the login page and enter your email address. You'll receive a password reset link via email. ### Can I change my email address? Currently, email address changes must be done through your Auth0 profile. Contact support if you need assistance with this. ## Organizations and Projects ### What's the difference between organizations and projects? * **Organizations**: Top-level containers representing your company or team. Handle billing, team management, and company-wide policies. * **Projects**: Workspaces within organizations where you deploy and manage resources. Each project can have its own team members and access controls. ### How many organizations can I create? There's no limit on the number of organizations you can create. Most users start with one organization and create additional ones only when needed for business separation. ### Can I transfer projects between organizations? Currently, projects cannot be transferred between organizations. However, you can create a new project in the target organization and migrate your resources individually. ### How do I invite team members? Go to your organization or project settings, click "Access Control", then "Invite User". Enter their email address and select the appropriate role. They'll receive an invitation email with instructions to join. ## Resources and Billing ### What resource types are available? KtrlPlane supports four main resource types: 1. **Konnektr.Graph**: Graph database for connected data and digital twins 2. **Konnektr.Flow**: Real-time event and data processing 3. **Konnektr.Assembler**: AI-powered digital twin model builder 4. **Konnektr.Compass**: Navigation and discovery (coming soon) ### How is billing calculated? Billing is resource-based with three components: * **Base tier cost**: Monthly fee for your selected tier (Free, Pro, Enterprise) * **Usage charges**: Additional costs for usage beyond tier limits * **Storage costs**: Data storage charges for persistent data ### Can I change resource tiers after creation? Yes, you can upgrade or downgrade resource tiers at any time. Changes take effect immediately, and billing is prorated to reflect the change. ### What happens if I exceed my tier limits? * **Free tier**: Your resource may be temporarily throttled or paused * **Paid tiers**: You'll be charged for additional usage beyond your tier limits * **All tiers**: You'll receive email notifications when approaching limits ### How do I set up billing alerts? Go to your organization or project billing settings and configure alert thresholds. You can set alerts for: * Monthly spending limits * Usage approaching tier limits * Unexpected billing increases ## Technical Questions ### What are the system requirements for KtrlPlane? KtrlPlane is a web-based platform with no local installation required. You just need: * A modern web browser (Chrome, Firefox, Safari, Edge) * Internet connection * JavaScript enabled ### Can I use KtrlPlane APIs programmatically? Yes! KtrlPlane provides comprehensive REST APIs for all functionality. You can: * Manage organizations, projects, and resources * Control access and permissions * Monitor usage and billing * Automate deployments API documentation is available at `/api/` in this documentation. ### Is there a command-line interface (CLI)? Yes, we provide a CLI tool for KtrlPlane that allows you to manage resources from the command line. Install it with: ```bash npm install -g @konnektr/ktrlplane-cli # or pip install ktrlplane-cli ``` ### How do I integrate KtrlPlane with my CI/CD pipeline? Use the KtrlPlane API or CLI in your deployment scripts: ```yaml # GitHub Actions example - name: Deploy to KtrlPlane run: | ktrlplane resource create \ --project $PROJECT_ID \ --type Konnektr.Graph \ --config @graph-config.json env: KTRLPLANE_API_KEY: ${{ secrets.KTRLPLANE_API_KEY }} ``` ## Troubleshooting ### I can't log in to KtrlPlane Common solutions: 1. Check your email address and password 2. Try resetting your password 3. Clear your browser cache and cookies 4. Try a different browser or incognito mode 5. Check if you're using the correct login URL ### My resource deployment failed Check the resource status page for error details. Common causes: * Invalid configuration settings * Insufficient tier limits * Network connectivity issues * Billing account problems ### I'm getting API authentication errors Verify that: * Your API key is valid and not expired * You're using the correct API endpoint * Your account has the required permissions * The API key is properly included in the Authorization header ### How do I report a bug or request a feature? You can: 1. Create an issue on our [GitHub repository](https://github.com/konnektr-io/ktrlplane) 2. Contact support through the platform 3. Join our community forums 4. Reach out on Discord ## Data and Privacy ### Where is my data stored? Data is stored in secure, SOC 2 compliant data centers. The specific location depends on your chosen region during resource creation. ### Is my data encrypted? Yes, all data is encrypted: * **In transit**: All API calls and web traffic use TLS 1.2+ * **At rest**: All stored data is encrypted using AES-256 * **In processing**: Memory encryption for sensitive operations ### Can I export my data? Yes, you can export your data at any time: * **Configuration data**: Use the API to export project and resource configurations * **Resource data**: Each resource type provides data export capabilities * **Billing data**: Download invoices and usage reports from billing settings ### What is your data retention policy? * **Active accounts**: Data is retained as long as your account is active * **Deleted resources**: Data is permanently deleted within 30 days * **Closed accounts**: Data is retained for 90 days then permanently deleted * **Billing data**: Retained for 7 years for compliance purposes ## Limits and Quotas ### Are there any usage limits? Yes, limits vary by tier and resource type: **Organization limits:** * Free: 3 projects per organization * Pro: 50 projects per organization * Enterprise: Unlimited projects **Resource limits:** * Vary by resource type and tier * Detailed in each resource type's documentation * Can be increased by upgrading tiers ### How do I request limit increases? For limits beyond the Enterprise tier, contact our sales team. We can provide custom quotas for large deployments. ## Support and Community ### How do I get help? Multiple support options are available: 1. **Documentation**: Comprehensive guides and API references 2. **Community Forums**: Get help from other users 3. **Discord**: Real-time chat with the community 4. **Email Support**: For Pro and Enterprise customers 5. **Dedicated Support**: For Enterprise customers ### What are your support hours? * **Community Support**: 24/7 via forums and Discord * **Email Support**: Business hours (9 AM - 6 PM PST, Monday-Friday) * **Enterprise Support**: 24/7 for critical issues ### Do you offer training or consulting? Yes, we offer: * **Documentation and tutorials**: Self-service learning * **Webinars**: Regular training sessions * **Professional services**: Custom training and consulting (Enterprise) * **Partner program**: Certified implementation partners Still have questions? Check our [community forums](/support/community) or [contact support](/support/contact). # boolean # boolean Converts the input to a boolean. **Type:** transform ## Aliases `toBoolean`, `bool`, `boolean`, `toBool` ## Parameters * **input** (unknown): The input to convert to a boolean. ## Returns **Type:** `boolean` The boolean value, or undefined for ambiguous string values. ## Examples ```javascript toBoolean("true") // true ``` ```javascript "false"|toBoolean // false ``` ```javascript toBoolean(1) // true ``` # formatBase # formatBase Formats a number as a string in the specified base. **Type:** transform ## Parameters * **input** (unknown): The input number to format. * **base** (number): The numeric base to convert to (2-36). ## Returns **Type:** `string` The number formatted in the specified base, or empty string if input cannot be converted to a number. ## Examples ```javascript formatBase(255, 16) // "ff" ``` ```javascript (10)|formatBase(2) // "1010" ``` ```javascript formatBase(64, 8) // "100" ``` # formatInteger # formatInteger Formats a number as an integer with zero padding. **Type:** transform ## Parameters * **input** (unknown): The input number to format. * **format** (string): The format string indicating the minimum number of digits. ## Returns **Type:** `string` The zero-padded integer string, or empty string if input cannot be converted to a number. ## Examples ```javascript formatInteger(42, "000") // "042" ``` ```javascript (7)|formatInteger("0000") // "0007" ``` ```javascript formatInteger(123, "00") // "123" ``` # formatNumber # formatNumber Formats a number to a decimal representation as specified by the format string. **Type:** transform ## Parameters * **input** (unknown): The input number to format. * **format** (string): The format string specifying decimal places and grouping. ## Returns **Type:** `string` The formatted number string, or empty string if input cannot be converted to a number. ## Examples ```javascript formatNumber(1234.567, "#,##0.00") // "1,234.57" ``` ```javascript (1000)|formatNumber("0.00") // "1000.00" ``` ```javascript formatNumber(42, "#,###") // "42" ``` # Conversion # Conversion Convert between different data types ## Functions * [`boolean`](./conversion/boolean): Converts the input to a boolean. * [`formatBase`](./conversion/formatBase): Formats a number as a string in the specified base. * [`formatInteger`](./conversion/formatInteger): Formats a number as an integer with zero padding. * [`formatNumber`](./conversion/formatNumber): Formats a number to a decimal representation as specified by the format string. * [`json`](./conversion/json): Parses the string and returns a JSON object. * [`number`](./conversion/number): Converts the input to a number. * [`parseInteger`](./conversion/parseInteger): Parses a string and returns an integer. * [`string`](./conversion/string): Casts the input to a string. ## Transforms * [`boolean`](./conversion/boolean): Converts the input to a boolean. * [`formatBase`](./conversion/formatBase): Formats a number as a string in the specified base. * [`formatInteger`](./conversion/formatInteger): Formats a number as an integer with zero padding. * [`formatNumber`](./conversion/formatNumber): Formats a number to a decimal representation as specified by the format string. * [`json`](./conversion/json): Parses the string and returns a JSON object. * [`number`](./conversion/number): Converts the input to a number. * [`parseInteger`](./conversion/parseInteger): Parses a string and returns an integer. * [`string`](./conversion/string): Casts the input to a string. # json # json Parses the string and returns a JSON object. **Type:** transform ## Aliases `toJson`, `parseJson` ## Parameters * **input** (string): The JSON string to parse. ## Returns **Type:** `any` The parsed JSON object or value. ## Examples ```javascript toJson('{"key": "value"}') // { key: "value" } ``` ```javascript '{"name": "John", "age": 30}'|toJson // { name: "John", age: 30 } ``` # number # number Converts the input to a number. **Type:** transform ## Aliases `toNumber`, `parseFloat`, `number`, `float`, `toFloat` ## Parameters * **input** (unknown): The input to convert to a number. ## Returns **Type:** `number` The numeric value, or NaN if conversion fails. ## Examples ```javascript toNumber("123") // 123 ``` ```javascript "45.67"|toNumber // 45.67 ``` ```javascript toNumber("abc") // NaN ``` # parseInteger # parseInteger Parses a string and returns an integer. **Type:** transform ## Aliases `parseInt`, `toInt`, `integer` ## Parameters * **input** (unknown): The input to parse as an integer. ## Returns **Type:** `number` The integer value, or NaN if parsing fails. ## Examples ```javascript parseInteger("123") // 123 ``` ```javascript "45.67"|parseInteger // 45 ``` ```javascript parseInteger(123.89) // 123 ``` # string # string Casts the input to a string. **Type:** transform ## Aliases `toString`, `string` ## Parameters * **input** (unknown): The input can be any type. * **prettify** (boolean): If true, the output will be pretty-printed. ## Returns **Type:** `string` The input converted to a JSON string representation. ## Examples ```javascript string(123) // "123" ``` ```javascript 123|string // "123" ``` # all # all Checks whether the provided array has all elements that match the specified expression. **Type:** transform ## Aliases `arrayEvery`, `every`, `all` ## Parameters * **input** (array): The input array to test. * **expression** (string): The JEXL expression to test against each element (supports value, index and array as context). ## Returns **Type:** `boolean` True if all elements match the expression, false otherwise or if input is not an array. ## Examples ```javascript every([2, 4, 6], "value % 2 == 0") // true ``` ```javascript [{age: 25}, {age: 35}]|every("value.age > 20") // true ``` ```javascript every([1, 2, 3], "value > 2") // false ``` # any # any Checks whether the provided array has any elements that match the specified expression. **Type:** transform ## Aliases `arrayAny`, `some`, `any` ## Parameters * **input** (array): The input array to test. * **expression** (string): The JEXL expression to test against each element (supports value, index and array as context). ## Returns **Type:** `boolean` True if any element matches the expression, false otherwise or if input is not an array. ## Examples ```javascript any([1, 2, 3], "value > 2") // true ``` ```javascript [{age: 25}, {age: 35}]|any("value.age > 30") // true ``` ```javascript any([1, 2, 3], "value > 5") // false ``` # append # append Appends elements to an array. **Type:** transform ## Aliases `arrayAppend`, `concat`, `append` ## Parameters * **input** (array): The input values to append to an array. ## Returns **Type:** `array` A new array with all inputs flattened and appended, or empty array if no valid input. ## Examples ```javascript append([1, 2], 3) // [1, 2, 3] ``` ```javascript [1, 2]|append(3, 4) // [1, 2, 3, 4] ``` ```javascript append([], 1, 2, 3) // [1, 2, 3] ``` # distinct # distinct Returns a new array with duplicate elements removed. **Type:** transform ## Aliases `arrayDistinct`, `distinct` ## Parameters * **input** (array): The input array to remove duplicates from. ## Returns **Type:** `array` A new array with duplicates removed, or empty array if input is not an array. ## Examples ```javascript distinct([1, 2, 2, 3, 1]) // [1, 2, 3] ``` ```javascript [1, 2, 2, 3]|distinct // [1, 2, 3] ``` ```javascript distinct(["a", "b", "a", "c"]) // ["a", "b", "c"] ``` # filter # filter Returns a new array with the elements of the input array that match the specified expression. **Type:** transform ## Aliases `arrayFilter`, `filter` ## Parameters * **input** (array): The input array to filter. * **expression** (string): The JEXL expression to test against each element (supports value, index and array as context). ## Returns **Type:** `array` A new array containing only elements that match the expression, or empty array if input is not an array. ## Examples ```javascript filter([1, 2, 3, 4], "value > 2") // [3, 4] ``` ```javascript [{age: 25}, {age: 35}]|filter("value.age > 30") // [{age: 35}] ``` ```javascript filter([1, 2, 3, 4], "value % 2 == 0") // [2, 4] ``` # find # find Finds the first element in an array that matches the specified expression. **Type:** transform ## Aliases `arrayFind`, `find` ## Parameters * **input** (array): The input array to search. * **expression** (string): The JEXL expression to test against each element (supports value, index and array as context). ## Returns **Type:** `unknown` The first element that matches the expression, or undefined if no match found or input is not an array. ## Examples ```javascript find([1, 2, 3, 4], "value > 2") // 3 ``` ```javascript [{name: "John"}, {name: "Jane"}]|find("value.name == 'Jane'") // {name: "Jane"} ``` ```javascript find([1, 2, 3], "value > 5") // undefined ``` # findIndex # findIndex Finds the index of the first element in the input array that satisfies the given Jexl expression. **Type:** transform ## Aliases `arrayFindIndex`, `findIndex` ## Parameters * **input** (array): The array to search through. * **expression** (string): A Jexl expression string to evaluate for each element. The expression has access to ## Returns **Type:** `number` The index of the first matching element, or ## Examples ```javascript [1, 2, 3, 4]|findIndex('value > 2'); // returns 2 ``` # Array # Array Array operations and transformations ## Functions * [`all`](./array/all): Checks whether the provided array has all elements that match the specified expression. * [`any`](./array/any): Checks whether the provided array has any elements that match the specified expression. * [`append`](./array/append): Appends elements to an array. * [`distinct`](./array/distinct): Returns a new array with duplicate elements removed. * [`filter`](./array/filter): Returns a new array with the elements of the input array that match the specified expression. * [`find`](./array/find): Finds the first element in an array that matches the specified expression. * [`findIndex`](./array/findIndex): Finds the index of the first element in the input array that satisfies the given Jexl expression. * [`join`](./array/join): Joins elements of an array into a string. * [`keys`](./array/keys): Returns the keys of an object as an array. * [`map`](./array/map): Returns an array containing the results of applying the expression parameter to each value in the array parameter. * [`mapField`](./array/mapField): Returns a new array with elements transformed by extracting a specific field. * [`range`](./array/range): Returns a sub-array from start index to end index. * [`reduce`](./array/reduce): Returns an aggregated value derived from applying the function parameter successively to each value in array in combination with the result of the previous application of the function. * [`reverse`](./array/reverse): Reverses the elements of an array. * [`shuffle`](./array/shuffle): Shuffles the elements of an array randomly. * [`sort`](./array/sort): Sorts the elements of an array. * [`toObject`](./array/toObject): Creates a new object based on key-value pairs or string keys. ## Transforms * [`all`](./array/all): Checks whether the provided array has all elements that match the specified expression. * [`any`](./array/any): Checks whether the provided array has any elements that match the specified expression. * [`append`](./array/append): Appends elements to an array. * [`distinct`](./array/distinct): Returns a new array with duplicate elements removed. * [`filter`](./array/filter): Returns a new array with the elements of the input array that match the specified expression. * [`find`](./array/find): Finds the first element in an array that matches the specified expression. * [`findIndex`](./array/findIndex): Finds the index of the first element in the input array that satisfies the given Jexl expression. * [`join`](./array/join): Joins elements of an array into a string. * [`keys`](./array/keys): Returns the keys of an object as an array. * [`map`](./array/map): Returns an array containing the results of applying the expression parameter to each value in the array parameter. * [`mapField`](./array/mapField): Returns a new array with elements transformed by extracting a specific field. * [`range`](./array/range): Returns a sub-array from start index to end index. * [`reduce`](./array/reduce): Returns an aggregated value derived from applying the function parameter successively to each value in array in combination with the result of the previous application of the function. * [`reverse`](./array/reverse): Reverses the elements of an array. * [`shuffle`](./array/shuffle): Shuffles the elements of an array randomly. * [`sort`](./array/sort): Sorts the elements of an array. * [`toObject`](./array/toObject): Creates a new object based on key-value pairs or string keys. # join # join Joins elements of an array into a string. **Type:** transform ## Aliases `arrayJoin`, `join` ## Parameters * **input** (unknown): The input array to join. * **separator** (string?): The separator string to use between elements. Defaults to comma. ## Returns **Type:** `string` The joined string, or undefined if input is not an array. ## Examples ```javascript arrayJoin(["foo", "bar", "baz"], ",") // "foo,bar,baz" ``` ```javascript ["one", "two", "three"]|arrayJoin("-") // "one-two-three" ``` ```javascript arrayJoin([1, 2, 3]) // "1,2,3" ``` # keys # keys Returns the keys of an object as an array. **Type:** transform ## Aliases `objectKeys`, `keys` ## Parameters * **input** (unknown): The input object to get keys from. ## Returns **Type:** `array` An array of object keys, or undefined if input is not an object. ## Examples ```javascript keys({name: "John", age: 30}) // ["name", "age"] ``` ```javascript {a: 1, b: 2}|keys // ["a", "b"] ``` ```javascript keys({}) // [] ``` # map # map Returns an array containing the results of applying the expression parameter to each value in the array parameter. **Type:** transform ## Aliases `arrayMap`, `map` ## Parameters * **input** (array): The input array to transform. * **expression** (string): The JEXL expression to apply to each element. ## Returns **Type:** `array` A new array with transformed elements, or undefined if input is not an array. ## Examples ```javascript map([1, 2, 3], "value * 2") // [2, 4, 6] ``` ```javascript [{name: "John"}, {name: "Jane"}]|map("value.name") // ["John", "Jane"] ``` ```javascript map([1, 2, 3], "value + index") // [1, 3, 5] ``` # mapField # mapField Returns a new array with elements transformed by extracting a specific field. **Type:** transform ## Parameters * **input** (array): The input array of objects to extract fields from. * **field** (string): The field name to extract from each object. ## Returns **Type:** `array` A new array with extracted field values, or empty array if input is not an array. ## Examples ```javascript mapField([{name: "John"}, {name: "Jane"}], "name") // ["John", "Jane"] ``` ```javascript [{age: 30}, {age: 25}]|mapField("age") // [30, 25] ``` ```javascript mapField([{x: 1, y: 2}, {x: 3, y: 4}], "x") // [1, 3] ``` # range # range Returns a sub-array from start index to end index. **Type:** transform ## Aliases `arrayRange`, `range` ## Parameters * **array** (array): The input array. * **start** (number): The starting index (inclusive). * **end** (number?): The ending index (exclusive). If not provided, slices to the end of the array. ## Returns **Type:** `array` The sub-array from start to end, or empty array if input is not an array. ## Examples ```javascript range([1, 2, 3, 4, 5], 1, 4) // [2, 3, 4] ``` ```javascript [10, 20, 30, 40]|range(0, 2) // [10, 20] ``` ```javascript range(["a", "b", "c", "d"], 2) // ["c", "d"] ``` # reduce # reduce Returns an aggregated value derived from applying the function parameter successively to each value in array in combination with the result of the previous application of the function. **Type:** transform ## Aliases `arrayReduce`, `reduce` ## Parameters * **input** (array): The input array to reduce. * **expression** (string): The JEXL expression to apply for each reduction step. * **initialValue** (unknown): The initial value for the accumulator. ## Returns **Type:** `unknown` The final accumulated value, or undefined if input is not an array. ## Examples ```javascript reduce([1, 2, 3, 4], "accumulator + value", 0) // 10 ``` ```javascript [1, 2, 3]|reduce("accumulator * value", 1) // 6 ``` ```javascript reduce(["a", "b", "c"], "accumulator + value", "") // "abc" ``` # reverse # reverse Reverses the elements of an array. **Type:** transform ## Aliases `arrayReverse`, `reverse` ## Parameters * **input** (array): The input values to reverse. ## Returns **Type:** `array` A new array with elements in reverse order, or empty array if no valid input. ## Examples ```javascript reverse([1, 2, 3]) // [3, 2, 1] ``` ```javascript [1, 2, 3]|reverse // [3, 2, 1] ``` ```javascript reverse(["a", "b", "c"]) // ["c", "b", "a"] ``` # shuffle # shuffle Shuffles the elements of an array randomly. **Type:** transform ## Aliases `arrayShuffle`, `shuffle` ## Parameters * **input** (array): The input array to shuffle. ## Returns **Type:** `array` The same array with elements randomly shuffled, or empty array if input is not an array. ## Examples ```javascript shuffle([1, 2, 3]) // [2, 1, 3] (random order) ``` ```javascript [1, 2, 3]|shuffle // [3, 1, 2] (random order) ``` ```javascript shuffle(["a", "b", "c"]) // ["c", "a", "b"] (random order) ``` # sort # sort Sorts the elements of an array. **Type:** transform ## Aliases `arraySort`, `order`, `sort` ## Parameters * **input** (array): The input array to sort. * **expression** (string?): Optional JEXL expression to determine sort value for objects. * **descending** (boolean?): Optional flag to sort in descending order. ## Returns **Type:** `array` A new sorted array, or empty array if input is not an array. ## Examples ```javascript sort([3, 1, 2]) // [1, 2, 3] ``` ```javascript [3, 1, 2]|sort // [1, 2, 3] ``` ```javascript sort([{age: 30}, {age: 20}], "age") // [{age: 20}, {age: 30}] ``` # toObject # toObject Creates a new object based on key-value pairs or string keys. **Type:** transform ## Aliases `arrayToObject`, `fromEntries`, `toObject` ## Parameters * **input** (unknown): The input string key or array of key-value pairs. * **val** (unknown?): Optional default value for string keys or when array elements are strings. ## Returns **Type:** `any` A new object created from the input, or empty object if input is invalid. ## Examples ```javascript toObject([["name", "John"], ["age", 30]]) // {name: "John", age: 30} ``` ```javascript toObject("name", "John") // {name: "John"} ``` ```javascript toObject(["key1", "key2"], "defaultValue") // {key1: "defaultValue", key2: "defaultValue"} ``` # base64Decode # base64Decode Decodes a Base64 encoded string. **Type:** transform ## Parameters * **input** (unknown): The Base64 encoded string to decode. ## Returns **Type:** `string` The decoded string, or empty string if input is not a string. ## Examples ```javascript base64Decode("aGVsbG8=") // "hello" ``` ```javascript "aGVsbG8gd29ybGQ="|base64Decode // "hello world" ``` ```javascript base64Decode("dGVzdA==") // "test" ``` # base64Encode # base64Encode Encodes a string to Base64. **Type:** transform ## Parameters * **input** (unknown): The input string to encode. ## Returns **Type:** `string` The Base64 encoded string, or empty string if input is not a string or encoding fails. ## Examples ```javascript base64Encode("hello") // "aGVsbG8=" ``` ```javascript "hello world"|base64Encode // "aGVsbG8gd29ybGQ=" ``` ```javascript base64Encode("test") // "dGVzdA==" ``` # formUrlEncoded # formUrlEncoded Encodes a string or object to URI component format. **Type:** transform ## Parameters * **input** (unknown): The input string or object to encode. ## Returns **Type:** `string` The URL encoded string, or empty string if input is not a string or object. ## Examples ```javascript formUrlEncoded("hello world") // "hello%20world" ``` ```javascript formUrlEncoded({name: "John", age: 30}) // "name=John&age=30" ``` ```javascript "hello & world"|formUrlEncoded // "hello%20%26%20world" ``` # Encoding # Encoding Data encoding and formatting utilities ## Functions * [`base64Decode`](./encoding/base64Decode): Decodes a Base64 encoded string. * [`base64Encode`](./encoding/base64Encode): Encodes a string to Base64. * [`formUrlEncoded`](./encoding/formUrlEncoded): Encodes a string or object to URI component format. ## Transforms * [`base64Decode`](./encoding/base64Decode): Decodes a Base64 encoded string. * [`base64Encode`](./encoding/base64Encode): Encodes a string to Base64. * [`formUrlEncoded`](./encoding/formUrlEncoded): Encodes a string or object to URI component format. # entries # entries Returns an array of key-value pairs from the input object. **Type:** transform ## Aliases `objectEntries`, `entries` ## Parameters * **input** (unknown): The input object to get entries from. ## Returns **Type:** `array` An array of \[key, value] pairs, or undefined if input is not an object. ## Examples ```javascript entries({name: "John", age: 30}) // [["name", "John"], ["age", 30]] ``` ```javascript {a: 1, b: 2}|entries // [["a", 1], ["b", 2]] ``` ```javascript entries({}) // [] ``` # Object # Object Object manipulation and inspection ## Functions * [`entries`](./object/entries): Returns an array of key-value pairs from the input object. * [`merge`](./object/merge): Returns a new object with the properties of the input objects merged together. * [`values`](./object/values): Returns the values of an object as an array. ## Transforms * [`entries`](./object/entries): Returns an array of key-value pairs from the input object. * [`merge`](./object/merge): Returns a new object with the properties of the input objects merged together. * [`values`](./object/values): Returns the values of an object as an array. # merge # merge Returns a new object with the properties of the input objects merged together. **Type:** transform ## Aliases `objectMerge`, `merge` ## Parameters * **args** (array): The input objects to merge. ## Returns **Type:** `Record` A new object with all properties merged together. ## Examples ```javascript merge({a: 1}, {b: 2}) // {a: 1, b: 2} ``` ```javascript {a: 1}|merge({b: 2}, {c: 3}) // {a: 1, b: 2, c: 3} ``` ```javascript merge({a: 1}, {a: 2}) // {a: 2} (later values override) ``` # values # values Returns the values of an object as an array. **Type:** transform ## Aliases `objectValues`, `values` ## Parameters * **input** (unknown): The input object to get values from. ## Returns **Type:** `array` An array of object values, or undefined if input is not an object. ## Examples ```javascript values({name: "John", age: 30}) // ["John", 30] ``` ```javascript {a: 1, b: 2}|values // [1, 2] ``` ```javascript values({}) // [] ``` # abs # abs Returns the absolute value of a number. **Type:** transform ## Aliases `absoluteValue`, `abs` ## Parameters * **input** (unknown): The input number to get the absolute value of. ## Returns **Type:** `number` The absolute value, or NaN if input cannot be converted to a number. ## Examples ```javascript absoluteValue(-5) // 5 ``` ```javascript (-10)|absoluteValue // 10 ``` ```javascript absoluteValue(3.14) // 3.14 ``` # average # average Calculates the average of an array of numbers. **Type:** transform ## Aliases `avg` ## Parameters * **input** (array): The input array of numbers or individual number arguments. ## Returns **Type:** `number` The average value, or NaN if input is not an array. ## Examples ```javascript average([1, 2, 3, 4]) // 2.5 ``` ```javascript [10, 20, 30]|average // 20 ``` ```javascript average(1, 2, 3, 4) // 2.5 ``` # ceil # ceil Rounds a number up to the nearest integer. **Type:** transform ## Parameters * **input** (unknown): The input number to round up. ## Returns **Type:** `number` The rounded up integer, or NaN if input cannot be converted to a number. ## Examples ```javascript ceil(3.2) // 4 ``` ```javascript (3.14)|ceil // 4 ``` ```javascript ceil(-2.8) // -2 ``` # floor # floor Rounds a number down to the nearest integer. **Type:** transform ## Parameters * **input** (unknown): The input number to round down. ## Returns **Type:** `number` The rounded down integer, or NaN if input cannot be converted to a number. ## Examples ```javascript floor(3.7) // 3 ``` ```javascript (3.14)|floor // 3 ``` ```javascript floor(-2.8) // -3 ``` # Math # Math Mathematical operations and calculations ## Functions * [`abs`](./math/abs): Returns the absolute value of a number. * [`average`](./math/average): Calculates the average of an array of numbers. * [`ceil`](./math/ceil): Rounds a number up to the nearest integer. * [`floor`](./math/floor): Rounds a number down to the nearest integer. * [`max`](./math/max): Finds the maximum value in an array of numbers. * [`min`](./math/min): Finds the minimum value in an array of numbers. * [`power`](./math/power): Returns the value of a number raised to a power. * [`random`](./math/random): Generates a random number between 0 (inclusive) and 1 (exclusive). * [`round`](./math/round): Rounds a number to the nearest integer or to specified decimal places. * [`sqrt`](./math/sqrt): Returns the square root of a number. * [`sum`](./math/sum): Calculates the sum of an array of numbers. ## Transforms * [`abs`](./math/abs): Returns the absolute value of a number. * [`average`](./math/average): Calculates the average of an array of numbers. * [`ceil`](./math/ceil): Rounds a number up to the nearest integer. * [`floor`](./math/floor): Rounds a number down to the nearest integer. * [`max`](./math/max): Finds the maximum value in an array of numbers. * [`min`](./math/min): Finds the minimum value in an array of numbers. * [`power`](./math/power): Returns the value of a number raised to a power. * [`round`](./math/round): Rounds a number to the nearest integer or to specified decimal places. * [`sqrt`](./math/sqrt): Returns the square root of a number. * [`sum`](./math/sum): Calculates the sum of an array of numbers. # max # max Finds the maximum value in an array of numbers. **Type:** transform ## Parameters * **input** (array): The input array of numbers or individual number arguments. ## Returns **Type:** `number` The maximum value, or NaN if input is not an array. ## Examples ```javascript max([1, 5, 3, 2]) // 5 ``` ```javascript [10, 20, 15]|max // 20 ``` ```javascript max(1, 5, 3, 2) // 5 ``` # min # min Finds the minimum value in an array of numbers. **Type:** transform ## Parameters * **input** (array): The input array of numbers or individual number arguments. ## Returns **Type:** `number` The minimum value, or NaN if input is not an array. ## Examples ```javascript min([1, 5, 3, 2]) // 1 ``` ```javascript [10, 20, 15]|min // 10 ``` ```javascript min(1, 5, 3, 2) // 1 ``` # power # power Returns the value of a number raised to a power. **Type:** transform ## Parameters * **input** (unknown): The base number. * **exponent** (number?): The exponent to raise the base to. Defaults to 2. ## Returns **Type:** `number` The result of base raised to the exponent, or NaN if input cannot be converted to a number. ## Examples ```javascript power(2, 3) // 8 ``` ```javascript (2)|power(4) // 16 ``` ```javascript power(9) // 81 (defaults to power of 2) ``` # random # random Generates a random number between 0 (inclusive) and 1 (exclusive). **Type:** function ## Aliases `randomNumber` ## Returns **Type:** `number` A random floating-point number between 0 and 1. ## Examples ```javascript randomNumber() // 0.123456789 (example output) ``` ```javascript randomNumber() // 0.987654321 (different each time) ``` # round # round Rounds a number to the nearest integer or to specified decimal places. **Type:** transform ## Parameters * **input** (unknown): The input number to round. * **decimals** (number?): Optional number of decimal places to round to. ## Returns **Type:** `number` The rounded number, or NaN if input cannot be converted to a number. ## Examples ```javascript round(3.7) // 4 ``` ```javascript round(3.14159, 2) // 3.14 ``` ```javascript (2.567)|round // 3 ``` # sqrt # sqrt Returns the square root of a number. **Type:** transform ## Parameters * **input** (unknown): The input number to get the square root of. ## Returns **Type:** `number` The square root of the input, or NaN if input cannot be converted to a number. ## Examples ```javascript sqrt(16) // 4 ``` ```javascript (25)|sqrt // 5 ``` ```javascript sqrt(2) // 1.4142135623730951 ``` # sum # sum Calculates the sum of an array of numbers. **Type:** transform ## Parameters * **input** (array): The input array of numbers or individual number arguments. ## Returns **Type:** `number` The sum of all numbers, or NaN if input is not an array. ## Examples ```javascript sum([1, 2, 3, 4]) // 10 ``` ```javascript [1.5, 2.5, 3.0]|sum // 7 ``` ```javascript sum(1, 2, 3, 4) // 10 ``` # convertTimeZone # convertTimeZone Converts an ISO datetime string to a target timezone, handling daylight savings, and returns an ISO string with the correct offset. **Type:** transform ## Parameters * **input** (unknown): ISO datetime string * **targetTimeZone** (unknown): Target timezone (IANA or Windows ID or fixed offset) ## Returns **Type:** `string` ISO datetime string with correct offset ## Examples ```javascript convertTimeZone('2025-06-26T12:00:00Z', 'Europe/Amsterdam') // 2025-06-26T14:00:00.0000000+02:00 ``` ```javascript '2025-06-26T12:00:00Z'|convertTimeZone('Pacific Standard Time') // '2025-06-26T05:00:00.0000000-07:00' ``` # dateTimeAdd # dateTimeAdd Adds a time range to a date and time in the ISO 8601 format. **Type:** transform ## Parameters * **input** (string): The input date and time string in ISO 8601 format. * **unit** (string): The time unit to add ("day", "hour", "minute", "second", "month", "year", etc.). * **value** (number): The amount to add (can be negative to subtract). ## Returns **Type:** `string` The new date and time as an ISO 8601 string. ## Examples ```javascript dateTimeAdd("2023-12-25T10:30:00.000Z", "day", 1) // "2023-12-26T10:30:00.000Z" ``` ```javascript now()|dateTimeAdd("hour", -2) // Two hours ago ``` ```javascript dateTimeAdd("2023-01-01T00:00:00.000Z", "month", 3) // "2023-04-01T00:00:00.000Z" ``` # dateTimeFormat # dateTimeFormat Converts a date and time to a provided format. **Type:** transform ## Parameters * **input** (union): The input date and time, either as a string or number. * **format** (string): The format to convert the date and time to. ## Returns **Type:** `string` The date and time in the specified format. ## Examples ```javascript dateTimeFormat(datetime, format) ``` ```javascript datetime|dateTimeFormat(format) ``` # dateTimeToMillis # dateTimeToMillis Parses the date and time in the ISO 8601 format and returns the number of milliseconds since the Unix epoch. **Type:** transform ## Aliases `toMillis` ## Parameters * **input** (string): The date and time string to parse. ## Returns **Type:** `number` The timestamp in milliseconds since Unix epoch. ## Examples ```javascript dateTimeToMillis("2023-12-25T10:30:00.000Z") // 1703505000000 ``` ```javascript "2023-01-01T00:00:00.000Z"|dateTimeToMillis // 1672531200000 ``` ```javascript dateTimeToMillis("2023-12-25") // 1703462400000 ``` # DateTime # DateTime Date and time operations ## Functions * [`convertTimeZone`](./datetime/convertTimeZone): Converts an ISO datetime string to a target timezone, handling daylight savings, and returns an ISO string with the correct offset. * [`dateTimeAdd`](./datetime/dateTimeAdd): Adds a time range to a date and time in the ISO 8601 format. * [`dateTimeFormat`](./datetime/dateTimeFormat): Converts a date and time to a provided format. * [`dateTimeToMillis`](./datetime/dateTimeToMillis): Parses the date and time in the ISO 8601 format and returns the number of milliseconds since the Unix epoch. * [`localTimeToIsoWithOffset`](./datetime/localTimeToIsoWithOffset): Converts a local time string in a specified timezone to an ISO datetime string with the correct offset. * [`millis`](./datetime/millis): Returns the current date and time in milliseconds since the Unix epoch. * [`millisToDateTime`](./datetime/millisToDateTime): Parses the number of milliseconds since the Unix epoch or parses a string (with or without specified format) and returns the date and time in the ISO 8601 format. * [`now`](./datetime/now): Returns the current date and time in the ISO 8601 format. ## Transforms * [`convertTimeZone`](./datetime/convertTimeZone): Converts an ISO datetime string to a target timezone, handling daylight savings, and returns an ISO string with the correct offset. * [`dateTimeAdd`](./datetime/dateTimeAdd): Adds a time range to a date and time in the ISO 8601 format. * [`dateTimeFormat`](./datetime/dateTimeFormat): Converts a date and time to a provided format. * [`dateTimeToMillis`](./datetime/dateTimeToMillis): Parses the date and time in the ISO 8601 format and returns the number of milliseconds since the Unix epoch. * [`localTimeToIsoWithOffset`](./datetime/localTimeToIsoWithOffset): Converts a local time string in a specified timezone to an ISO datetime string with the correct offset. * [`millisToDateTime`](./datetime/millisToDateTime): Parses the number of milliseconds since the Unix epoch or parses a string (with or without specified format) and returns the date and time in the ISO 8601 format. # localTimeToIsoWithOffset # localTimeToIsoWithOffset Converts a local time string in a specified timezone to an ISO datetime string with the correct offset. **Type:** transform ## Parameters * **localTime** (string): Local time string * **timeZone** (string): Timezone (IANA or Windows ID or fixed offset) ## Returns **Type:** `string` ISO datetime string with correct offset ## Examples ```javascript localTimeToIsoWithOffset('2025-06-26 14:00:00', 'Europe/Amsterdam') // '2025-06-26T14:00:00.0000000+02:00' ``` ```javascript '2025-06-26 05:00:00'|localTimeToIsoWithOffset('Pacific Standard Time') // '2025-06-26T05:00:00.0000000-08:00' ``` # millis # millis Returns the current date and time in milliseconds since the Unix epoch. **Type:** function ## Returns **Type:** `number` The current timestamp in milliseconds. ## Examples ```javascript millis() // 1703505000000 ``` ```javascript millis() // 1703505123456 (different time) ``` # millisToDateTime # millisToDateTime Parses the number of milliseconds since the Unix epoch or parses a string (with or without specified format) and returns the date and time in the ISO 8601 format. **Type:** transform ## Aliases `toDateTime`, `fromMillis`, `dateTimeString`, `millisToDateTime` ## Parameters * **input** (union?): Optional timestamp in milliseconds or date string. * **format** (string?): Optional format string for parsing date strings. ## Returns **Type:** `string` The date and time as an ISO 8601 string, or undefined if parsing fails. ## Examples ```javascript toDateTime(1703505000000) // "2023-12-25T10:30:00.000Z" ``` ```javascript toDateTime("2023-12-25") // "2023-12-25T00:00:00.000Z" ``` ```javascript toDateTime("25/12/2023", "dd/MM/yyyy") // "2023-12-25T00:00:00.000Z" ``` # now # now Returns the current date and time in the ISO 8601 format. **Type:** function ## Returns **Type:** `string` The current date and time as an ISO 8601 string. ## Examples ```javascript now() // "2023-12-25T10:30:00.000Z" ``` ```javascript now() // "2023-12-25T14:45:30.123Z" (different time) ``` # case # case Evaluates a list of predicates and returns the first result expression whose predicate is satisfied. **Type:** transform ## Aliases `switchCase`, `switch`, `case` ## Parameters * **args** (array): The arguments array where the first element is the expression to evaluate, followed by pairs of case and result, and optionally a default value. ## Returns **Type:** `unknown` The result of the first case whose predicate is satisfied, or the default value if no case is satisfied. ## Examples ```javascript switch(expression, case1, result1, case2, result2, ..., default) ``` # eval # eval Evaluates a JEXL expression and returns the result. **Type:** transform ## Aliases `_eval`, `eval` ## Parameters * **input** (unknown): Either a JEXL expression string or a context object. * **expression** (string): Optional JEXL expression when first argument is context. ## Returns **Type:** `any` The result of evaluating the expression, or undefined if evaluation fails. ## Examples ```javascript _eval("1 + 2") // 3 ``` ```javascript _eval({x: 5, y: 10}, "x + y") // 15 ``` ```javascript "2 * 3"|_eval // 6 ``` # Utility # Utility General utility functions ## Functions * [`case`](./utility/case): Evaluates a list of predicates and returns the first result expression whose predicate is satisfied. * [`eval`](./utility/eval): Evaluates a JEXL expression and returns the result. * [`length`](./utility/length): Returns the number of characters in a string, or the length of an array. * [`not`](./utility/not): Returns the logical NOT of the input. * [`type`](./utility/type): Returns the type of the input value as a string. * [`uuid`](./utility/uuid): Generates a new UUID (Universally Unique Identifier). ## Transforms * [`case`](./utility/case): Evaluates a list of predicates and returns the first result expression whose predicate is satisfied. * [`eval`](./utility/eval): Evaluates a JEXL expression and returns the result. * [`length`](./utility/length): Returns the number of characters in a string, or the length of an array. * [`not`](./utility/not): Returns the logical NOT of the input. * [`type`](./utility/type): Returns the type of the input value as a string. # length # length Returns the number of characters in a string, or the length of an array. **Type:** transform ## Aliases `count`, `size` ## Parameters * **input** (unknown): The input can be a string, an array, or an object. ## Returns **Type:** `number` The number of characters in a string, or the length of an array. ## Examples ```javascript length("hello") // 5 ``` ```javascript length([1, 2, 3]) // 3 ``` # not # not Returns the logical NOT of the input. **Type:** transform ## Parameters * **input** (unknown): The input to apply logical NOT to. ## Returns **Type:** `boolean` The logical NOT of the input converted to boolean. ## Examples ```javascript not(true) // false ``` ```javascript false|not // true ``` ```javascript not(0) // true ``` # type # type Returns the type of the input value as a string. **Type:** transform ## Aliases `getType`, `type` ## Parameters * **input** (unknown): The value to check the type of. ## Returns **Type:** `string` The type of the input value. ## Examples ```javascript type(5); // "number" ``` ```javascript foo|type; // "string" ``` ```javascript type(true); // "boolean" ``` # uuid # uuid Generates a new UUID (Universally Unique Identifier). **Type:** function ## Aliases `uid` ## Returns **Type:** `string` A new UUID v4 string. ## Examples ```javascript uuid() // "123e4567-e89b-12d3-a456-426614174000" ``` ```javascript uuid() // "987fcdeb-51a2-43d7-b123-456789abcdef" (different each time) ``` # camelCase # camelCase Converts the input string to camel case. **Type:** transform ## Aliases `camelcase`, `toCamelCase` ## Parameters * **input** (unknown): The input string to convert to camel case. ## Returns **Type:** `string` The camel case string, or empty string if input is not a string. ## Examples ```javascript camelCase("foo bar") // "fooBar" ``` ```javascript "hello-world"|camelCase // "helloWorld" ``` ```javascript camelCase("HELLO_WORLD") // "helloWorld" ``` # contains # contains Checks if the input string or array contains the specified value. **Type:** transform ## Aliases `includes` ## Parameters * **input** (unknown): The input string or array to search in. * **search** (string): The value to search for. ## Returns **Type:** `boolean` True if the input contains the search value, false otherwise. ## Examples ```javascript contains("hello world", "world") // true ``` ```javascript "foo-bar"|contains("bar") // true ``` ```javascript contains([1, 2, 3], 2) // true ``` # endsWith # endsWith Checks if the input string ends with the specified substring. **Type:** transform ## Parameters * **input** (unknown): The input string to check. * **search** (string): The substring to search for at the end. ## Returns **Type:** `boolean` True if the input ends with the search string, false otherwise. ## Examples ```javascript endsWith("hello world", "world") // true ``` ```javascript "foo-bar"|endsWith("bar") // true ``` ```javascript endsWith("test", "xyz") // false ``` # String # String String manipulation and formatting functions ## Functions * [`camelCase`](./string/camelCase): Converts the input string to camel case. * [`contains`](./string/contains): Checks if the input string or array contains the specified value. * [`endsWith`](./string/endsWith): Checks if the input string ends with the specified substring. * [`lowercase`](./string/lowercase): Converts the input string to lowercase. * [`pad`](./string/pad): Pads the input string to the specified width. * [`pascalCase`](./string/pascalCase): Converts the input string to pascal case. * [`replace`](./string/replace): Replaces occurrences of a specified string with a replacement string. * [`split`](./string/split): Splits the input string into an array of substrings. * [`startsWith`](./string/startsWith): Checks if the input string starts with the specified substring. * [`substring`](./string/substring): Gets a substring of a string. * [`substringAfter`](./string/substringAfter): Returns the substring after the first occurrence of the character sequence chars in str. * [`substringBefore`](./string/substringBefore): Returns the substring before the first occurrence of the character sequence chars in str. * [`trim`](./string/trim): Trims whitespace from both ends of a string. * [`uppercase`](./string/uppercase): Converts the input string to uppercase. ## Transforms * [`camelCase`](./string/camelCase): Converts the input string to camel case. * [`contains`](./string/contains): Checks if the input string or array contains the specified value. * [`endsWith`](./string/endsWith): Checks if the input string ends with the specified substring. * [`lowercase`](./string/lowercase): Converts the input string to lowercase. * [`pad`](./string/pad): Pads the input string to the specified width. * [`pascalCase`](./string/pascalCase): Converts the input string to pascal case. * [`replace`](./string/replace): Replaces occurrences of a specified string with a replacement string. * [`split`](./string/split): Splits the input string into an array of substrings. * [`startsWith`](./string/startsWith): Checks if the input string starts with the specified substring. * [`substring`](./string/substring): Gets a substring of a string. * [`substringAfter`](./string/substringAfter): Returns the substring after the first occurrence of the character sequence chars in str. * [`substringBefore`](./string/substringBefore): Returns the substring before the first occurrence of the character sequence chars in str. * [`trim`](./string/trim): Trims whitespace from both ends of a string. * [`uppercase`](./string/uppercase): Converts the input string to uppercase. # lowercase # lowercase Converts the input string to lowercase. **Type:** transform ## Aliases `lower` ## Parameters * **input** (unknown): The input to convert to lowercase. Non-string inputs are converted to JSON string first. ## Returns **Type:** `string` The lowercase string. ## Examples ```javascript lowercase("HELLO") // "hello" ``` ```javascript "HELLO WORLD"|lowercase // "hello world" ``` # pad # pad Pads the input string to the specified width. **Type:** transform ## Parameters * **input** (unknown): The input to pad. Non-string inputs are converted to JSON string first. * **width** (number): The target width. Positive values pad to the right, negative values pad to the left. * **char** (string): The character to use for padding. Defaults to space. ## Returns **Type:** `string` The padded string. ## Examples ```javascript pad("hello", 10) // "hello " ``` ```javascript pad("world", -8, "0") // "000world" ``` ```javascript "foo"|pad(5, ".") // "foo.." ``` # pascalCase # pascalCase Converts the input string to pascal case. **Type:** transform ## Aliases `pascalcase`, `toPascalCase` ## Parameters * **input** (unknown): The input string to convert to pascal case. ## Returns **Type:** `string` The pascal case string, or empty string if input is not a string. ## Examples ```javascript pascalCase("foo bar") // "FooBar" ``` ```javascript "hello-world"|pascalCase // "HelloWorld" ``` ```javascript pascalCase("HELLO_WORLD") // "HelloWorld" ``` # replace # replace Replaces occurrences of a specified string with a replacement string. **Type:** transform ## Parameters * **input** (unknown): The input string to perform replacements on. * **search** (string): The string to search for and replace. * **replacement** (string): The string to replace matches with. Defaults to empty string. ## Returns **Type:** `string` The string with replacements made, or undefined if input is not a string. ## Examples ```javascript replace("foo-bar-baz", "-", "_") // "foo_bar_baz" ``` ```javascript "hello world"|replace("world", "there") // "hello there" ``` ```javascript replace("test test test", "test", "demo") // "demo demo demo" ``` # split # split Splits the input string into an array of substrings. **Type:** transform ## Parameters * **input** (unknown): The input string to split. * **separator** (string): The separator string to split on. ## Returns **Type:** `array` An array of substrings, or empty array if input is not a string. ## Examples ```javascript split("foo,bar,baz", ",") // ["foo", "bar", "baz"] ``` ```javascript "one-two-three"|split("-") // ["one", "two", "three"] ``` ```javascript split("hello world", " ") // ["hello", "world"] ``` # startsWith # startsWith Checks if the input string starts with the specified substring. **Type:** transform ## Parameters * **input** (unknown): The input string to check. * **search** (string): The substring to search for at the beginning. ## Returns **Type:** `boolean` True if the input starts with the search string, false otherwise. ## Examples ```javascript startsWith("hello world", "hello") // true ``` ```javascript "foo-bar"|startsWith("foo") // true ``` ```javascript startsWith("test", "xyz") // false ``` # substring # substring Gets a substring of a string. **Type:** transform ## Parameters * **input** (unknown): The input string. * **start** (number): The starting index of the substring. * **length** (number): The length of the substring. ## Returns **Type:** `string` The substring of the input string. ## Examples ```javascript substring("hello world", 0, 5) // "hello" ``` # substringAfter # substringAfter Returns the substring after the first occurrence of the character sequence chars in str. **Type:** transform ## Parameters * **input** (unknown): The input string. * **chars** (unknown): The character sequence to search for. ## Returns **Type:** `string` The substring after the first occurrence of the character sequence chars in str. ## Examples ```javascript substringAfter("hello world", " ") // "world" ``` # substringBefore # substringBefore Returns the substring before the first occurrence of the character sequence chars in str. **Type:** transform ## Parameters * **input** (unknown): The input string. * **chars** (unknown): The character sequence to search for. ## Returns **Type:** `string` The substring before the first occurrence of the character sequence chars in str. ## Examples ```javascript substringBefore("hello world", " ") // "hello" ``` # trim # trim Trims whitespace from both ends of a string. **Type:** transform ## Parameters * **input** (unknown): The input string to trim. * **trimChar** (string?): Optional character to trim instead of whitespace. ## Returns **Type:** `string` The trimmed string, or empty string if input is not a string. ## Examples ```javascript trim(" hello ") // "hello" ``` ```javascript " world "|trim // "world" ``` ```javascript trim("__hello__", "_") // "hello" ``` # uppercase # uppercase Converts the input string to uppercase. **Type:** transform ## Aliases `upper` ## Parameters * **input** (unknown): The input to convert to uppercase. Non-string inputs are converted to JSON string first. ## Returns **Type:** `string` The uppercase string. ## Examples ```javascript uppercase("hello") // "HELLO" ``` ```javascript "hello world"|uppercase // "HELLO WORLD" ```