Meteor Giga-Factory 🚀

I know this is a catchy name but it’s a name used by the “lord of announcement” himself and it fits this announcement very well :slight_smile:

So what’s this about?

After lot’s of rewrites, refactoring and abstractions I came up with some abstract factory packages that should be flexible enough to be used in nearly every project (and from older Meteor versions to the newest):

A simple example

All of them together can act like this huge factory that allows you to throw on your objects and just creates all you need to run your app within a few steps. Consider this simple Article Object:

export const Article = {
  name: 'article'
}

Article.collection = () => {
  throw new Error('not yet created')
}

Article.schema = {
  title: String,
  text: String
}

Article.methods = {
  create: {
    name: 'article.methods.create',
    schema: Article.schema,
    run: function ({ title, text }) {
      return Article.collection().insert({ title, text })
    }
  }
}

Article.publications = {
  all: {
    name: 'article.publications.all',
    run: function () {
      return Article.collection().find()
    }
  }
}

Article.http = {
  byTitle: {
    path: '/article',
    method: 'get',
    schema: {
      title: String
    }
    run: function (/* req, res, next */) {
      const { title } = this.data()
      return Article.collection().findOne({ title })
    }
  }
}

A simple factory pipeline

Now let’s create our (simplified) factory pipeline:

import { createCollectionFactory } from 'meteor/leaonline:collection-factory'
import { createMethodFactory } from 'meteor/leaonline:method-factory'
import { createPublicationFactory } from 'meteor/leaonline:publication-factory'
import { createHTTPFactory } from 'meteor/leaonline:http-factory'
import { rateLimitMethods, rateLimitPublications } from 'meteor/leaonline:ratelimit-factory'
import SimpleSchema from 'simpl-schema'

// create a simple factory function for schema validation
const simpleSchemaFactory  = definitions => new SimpleSchema(definitions)

// alternatively you could use check/Match: 
const checkMatchFactory = schema => ({
  validate (args) {
    check(args, schema)
  }
})

// default mixin to check permission on each method / publication
const permissionMixin = function (options) {
  const runFct = options.run
  options.run = function run (...args) {
    if (!this.userId || !Meteor.users.findOne(this.userId)) 
      throw new Meteor.Error('errors.permissionDenied', 'errors.userNotExists', this.userId)
    return runFct.apply(this, args)
  }
  return options
}

// with schema = support for aldeed:collection2
const createCollection = createCollectionFactory({ schemaFactory })
// all methods and publications are now private and validate input by default
const createMethod = createMethodFactory({ schemaFactory, mixins: [permissionMixin] })
const createPublication = createPublicationFactory({ schemaFactory, mixins: [permissionMixin] })
// HTTP params/queryparams are validated via schema
const createHttpRoute = createHTTPFactory({ schemaFactory })


export const factoryPipeline = ({ name, schema, methods, publications, http }) => {
  const product = {}
  product.collection = createCollection({ name, schema })
  
  const methodsInput = Object.values(methods || [])
  product.methods = methods.map(methodsInput)
  rateLimitMethods(methodsInput)

  const publicationsInput = Object.values(publications || [])
  product.publications = publications.map(publicationsInput)
  rateLimitPublications(publicationsInput)

  const httpInput = Object.values(http || [])
  product.http = http.map(createHttpRoute)

  return product
}

Note: If you wonder, why you first need to create the factories - it allows to create different factories, where each has a certain set of default configuration (think of one for all public methods and one for all methods for logged-in users), which themselves can then even further be configured by the input.

Putting all together

From here we can now simply import our definition Objects at startup and throw them into the factory pipline:

import { Article } from '/path/to/article'
import { factoryPipeline } from '/path/to/factoryPipeline'
export { runRateLimiter } from 'meteor/leaonline:ratelimit-factory'

const allInputs = [Article]

// here we not only create the products but also
// add the collection to the Objects to make them accessible
// in thier run functions of their methods / publications / http routes
allInputs.forEach(input => {
  const product = factoryPipeline(input)
  const { collection } = product
  input.collection = () => collection
})

// provide a callback that is used when rate-limit has been exceeded
runRateLimiter(function callback (reply, input) {
  if (reply.allowed) {
     // ...
  } else {
     // log exceeding, if you like here
    console.log('rate limit exceeded')
    console.log(reply)
    console.log(input)
  }
})

Why all this?

I know this has been already invented in similar packages and projects but I always had the issue, that they

  • had many dependencies
  • were not flexible / extendible enough for edge cases
  • contain lots of code
  • all have different API designs, making it hard to switch between them
  • cannot easily replaced in case of abandonment

I made these packages fix the mentioned issues and I hope they do it for you, too!

What you can do :slight_smile:

It would be great to get any feedback on these packages, especially if they are flexible enough to cover your use-cases. Contributions are always welcome and I would be super happy to see others using these packages in their projects.

26 Likes

Great name :slight_smile: and very nice concept! I’ll have to give this some more thought about how I could benefit with this approach in my app. I could see it saving some time & creating a consistent approach to some common Meteor server coding tasks. Thanks for sharing it! :+1:

1 Like

These look really good!

I suspect many of us with larger apps end up with internal packages/patterns along these lines (for example the quavedev packages e.g. quave:collections by @filipenevola).

Adding one or more of these into the Meteor Guide would probably help with adoption and growing the ecosystem.

1 Like