#Dynamically defined classes (an Entity engine)

15 messages · Page 1 of 1 (latest)

acoustic flame
#

NestJS newbie here. Just started messing around with it (but been coding a long, long, long time. . . .)

I am looking to define a class dynamically. That is, I'd like to define its fields, types, and validations in some type of database (e.g. a JSON file) that gets loaded at runtime.

Is there a pattern or example of something like this for NestJS? I am planning on persisting data to MongoDB or similar, so I don't need to worry about the database schema as a part of this.

Essentially I'm trying to create an "Entity Engine" where the definition of what Entities are available and their properties are defined elsewhere and interpreted at runtime.

spark spruce
#

@acoustic flame - I haven't gotten to it myself, just a very rough POC, but my intentions are to create a templating system to write the code. Metadata in the database will be read and the code generated from it.

acoustic flame
#

Thoughts on how to proceed?

spark spruce
#

Welp. I'm going to be using https://ejs.co/ for the templating system. I started my POC with handlebars, but after more research came to the conclusion that ejs would be a better templating system for what I need it for.

As an example of my "first" attempt, this is what an entity (with Typegoose) class template file would look like (this was with Handlebars):

// This is an auto-generated file - do not modify!! 
// Modifications will be overwritten. 

import { Prop, Index, ModelOptions } from '@typegoose/typegoose'
import { BaseEntity } from '@core/base/base.entity'

<{#each indexes as |index|}>
<{>index index=.}>
<{/each}>
<{#with modelOptions}>
<{>modelOption modelOptions=.}>
<{/with}>
class <{entityName}> extends BaseEntity {

<{#each properties as |prop|}>
  <{>prop prop=.}>
<{/each}>
}
#

So, that would be used in a call from a generator (again, this is with Handlebars):

export class IndexGenerator {
  static generateIndexPartial (hbs: typeof Handlebars) {
    const templatePath = join(__dirname, '/templates/partials/entity/index.tmpl')
    let indexTemplate = readFileSync(templatePath, 'utf-8')
    indexTemplate = indexTemplate + os.EOL
    return hbs.registerPartial('index', indexTemplate)
  }
}
#

And lastly, I would have a service. This service is hardcoded to produce the entity code, but imagine it being abstract to produce any template. You'll notice, this is where the "data" comes in too.

@Injectable()
export class CodeGenService {
  constructor () {
    delimiters(hbs, ['<{', '}>'])
  }

  async generate () {
    const templatePath = join(__dirname, '/templates/entity.class.tmpl')
    const template = readFileSync(templatePath, 'utf-8')
    const compiledTemplate = hbs.compile(template)
    registerHelpers(hbs)
    registerGlobalPartials(hbs)
    IndexGenerator.generateIndexPartial(hbs)
    ModelOptionsGenerator.generateModelOptionPartial(hbs)
    PropGenerator.generatePropPartial(hbs)
    const finishedTemplate = compiledTemplate(testData)
    console.log(finishedTemplate)
  }
}
#

I was only console.logging the "file" in the end to see what it was looking like, but this result would be written to the file system. Then you can run your app.

#

This was the data being used. Very, very basic... 🙂

#
export const testData = {
  entityName: 'testEntityName',
  indexes: [
    {
      type: 'single',
      fields: [
        {
          name: 'foo',
          type: 1
        }
      ],
      options: [
        {
          key: 'unique',
          value: true
        }
      ]
    },
    {
      type: 'single',
      fields: [
        {
          name: 'bar',
          type: 1
        }
      ],
      options: [
        {
          key: 'expireAfterSeconds',
          value: 360000
        },
        {
          key: 'partialFilterExpression',
          value: '{ baz: { $gt: 5 } }'
        },
        {
          key: 'hidden',
          value: true
        }
      ]
    },
    {
      type: 'compound',
      fields: [
        {
          name: 'bar',
          type: 1
        },
        {
          name: 'baz',
          type: 1
        }
      ],
      options: []
    },
    {
      type: '2dsphere',
      fields: [
        {
          name: 'fooCoordinates',
          type: '2dsphere'
        }
      ],
      options: []
    }
  ],
  modelOptions: [
    {
      type: 'schemaOptions',
      options: {
        key: 'collection',
        value: 'SomeCollectionName'
      }
    },
    {
      type: 'options',
      options: {
        key: 'customName',
        value: 'SomeOtherModelName'
      }
    }
  ],
  properties: [
    {
      name: 'someProp1',
      type: 'number',
      required: false,
      propOptions: [
        {
          key: 'unique',
          value: true
        }
      ]
    },
    {
      name: 'someProp2',
      type: 'string',
      required: true,
      propOptions: [
        {
          key: 'unique',
          value: true
        },
        {
          key: 'required',
          value: true
        },
        {
          key: 'default',
          value: new Handlebars.SafeString(func.toString())
        }
      ]
    },
    {
      name: 'someProp3',
      type: 'boolean',
      required: true,
      propOptions: []
    }
  ]
}
#

But, imagine this data being pulled from a database. 🙂

acoustic flame
#

Very intriguing start. So what about mapping these templated classes to persistence? I'm looking to use MongoDB.

spark spruce
#

@acoustic flame - The data example could come from a database, as I noted.

acoustic flame
#

Oh, I see. So you have the field mappings in the JS object!
The use of Handlebars/EJS is an intriguing one. I have Handlebars used in several places in the rest of my stack already.

spark spruce
#

So you have the field mappings in the JS object!
Exactly.

acoustic flame
#

hmm....willing to collaborate on this?