Transformation Domains

Guide for building non-CRUD, transformation-centric schemas with morph.

Overview

Traditional morph schemas are entity-centric: entities with identity, lifecycle, and persistence. This works great for CRUD applications (todo apps, CRMs, etc.).

Transformation domains are different:

  • Pure functions (no side effects)
  • Stateless operations (input → output)
  • No entity lifecycle (no create/update/delete)
  • No persistence (no repositories)

Examples: code generators, compilers, parsers, data transformers.

Schema Structure

Transformation domains use types and functions instead of entities and commands:

{
  "name": "my-generator",
  "contexts": {
    "generation": {
      "description": "Code generation operations",
      "types": { ... },
      "functions": { ... },
      "entities": {},
      "invariants": [],
      "dependencies": []
    }
  }
}

Types

Pure algebraic data types without identity or lifecycle.

Product Types (Records)

{
	"GeneratedFile": {
		"kind": "product",
		"description": "A file produced by code generation",
		"fields": {
			"filename": {
				"description": "Relative path for the file",
				"type": { "kind": "primitive", "name": "string" }
			},
			"content": {
				"description": "File contents",
				"type": { "kind": "primitive", "name": "string" }
			}
		}
	}
}

Generated:

export const GeneratedFileSchema = S.Struct({
	filename: S.String,
	content: S.String,
});
export type GeneratedFile = S.Schema.Type<typeof GeneratedFileSchema>;

Sum Types (Discriminated Unions)

{
	"Result": {
		"kind": "sum",
		"description": "Success or failure result",
		"discriminator": "kind",
		"variants": {
			"success": {
				"description": "Successful result",
				"fields": {
					"value": {
						"description": "The result value",
						"type": { "kind": "primitive", "name": "string" }
					}
				}
			},
			"error": {
				"description": "Error result",
				"fields": {
					"message": {
						"description": "Error message",
						"type": { "kind": "primitive", "name": "string" }
					}
				}
			}
		}
	}
}

Generated:

export const ResultSchema = S.Union(
	S.Struct({ kind: S.Literal("success"), value: S.String }),
	S.Struct({ kind: S.Literal("error"), message: S.String }),
);
export type Result = S.Schema.Type<typeof ResultSchema>;

Aliases

{
	"FileList": {
		"kind": "alias",
		"description": "List of generated files",
		"type": {
			"kind": "array",
			"element": { "kind": "type", "name": "GeneratedFile" }
		}
	}
}

Generated:

export const FileListSchema = S.Array(GeneratedFileSchema);
export type FileList = S.Schema.Type<typeof FileListSchema>;

Functions

Pure transformations without side effects.

{
	"functions": {
		"generateTypes": {
			"description": "Generate TypeScript types from domain schema",
			"input": {
				"schema": {
					"description": "The source schema",
					"type": { "kind": "type", "name": "DomainSchema" }
				}
			},
			"output": {
				"kind": "array",
				"element": { "kind": "type", "name": "GeneratedFile" }
			},
			"errors": [
				{
					"name": "InvalidSchema",
					"description": "Schema validation failed",
					"when": "schema is malformed"
				}
			],
			"tags": ["@cli"]
		}
	}
}

Generated:

export const GenerateTypesInputSchema = S.Struct({
	schema: DomainSchemaSchema,
});
export type GenerateTypesInput = S.Schema.Type<typeof GenerateTypesInputSchema>;
export type GenerateTypesOutput = readonly GeneratedFile[];
export type GenerateTypesError = InvalidSchema;

Differences from Commands

Aspect Command Function
Events Emits domain events None (pure)
Aggregates Declares aggregate access None (stateless)
Pre/post Can have invariants None (types are contract)
Use case State changes Data transformation

Hybrid Schemas

You can mix CRUD and transformation elements:

{
  "contexts": {
    "reports": {
      "entities": {
        "Report": { ... }
      },
      "types": {
        "ReportOutput": { ... }
      },
      "commands": {
        "createReport": { ... }
      },
      "functions": {
        "formatReport": { ... }
      }
    }
  }
}

Use entities for things that need persistence; use types for intermediate data.

Referencing Types

Use the type kind in TypeRef to reference a defined type:

{ "kind": "type", "name": "GeneratedFile" }
{ "kind": "type", "name": "DomainSchema", "context": "schema" }

Cross-context references use the context field.

Future Work

Features planned but not yet implemented:

Feature Description Use Case
Generics Type parameters like Result<T, E> Polymorphic types
Streaming Stream<T> for async iteration Large file generation
Function types (A) => B in TypeRef Callbacks, visitors
Function invariants Pre/post conditions on functions Formal verification

See TODO.md for the backlog.

Example: Code Generator Schema

A complete transformation domain schema:

{
	"$schema": "domain-schema.json",
	"name": "code-generator",
	"contexts": {
		"generation": {
			"description": "Code generation from schemas",
			"types": {
				"GeneratedFile": {
					"kind": "product",
					"description": "A generated source file",
					"fields": {
						"path": {
							"description": "File path",
							"type": { "kind": "primitive", "name": "string" }
						},
						"content": {
							"description": "File content",
							"type": { "kind": "primitive", "name": "string" }
						}
					}
				},
				"GenerationResult": {
					"kind": "sum",
					"description": "Generation outcome",
					"discriminator": "status",
					"variants": {
						"success": {
							"description": "Successful generation",
							"fields": {
								"files": {
									"description": "Generated files",
									"type": {
										"kind": "array",
										"element": { "kind": "type", "name": "GeneratedFile" }
									}
								},
								"warnings": {
									"description": "Non-fatal warnings",
									"type": {
										"kind": "array",
										"element": { "kind": "primitive", "name": "string" }
									}
								}
							}
						},
						"failure": {
							"description": "Generation failed",
							"fields": {
								"errors": {
									"description": "Error messages",
									"type": {
										"kind": "array",
										"element": { "kind": "primitive", "name": "string" }
									}
								}
							}
						}
					}
				}
			},
			"functions": {
				"generate": {
					"description": "Generate code from input schema",
					"input": {
						"schema": {
							"description": "Input schema",
							"type": { "kind": "primitive", "name": "string" }
						},
						"options": {
							"description": "Generation options",
							"type": { "kind": "primitive", "name": "string" },
							"optional": true
						}
					},
					"output": { "kind": "type", "name": "GenerationResult" },
					"errors": [],
					"tags": ["@cli", "@api"]
				}
			},
			"entities": {},
			"invariants": [],
			"dependencies": []
		}
	}
}