{"id":31193116,"url":"https://github.com/tripsnek/tmf","last_synced_at":"2025-09-20T00:06:47.359Z","repository":{"id":309265196,"uuid":"1035635471","full_name":"tripsnek/tmf","owner":"tripsnek","description":"TypeScript Modeling Framework - A TypeScript port of the Eclipse Modeling Framework (EMF) for Node/Angular/React environments,","archived":false,"fork":false,"pushed_at":"2025-09-08T01:50:40.000Z","size":788,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-08T22:26:34.536Z","etag":null,"topics":["code-generation","eclipse-modeling-framework","ecore","emf","metamodel","modeling","typescript"],"latest_commit_sha":null,"homepage":"https://www.tripsnek.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tripsnek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-10T20:16:48.000Z","updated_at":"2025-09-08T01:50:44.000Z","dependencies_parsed_at":"2025-08-10T22:47:41.913Z","dependency_job_id":"0633aa42-daab-4656-a80c-d84d9ee10e34","html_url":"https://github.com/tripsnek/tmf","commit_stats":null,"previous_names":["tripsnek/tmf"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/tripsnek/tmf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripsnek%2Ftmf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripsnek%2Ftmf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripsnek%2Ftmf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripsnek%2Ftmf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tripsnek","download_url":"https://codeload.github.com/tripsnek/tmf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripsnek%2Ftmf/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276019998,"owners_count":25571441,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-19T02:00:09.700Z","response_time":108,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-generation","eclipse-modeling-framework","ecore","emf","metamodel","modeling","typescript"],"created_at":"2025-09-20T00:03:12.467Z","updated_at":"2025-09-20T00:06:47.330Z","avatar_url":"https://github.com/tripsnek.png","language":"TypeScript","readme":"# TypeScript Modeling Framework (TMF)\n\n[![npm version](https://img.shields.io/npm/v/@tripsnek/tmf.svg)](https://www.npmjs.com/package/@tripsnek/tmf)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nTMF 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.\n\n## A Quick Demo video\n\n[https://github.com/user-attachments/assets/ee35ca1a-24d5-4a43-8926-96dffecd8d0e](https://github.com/user-attachments/assets/208731e7-e674-45d6-af5b-6426763feed9)\n\nQuick 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](https://github.com/tripsnek/tmf-examples) repository (specifically the [NX Angular/Node example](https://github.com/tripsnek/tmf-examples/tree/main/angular-node-nx)).\n\n## Why TMF?\n\nTraditional 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.\n\nTMF can help eliminate this repetition through powerful runtime reflection and code generation. By default this includes:\n\n - Runtime enforcement of **containment relationships**. Convert an object to JSON, all of its nested objects go with it, and unpack neatly on the other side.\n - Runtime enforcement of **bi-directional relationships**. For example, imagine a tree structure of objects with \"parent\" and \"children\" relationships. You can add Y to X.children or set Y.parent to X, and the inverse is automatically maintained.\n - Runtime **reflection/introspection** capabilities: Each instance provides convenient facilties for navigating and manipulating its structure and relationships without needing to code against the specific types and features.\n - **Code generated** source files for each data type that - beyond basic get/set functionality - provides all of the aforementioned capability.\n - **Serialization** (with TJson) that exploits containment relationships to turn complex object graphs into coherent trees. Its like if JSON.stringify() actually did something useful, and it is made possible by reflection.\n - **Editable implementation files** for each data type that let you extend the API for each type as you wish, enabling you to serialize directly to and from the same data objects that you use across your stack.\n - A **Visual Model Editor** VSCode extension ([TMF Ecore Editor](https://github.com/tripsnek/tmf-ecore-editor)) for intuitively editing your models and generating code with a few mouse clicks. \n\nFor 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:\n\n- REST APIs that generate themselves - e.g. CRUD endpoints for each \"root\" container type\n- UI components that build themselves - e.g. properties sheets that automatically build editors for each attribute field\n- Database persistence layers that require zero manual mapping\n- \"Proxy resolution\" strategies that identify references across containers and resolve them post-deserialization.\n- In-place merge logic that can automatically diff to versions of the same instance and apply the changes from one to another (useful when an instance is already bound in your UI)\n- Your own customized serialization strategies for your own data formats, or to satisfy integration or legacy data requirements\n\nThis README describes the basics of how reflection works, and many are demonstrated in the [tmf-examples](https://github.com/tripsnek/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.\n\n ## When is TMF useful?\n\nThere is no one-size-fits-all software design strategy. TMF is useful when:\n\n 1. Your domain model has lots of different types of entities with **nested structure** (that is, objects that are \"part\" of other objects).\n \n 2. Your entities are associated with interesting behavior, which can then simply become API methods on the entities themselves.\n \n 3. You need to make use of (1) and/or (2) on both your frontend and backend.\n \nIf 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.\n\n## Installation\n\n```bash\nnpm install @tripsnek/tmf\n```\n\n[Optonal] For visual model editing, install the VSCode extension:\n1. Open VSCode\n2. Search for \"TMF Ecore Editor\" in extensions\n3. Install the extension\n\n## Quick Start\n\n### Step 1: Create Your Model\n\nCreate a new file `\u003cyour-model-name\u003e.ecore` in VSCode. The TMF Ecore Editor will auto-initialize it with a package:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cecore:EPackage xmi:version=\"2.0\" xmlns:xmi=\"http://www.omg.org/XMI\" \n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xmlns:ecore=\"http://www.eclipse.org/emf/2002/Ecore\" \n    name=\"blog\" nsURI=\"http://example.org/blog\" nsPrefix=\"blog\"\u003e\n\u003c/ecore:EPackage\u003e\n```\n\nUse 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:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cecore:EPackage xmi:version=\"2.0\" xmlns:xmi=\"http://www.omg.org/XMI\" \n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xmlns:ecore=\"http://www.eclipse.org/emf/2002/Ecore\" \n    name=\"blog\" nsURI=\"http://example.com/blog\" nsPrefix=\"blog\"\u003e\n  \n  \u003ceClassifiers xsi:type=\"ecore:EClass\" name=\"Blog\"\u003e\n    \u003ceStructuralFeatures xsi:type=\"ecore:EAttribute\" name=\"id\" \n        eType=\"ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString\"/\u003e\n    \u003ceStructuralFeatures xsi:type=\"ecore:EAttribute\" name=\"title\" \n        eType=\"ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString\"/\u003e\n    \u003ceStructuralFeatures xsi:type=\"ecore:EReference\" name=\"posts\" \n        upperBound=\"-1\" eType=\"#//Post\" containment=\"true\" eOpposite=\"#//Post/blog\"/\u003e\n  \u003c/eClassifiers\u003e\n  \n  \u003ceClassifiers xsi:type=\"ecore:EClass\" name=\"Post\"\u003e\n    \u003ceStructuralFeatures xsi:type=\"ecore:EAttribute\" name=\"title\" \n        eType=\"ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString\"/\u003e\n    \u003ceStructuralFeatures xsi:type=\"ecore:EAttribute\" name=\"content\" \n        eType=\"ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString\"/\u003e\n    \u003ceStructuralFeatures xsi:type=\"ecore:EReference\" name=\"blog\" \n        eType=\"#//Blog\" eOpposite=\"#//Blog/posts\"/\u003e\n  \u003c/eClassifiers\u003e\n\u003c/ecore:EPackage\u003e\n```\n\n### Step 2: Generate TypeScript Code\n\nClick \"Generate Code\" in the VSCode editor, or run TMF's code generator. This creates type-safe TypeScript classes with full metamodel support.\n\nYou can also invoke TMF directly using 'npx' as follows: ```npx @tripsnek/tmf ./path/to/your/\u003cmyecorefile\u003e.ecore```\n\nThis will create three folders in a src/ directory adjacent to the .ecore file:\n\n - `api/` contains interfaces for each of your types, as well as `*-package.ts`and `*-factory.ts` that define the metamodel at runtime and allow for reflective instantiation.\n - `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**\n - `impl/` contains (initially empty) concrete classes you can extend as you like. **THESE ARE SAFE TO EDIT**\n\nThe generator can be configured in various useful ways, see ```npx @tripsnek/tmf --help``` for more information.\n\n```typescript\nexport class BlogImpl extends BlogImplGen implements Blog {\n\n  // Implement any operations you defined for your eclass in Ecore\n  myBlogOperation(): void {\n   //do something interesting\n  }\n\n  // Or add any other custom business logic that isn't exposed at the interface level\n  validate(): boolean {\n    return this.getTitle() !== null;\n  }\n    \n}\n```\n\n### Step 3: Use Your Model\n\n```typescript\nimport { BlogFactory, BlogPackage, Blog, Post } from '@myorg/blog';\nimport { TJson } from '@tripsnek/tmf';\n\n// Initialize the package by 'touching' it (required for TJson serialization)\nconst pkg = BlogPackage.eINSTANCE;\nconst factory = BlogFactory.eINSTANCE;\n\n// Create instances\nconst blog = factory.createBlog();\nblog.setTitle(\"My Tech Blog\");\nblog.setId(\"blog_1\");\n\nconst post = factory.createPost();\npost.setTitle(\"Introduction to TMF\");\npost.setContent(\"TMF makes model-driven development straightforward...\");\n\n// Containment: adding post to blog automatically sets the inverse reference\nblog.getPosts().add(post);\nconsole.log(post.getBlog() === blog); // true - automatically maintained!\n\n// Serialize to JSON\nconst json = TJson.makeJson(blog);\nconsole.log(JSON.stringify(json, null, 2)); //your SAFELY stringified object\n\n// Deserialize from JSON\nconst blogCopy = TJson.makeEObject(json) as Blog;\nconsole.log(blogCopy.getPosts().get(0).getTitle()); // \"Introduction to TMF\"\n```\n\n## Understanding EMF Concepts\n\n### Core Elements\n\n**EPackage** - The root container for your model, defines namespace and contains classifiers\n\n**EClass** - Represents a class in your model. Can be:\n- *Concrete* - Standard instantiable class\n- *Abstract* - Cannot be instantiated directly\n- *Interface* - Defines contract without implementation\n\n**EAttribute** - Simple typed properties (String, Int, Boolean, etc.)\n\n**EReference** - Relationships between classes, with two key concepts:\n- *Containment* - Parent-child relationship where child lifecycle is determined by parent\n- *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\"](https://en.wikipedia.org/wiki/Domain-driven_design#aggregate_root) - the bundle of data that goes between your server and client all at once.\n\n**EOperation** - Methods on your classes with parameters and return types\n\n**EEnum** - Enumeration types with literal values\n\n### EMF Data Types\n\nWhen defining attributes and operation parameters, you can use these built-in Ecore data types:\n\n**Primitive Types**\n- `EString` - Text values (TypeScript: `string`)\n- `EInt|EDouble|EFloat` - Numeric values with no distinction in TS (TypeScript: `number`)\n- `EBoolean` - True/false values (TypeScript: `boolean`)\n- `EDate` - Date/time values (TypeScript: `Date`)\n\n**Classifier Types**\n- `EClass` - References to other classes in your model\n- `EEnum` - Your custom enumerations become TypeScript enums\n\n**Type Modifiers**\n- **Multiplicity**: Single-valued or Many-valued\n- **ID**: Marks an attribute as the unique identifier\n- **Transient**: Not persisted when serializing\n\n### Key Modeling Patterns\n\n**Containment Hierarchies**  \nWhen a reference has `containment=true`, the reference creates parent-child hierarchies where children follow their parent's lifecycle:\n\n```typescript\nconst blog = factory.createBlog();\nconst post1 = factory.createPost();\nconst post2 = factory.createPost();\n\n// Posts are contained by blog\nblog.getPosts().add(post1);\nblog.getPosts().add(post2);\n\n// When you serialize the blog, all contained posts are included\nconst json = TJson.makeJson(blog);  // Includes all posts\n```\n\n**Inverse References**  \nWhen references have opposites, TMF maintains both sides automatically:\n```typescript\n// Setting one side...\nblog.getPosts().add(post);\n// ...automatically sets the other\nconsole.log(post.getBlog() === blog); // true!\n```\n\n**ELists**\n\nWhen 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:\n\n```typescript\nconst posts = blog.getPosts(); // Returns EList\u003cPost\u003e\n\n// Standard operations\nposts.add(newPost);\nposts.remove(oldPost);\nposts.get(0);\nposts.size();\nposts.clear();\n\n// Iterate\nfor (const post of posts.elements()) {\n  console.log(post.getTitle());\n}\n\n// Convert to array when needed\nconst array = posts.elements();\n```\n\n## TJson Serialization\n\nTMF's `TJson` provides robust JSON serialization that preserves object relationships, containment hierarchies, and type information - enabling seamless data exchange between frontend and backend systems.\n\n### Package Registration\n\nTJson automatically registers packages when you \"touch\" them by importing and accessing their `eINSTANCE`:\n\n```typescript\nimport { BlogPackage } from '@myorg/blog';\nconst pkg = BlogPackage.eINSTANCE; // Auto-registers BlogPackage and subpackages\n\n// Now TJson can serialize/deserialize Blog objects\nconst json = TJson.makeJson(blog);\nconst copy = TJson.makeEObject(json);\n```\n\n### ID Attributes and Cross-References\n\nObjects need ID attributes to be referenced across containment boundaries. TMF automatically generates UUIDs during serialization for objects without IDs:\n\n```typescript\nconst blog = factory.createBlog(); \nblog.setId(\"blog_1\"); // Set your own ID, or...\n\n// TJson assigns UUIDs during serialization if no ID exists\nconst json = TJson.makeJson(blog); // UUID auto-generated here if needed\n\n// Containment: child objects are serialized inline\nblog.getPosts().add(post); // Post serialized with Blog\n\n// Cross-references: only IDs are serialized\nblog.setAuthor(externalUser); // Only author ID serialized\n```\n\n### Proxy Objects\n\nWhen deserializing, TJson creates proxy objects for external references (objects not in the containment tree):\n\n```typescript\n// External user referenced by blog\nconst deserializedBlog = TJson.makeEObject(json);\nconst author = deserializedBlog.getAuthor();\n\nif (author.eIsProxy()) {\n  // Proxy contains ID and type, load actual object as needed\n  const authorId = author.getId();\n  const realAuthor = await loadUserFromDatabase(authorId);\n  deserializedBlog.setAuthor(realAuthor);\n}\n```\n\n**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.*\n\n## Leveraging Reflection\n\nTMF'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!).\n\n### Example: Building a Generic REST CRUD server\n\nThis 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.\n\n```typescript\nimport express from 'express';\nimport { EClass, EObject, TJson, TUtils } from '@tripsnek/tmf';\nimport { BlogPackage } from '@myorg/blog';\n\nconst app = express();\napp.use(express.json());\n\n// Initialize your package\nconst pkg = BlogPackage.eINSTANCE;\n\n// Storage for instances (in production, this would be a database)\nconst storage = new Map\u003cstring, Map\u003cstring, EObject\u003e\u003e();\n\n// Get all \"root\" classes (those which are not contained by anything else) from your model\nconst rootClasses = TUtils.getRootEClasses(pkg);\n\n// Initialize storage for each class\nrootClasses.forEach(eClass =\u003e {\n  storage.set(eClass.getName(), new Map());\n});\n\n// Generate CRUD endpoints for each class automatically\nrootClasses.forEach(eClass =\u003e {\n  const className = eClass.getName();\n  const classStore = storage.get(className)!;\n  \n  // GET all instances\n  app.get(`/api/${className}`, (req, res) =\u003e {\n    const instances = Array.from(classStore.values());\n    res.json(TJson.makeJsonArray(instances));\n  });\n  \n  // POST new instance\n  app.post(`/api/${className}`, (req, res) =\u003e {\n    const instance = TJson.makeEObject(req.body)!;\n    // Get ID dynamically using reflection\n    const idAttr = instance.eClass().getEStructuralFeature('id');\n    if (idAttr) {\n      const id = String(instance.eGet(idAttr));\n      classStore.set(id, instance);\n    }\n    res.json(TJson.makeJson(instance));\n  });\n  \n  // Additional endpoints: GET by ID, PUT, DELETE...\n});\n\napp.listen(3000);\n```\n\n### Walking Object Trees with Reflection\n\n```typescript\nimport { EObject } from '@tripsnek/tmf';\n\n// Process any object and its recursively contained children\nfunction processTree(root: EObject) {\n  console.log(`Processing ${root.eClass().getName()}`);\n  \n  // Iterate through entire containment tree of objects\n  for (const ref of root.eClass().getEAllReferences()) {\n\n    //only traverse containment refs\n    if(ref.isContainment()){\n      //process many-valued (EList)\n      if(ref.isMany()){\n        for (const containedObj of \u003cEList\u003cEObject\u003e\u003eobj.eGet(ref)){\n          processTree(containedObj);\n        }\n      }\n      //process single-valued\n      else{\n        const containedObj = obj.eGet(ref);\n        if(containedObj){\n          processTree(containedObj)\n        }\n      }\n    }\n  }\n}\n\n//...or you could just iterate the tree as a flattened\n//array via eAllContents()\nfunction processTreeWithEAllContainets(root: EObject) {\n  console.log(`Processing ${root.eClass().getName()}`);\n  \n  // Iterate through entire containment tree of objects\n  for (const child of root.eAllContents()) {\n    console.log(`  Processing contained: ${child.eClass().getName()}`);\n  }\n}\n\n// Dynamically access all attributes\nfunction printAllAttributes(obj: EObject) {\n  const eClass = obj.eClass();\n  \n  for (const attr of eClass.getEAllAttributes().elements()) {\n    const value = obj.eGet(attr);\n    console.log(`${attr.getName()}: ${value}`);\n  }\n}\n\n// Find references to non-contained objects\nfunction findReferences(obj: EObject) {\n  const eClass = obj.eClass();\n  \n  for (const ref of eClass.getEAllReferences().elements()) {\n    if (!ref.isContainment()) {  // Skip containment refs\n      const value = obj.eGet(ref);\n      if (value) {\n        console.log(`Reference ${ref.getName()} points to ${value}`);\n      }\n    }\n  }\n}\n```\n\n## Examples and Resources\n\n- **[TMF Ecore Editor](https://github.com/tripsnek/tmf-ecore-editor)** - VSCode extension for visual model editing\n- **[TMF npm package](https://www.npmjs.com/package/@tripsnek/tmf)** - The installable TMF npm library \n- **[tmf-examples](https://github.com/tripsnek/tmf-examples)** - Complete applications demonstrating TMF patterns with Node, Java Spring Boot, Angular, React, and Nx\n- **[Eclipse EMF](https://eclipse.dev/emf/docs.html)** - Original EMF documentation\n- **[TripSnek](https://www.tripsnek.com)** - Production application built with TMF\n\n## Real-World Usage\n\nTMF powers [TripSnek](https://www.tripsnek.com), 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.\n\n## License\n\nTMF is MIT licensed. See [LICENSE](LICENSE) for details.\n\n## Acknowledgments\n\nTMF is inspired by the [Eclipse Modeling Framework](https://www.eclipse.org/modeling/emf/). While TMF is not a complete port of EMF, it brings the core benefits of model-driven development to the TypeScript ecosystem.\n","funding_links":[],"categories":["Development Utilities"],"sub_categories":["Generators and Scaffolding"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftripsnek%2Ftmf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftripsnek%2Ftmf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftripsnek%2Ftmf/lists"}