TypeScript Modeling Framework - A TypeScript port of the Eclipse Modeling Framework (EMF)
npm install @tripsnek/tmfbash
npm install @tripsnek/tmf
`
[Optonal] For visual model editing, install the VSCode extension:
1. Open VSCode
2. Search for "TMF Ecore Editor" in extensions
3. Install the extension
Quick Start
$3
Create a new file in VSCode. The TMF Ecore Editor will auto-initialize it with a package:
`xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore"
name="blog" nsURI="http://example.org/blog" nsPrefix="blog">
`
Use the visual editor to add classes, attributes, and references. You could also just edit the XML directly. Here is an example of a simple model for a Blog application:
`xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore"
name="blog" nsURI="http://example.com/blog" nsPrefix="blog">
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
upperBound="-1" eType="#//Post" containment="true" eOpposite="#//Post/blog"/>
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
eType="#//Blog" eOpposite="#//Blog/posts"/>
`
$3
Click "Generate Code" in the VSCode editor, or run TMF's code generator. This creates type-safe TypeScript classes with full metamodel support.
You can also invoke TMF directly using 'npx' as follows: `npx @tripsnek/tmf ./path/to/your/.ecore `
This will create three folders in a src/ directory adjacent to the .ecore file:
- api/ contains interfaces for each of your types, as well as -package.tsand -factory.ts that define the metamodel at runtime and allow for reflective instantiation.
- gen/ contains abstract base classes that implemente basic get/set behavior and special TMF behaviors (reflection and containment/inverse reference maintencance). DO NOT EDIT THESE
- impl/ contains (initially empty) concrete classes you can extend as you like. THESE ARE SAFE TO EDIT
The generator can be configured in various useful ways, see `npx @tripsnek/tmf --help` for more information.
`typescript
export class BlogImpl extends BlogImplGen implements Blog {
// Implement any operations you defined for your eclass in Ecore
myBlogOperation(): void {
//do something interesting
}
// Or add any other custom business logic that isn't exposed at the interface level
validate(): boolean {
return this.getTitle() !== null;
}
}
`
$3
`typescript
import { BlogFactory, BlogPackage, Blog, Post } from '@myorg/blog';
import { TJson } from '@tripsnek/tmf';
// Initialize packages (required for TJson serialization)
BlogPackageInitializer.registerAll();
// Create instances
const blog = factory.createBlog();
blog.setTitle("My Tech Blog");
blog.setId("blog_1");
const post = factory.createPost();
post.setTitle("Introduction to TMF");
post.setContent("TMF makes model-driven development straightforward...");
// Containment: adding post to blog automatically sets the inverse reference
blog.getPosts().add(post);
console.log(post.getBlog() === blog); // true - automatically maintained!
// Serialize to JSON
const json = TJson.makeJson(blog);
console.log(JSON.stringify(json, null, 2)); //your SAFELY stringified object
// Deserialize from JSON
const blogCopy = TJson.makeEObject(json) as Blog;
console.log(blogCopy.getPosts().get(0).getTitle()); // "Introduction to TMF"
`
Understanding EMF Concepts
$3
EPackage - The root container for your model, defines namespace and contains classifiers
EClass - Represents a class in your model. Can be:
- Concrete - Standard instantiable class
- Abstract - Cannot be instantiated directly
- Interface - Defines contract without implementation
EAttribute - Simple typed properties (String, Int, Boolean, etc.)
EReference - Relationships between classes, with two key concepts:
- Containment - Parent-child relationship where child lifecycle is determined by parent
- Opposite - Bidirectional relationship that TMF keeps synchronized automatically. Use these only when you know both ends will be serialized as part of the same containment hierarchy or "aggregate" - the bundle of data that goes between your server and client all at once.
EOperation - Methods on your classes with parameters and return types
EEnum - Enumeration types with literal values
$3
When defining attributes and operation parameters, you can use these built-in Ecore data types:
Primitive Types
- EString - Text values (TypeScript: string)
- EInt|EDouble|EFloat - Numeric values with no distinction in TS (TypeScript: number)
- EBoolean - True/false values (TypeScript: boolean)
- EDate - Date/time values (TypeScript: Date)
Classifier Types
- EClass - References to other classes in your model
- EEnum - Your custom enumerations become TypeScript enums
Type Modifiers
- Multiplicity: Single-valued or Many-valued
- ID: Marks an attribute as the unique identifier
- Transient: Not persisted when serializing
$3
Containment Hierarchies
When a reference has containment=true, the reference creates parent-child hierarchies where children follow their parent's lifecycle:
`typescript
const blog = factory.createBlog();
const post1 = factory.createPost();
const post2 = factory.createPost();
// Posts are contained by blog
blog.getPosts().add(post1);
blog.getPosts().add(post2);
// When you serialize the blog, all contained posts are included
const json = TJson.makeJson(blog); // Includes all posts
`
Inverse References
When references have opposites, TMF maintains both sides automatically:
`typescript
// Setting one side...
blog.getPosts().add(post);
// ...automatically sets the other
console.log(post.getBlog() === blog); // true!
`
ELists
When the multiplicity is set to many-valued, TMF uses EList collections to maintain model integrity (i.e. to enforce inverse references and containment). The collection otherwise behaves as you would expect:
`typescript
const posts = blog.getPosts(); // Returns EList
// Standard operations
posts.add(newPost);
posts.remove(oldPost);
posts.get(0);
posts.size();
posts.clear();
// Iterate
for (const post of posts.elements()) {
console.log(post.getTitle());
}
// Convert to array when needed
const array = posts.elements();
`
TJson Serialization
TMF's TJson provides robust JSON serialization that preserves object relationships, containment hierarchies, and type information - enabling seamless data exchange between frontend and backend systems.
$3
TJson automatically registers packages when you "touch" them by importing and accessing their eINSTANCE:
`typescript
import { BlogPackage } from '@myorg/blog';
BlogPackageInitializer.registerAll(); //registers BlogPackage and subpackages
// Now TJson can serialize/deserialize Blog objects
const json = TJson.makeJson(blog);
const copy = TJson.makeEObject(json);
`
$3
Objects need ID attributes to be referenced across containment boundaries. TMF automatically generates UUIDs during serialization for objects without IDs:
`typescript
const blog = factory.createBlog();
blog.setId("blog_1"); // Set your own ID, or...
// TJson assigns UUIDs during serialization if no ID exists
const json = TJson.makeJson(blog); // UUID auto-generated here if needed
// Containment: child objects are serialized inline
blog.getPosts().add(post); // Post serialized with Blog
// Cross-references: only IDs are serialized
blog.setAuthor(externalUser); // Only author ID serialized
`
$3
When deserializing, TJson creates proxy objects for external references (objects not in the containment tree):
`typescript
// External user referenced by blog
const deserializedBlog = TJson.makeEObject(json);
const author = deserializedBlog.getAuthor();
if (author.eIsProxy()) {
// Proxy contains ID and type, load actual object as needed
const authorId = author.getId();
const realAuthor = await loadUserFromDatabase(authorId);
deserializedBlog.setAuthor(realAuthor);
}
`
Note TMF proxies are simpler than EMF's full resource-based proxy system - they contain just the object type and ID information needed for JSON serialization scenarios, without EMF's broader resource loading and URI resolution capabilities.
Leveraging Reflection
TMF's real power comes from its reflection capabilities. While TMF itself provides the metamodel infrastructure, you can build powerful generic solutions on top of it (this is how TJson is implemented!).
$3
This example shows how reflection enables you to create a trivial backend that works with any TMF model, with automatically generated REST endpoints over an in-memory datastore.
`typescript
import express from 'express';
import { EClass, EObject, TJson, TUtils } from '@tripsnek/tmf';
import { BlogPackage } from '@myorg/blog';
const app = express();
app.use(express.json());
// Initialize your packages
BlogPackageInitializer.registerAll(); //registers BlogPackage and subpackages
// Storage for instances (in production, this would be a database)
const storage = new Map>();
// Get all "root" classes (those which are not contained by anything else) from your model
const rootClasses = TUtils.getRootEClasses(pkg);
// Initialize storage for each class
rootClasses.forEach(eClass => {
storage.set(eClass.getName(), new Map());
});
// Generate CRUD endpoints for each class automatically
rootClasses.forEach(eClass => {
const className = eClass.getName();
const classStore = storage.get(className)!;
// GET all instances
app.get( /api/${className}, (req, res) => {
const instances = Array.from(classStore.values());
res.json(TJson.makeJsonArray(instances));
});
// POST new instance
app.post(/api/${className}, (req, res) => {
const instance = TJson.makeEObject(req.body)!;
// Get ID dynamically using reflection
const idAttr = instance.eClass().getEStructuralFeature('id');
if (idAttr) {
const id = String(instance.eGet(idAttr));
classStore.set(id, instance);
}
res.json(TJson.makeJson(instance));
});
// Additional endpoints: GET by ID, PUT, DELETE...
});
app.listen(3000);
`
$3
`typescript
import { EObject } from '@tripsnek/tmf';
// Process any object and its recursively contained children
function processTree(root: EObject) {
console.log(Processing ${root.eClass().getName()});
// Iterate through entire containment tree of objects
for (const ref of root.eClass().getEAllReferences()) {
//only traverse containment refs
if(ref.isContainment()){
//process many-valued (EList)
if(ref.isMany()){
for (const containedObj of >obj.eGet(ref)){
processTree(containedObj);
}
}
//process single-valued
else{
const containedObj = obj.eGet(ref);
if(containedObj){
processTree(containedObj)
}
}
}
}
}
//...or you could just iterate the tree as a flattened
//array via eAllContents()
function processTreeWithEAllContainets(root: EObject) {
console.log( Processing ${root.eClass().getName()});
// Iterate through entire containment tree of objects
for (const child of root.eAllContents()) {
console.log( Processing contained: ${child.eClass().getName()});
}
}
// Dynamically access all attributes
function printAllAttributes(obj: EObject) {
const eClass = obj.eClass();
for (const attr of eClass.getEAllAttributes().elements()) {
const value = obj.eGet(attr);
console.log(${attr.getName()}: ${value});
}
}
// Find references to non-contained objects
function findReferences(obj: EObject) {
const eClass = obj.eClass();
for (const ref of eClass.getEAllReferences().elements()) {
if (!ref.isContainment()) { // Skip containment refs
const value = obj.eGet(ref);
if (value) {
console.log(Reference ${ref.getName()} points to ${value});
}
}
}
}
``