| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
TMF is a lightweight TypeScript port of the Eclipse Modeling Framework (EMF) that brings model-driven development to the TypeScript ecosystem. Build type-safe, reflective data models that work seamlessly across your entire stack - from Node.js (or Java!) servers to React/Angular frontends.
tmf_in_60_seconds.mp4Quick demonstration of adding types/features to an ecore model and generating code in a full stack reflective application, which can be downloaded from the tmf-examples repository (specifically the NX Angular/Node example).
Traditional TypeScript development requires writing a lot of similar boilerplate over and over: DTOs, validation, serialization, API endpoints, database mappings. Each a tedious chore and potential risk of becoming a bug as your data model evolves.
TMF can help eliminate this repetition through powerful runtime reflection and code generation. By default this includes:
For many applications, the above capabilities may be all you need, but even more value can be unlocked for applications that leverage reflection where possible throughout the stack, including:
This README describes the basics of how reflection works, and many are demonstrated in the tmf-examples repository, which contains multiple fully reflective full-stack architectures (demonstrated in the above video) using Node or Java Spring Boot backends, and Angular or React frontends.
There is no one-size-fits-all software design strategy. TMF is useful when:
Your domain model has lots of different types of entities with nested structure (that is, objects that are "part" of other objects).
Your entities are associated with interesting behavior, which can then simply become API methods on the entities themselves.
You need to make use of (1) and/or (2) on both your frontend and backend.
If all of the data in your app can be represented by a reasonably small set of simple, flat objects, and almost all of the complexity of your app is confined to only the backend or the frontend, TMF will be of little use for that application, and in fact it will likely only get in your way.
npm install @tripsnek/tmf[Optonal] For visual model editing, install the VSCode extension:
Create a new file <your-model-name>.ecore in VSCode. The TMF Ecore Editor will auto-initialize it with a package:
<?xml version="1.0" encoding="UTF-8"?>
<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
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">
</ecore:EPackage>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 version="1.0" encoding="UTF-8"?>
<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
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">
<eClassifiers xsi:type="ecore:EClass" name="Blog">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="id"
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="title"
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="posts"
upperBound="-1" eType="#//Post" containment="true" eOpposite="#//Post/blog"/>
</eClassifiers>
<eClassifiers xsi:type="ecore:EClass" name="Post">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="title"
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="content"
eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EReference" name="blog"
eType="#//Blog" eOpposite="#//Blog/posts"/>
</eClassifiers>
</ecore:EPackage>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/<myecorefile>.ecore
This will create three folders in a src/ directory adjacent to the .ecore file:
The generator can be configured in various useful ways, see npx @tripsnek/tmf --help for more information.
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;
}
}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"EPackage - The root container for your model, defines namespace and contains classifiers
EClass - Represents a class in your model. Can be:
EAttribute - Simple typed properties (String, Int, Boolean, etc.)
EReference - Relationships between classes, with two key concepts:
EOperation - Methods on your classes with parameters and return types
EEnum - Enumeration types with literal values
When defining attributes and operation parameters, you can use these built-in Ecore data types:
Primitive Types
Classifier Types
Type Modifiers
Containment Hierarchies
When a reference has containment=true, the reference creates parent-child hierarchies where children follow their parent's lifecycle:
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 postsInverse References
When references have opposites, TMF maintains both sides automatically:
// 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:
const posts = blog.getPosts(); // Returns EList<Post>
// 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();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.
TJson automatically registers packages when you "touch" them by importing and accessing their eINSTANCE:
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);Objects need ID attributes to be referenced across containment boundaries. TMF automatically generates UUIDs during serialization for objects without IDs:
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 serializedWhen deserializing, TJson creates proxy objects for external references (objects not in the containment tree):
// 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.
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!).
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.
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<string, Map<string, EObject>>();
// 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);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 <EList<EObject>>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}`);
}
}
}
}TMF powers TripSnek, a travel itinerary optimization app serving hundreds of thousands of users. It handles complex travel data models with dozens of entity types, automatic MongoDB persistence, and seamless client-server synchronization - all from a single model definition.
TMF is MIT licensed. See LICENSE for details.
TMF is inspired by the Eclipse Modeling Framework. While TMF is not a complete port of EMF, it brings the core benefits of model-driven development to the TypeScript ecosystem.
| Back | FazBrowse Home | New Git URL |