question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

module inheritance (moog) is difficult to master, developers don't know where to put their code in modules

See original GitHub issue

Here is the planned replacement for the current index.js syntax for modules. This is not backwards compatible, it’s the 3.0 format. However a conversion tool is under construction and has already been used to help convert apostrophe core itself.

In general, we are deprecating the imperative, “build the module by calling stuff” style and replacing it with a declarative style, while avoiding technical terms and invented language.

// lib/modules/shoes/index.js, a pieces subclass

module.exports = {

  extends: 'apostrophe-pieces',

  // "improve" would also go at this level

  // set up schema fields. If there are subclasses with fields they will
  // merge sensibly with what is done here.

  // fields may just be an object if you don't have any conditional fields to add, or not add, based
  // on options

  fields(self, options) => ({
    add: {
      shoeSize: {
        type: 'integer',
        label: 'Shoe Size'
      },
      price: {
        type: 'float'
      },
      // ES2015 makes option-dependent fields easy
      ...(options.specialField ? {
        special: {
          type: 'whatever'
        }
      } : {})
    },
    remove: [ 'tags' ],
    groups: {
      shoes: {
        label: 'Shoes',
        fields: [ 'shoeSize' ]
      },
      // The "utility rail" at right which appears no matter what tab is active.
      // Overuse of this space is discouraged
      utility: {
        fields: [ 'title', 'slug' ]
      }
    }
  }),

  options: {
    // "Plain old options" now go in their own distinct section. They
    // override straightforwardly in subclasses
    searchable: false
    // "name" option for pieces is dead, defaults to module name
    // as it should have in the first place, sorry
  },

  async init(self, options) {
    // options, fields and methods are ready for use here
    await self.connectToShoestoreBackend();
  },

  async afterAllSections(self, options) {
    // You'll probably never write one of these! But if you were adding support for an
    // entirely new section of module config (like routes or methods, but something
    // else) you might need to run code at this point to attach the routes to express, etc.
    // "init" has already resolved at this point and all of the sections have been attached
    // to `self`.
  },

  methods: (self, options) => ({
    async fetchMonkeys(req, doc) {
      ... code for this method ...
      // having self in scope is key here and in pretty much all other sections
      // containing functions
      self.doSomething();
    },
    doThing() {
      ... code for this method ...
    },
    doOtherThing() {
      ... code for this method ...
    },
    // Middleware we'll add selectively to certain routes
    requireAdmin(req, res, next) {
      if (!self.apos.permissions.can('admin')) {
        return res.status(403).send('forbidden');
      }
    }
  }),

  extendMethods: (self, options) => ({
    async adjustHovercraft(_super, req, doc, options) {
      await _super(req, doc, options);
      doSomethingMore();
    }
  }),

  handlers: (self, options) => ({
    'apostrophe-pages:beforeSend': {
      // Named so they can be extended
      async addPopularProducts(req) { ... },
      async addBoringBooks(req) { ... }
    }
  }),

  extendHandlers: (self, options) => ({
    'apostrophe-pages:beforeSend': {
      // Inherited from base class, we can use _super to
      // invoke the original and do more work
      async addNavigation(_super, req) { ... }
    }
  }),

  helpers: (self, options) => ({
    includes(arr, item) {
      return arr.includes(item);
    }
  }),

  extendHelpers: (self, options) => ({
    // Extend a base class helper called colorCode with a new default
    colorCode(_super, item) {
      const color = _super(item);
      return color || 'red';
    }
  }),

  // middleware registered in the middleware block runs on ALL requests
  middleware(self, options) => ({
    ensureData(req, res, next) {
      req.data = req.data || {};
      return next();
    }),
    // This middleware needs to run before that provided by another module,
    // so we need to pass an option as well as a function
    reallyEarlyThing: {
      before: 'other-module-name',
      middleware: (req, res, next) => { }
    }
  }),

  // there is no extendMiddleware because the middleware pattern does not lend itself to the
  // "super" pattern

  apiRoutes: (self, options) => ({
    post: {
      // This route needs middleware so we pass an array ending with the route function
      upload: [
        // Pass a method as middleware
        self.requireAdmin,
        // Actual route function
        async (req) => { ... }
      ],
      async insert(req) {
        // No route-specific middleware, but global middleware always applies
      }
      // Leading / turns off automatic css-casing and automatic mapping to a module URL.
      // Presto, public API at a non-Apostrophe-standard URL
      '/track-hit': async (req) => {
        // route becomes /modules/modulename/track-hit, auto-hyphenation so we can
        // use nice camelcase method names to define routes
        return self.apos.docs.db.update({ _id: req.body._id },
          { $inc: { hits: 1 } }
        );
      }
    }
  }),

  extendApiRoutes: (self, options) => ({
    post: {
      // insert route is inherited from base class, let's
      // extend the functionality
      async insert(_super, req) {
        await _super(req);
        // Now do something more...
      }
    }
  }),

  // REST API URLs can be done with `apiRoutes` but they would
  // look a little weird with `apiRoutes` because you
  // would need to specify the empty string as the name of the "get everything"
  // GET route, for instance. So let's provide a separate section
  // to make it less confusing to set them up

  restApiRoutes: (self, options) => ({
    async getAll(req) { returns everything... },
    async getOne(req, id) { returns one thing... },
    async post(req) { inserts one thing via req.body... },
    async put(req, id) { updates one thing via req.body and id... },
    async delete(req, id) { deletes one thing via id... }
    async patch(req, id) { patches one thing via req.body and id... }
  }),
  
  // extendRestApiRoutes works like extendApiRoutes of course

  components: (self, options) => ({
    // In template: {% component 'shoes:brandBrowser' with { color: 'blue' } %}
    async brandBrowser(req, data) {
      // Renders the `brandBrowser.html` template of this module,
      // with `data.brands` available containing the results of this
      // third party API call
      return {
        // Pass on parameter we got from the component tag in the template
        brands: await rq('https://shoebrands.api', { color: data.color })
      };
    }
  }),

  extendComponents: (self, options) => ({
    // Extend a component's behavior, reusing the original to do most of the work 
    async brandBrowser(_super, req, data) {
      const result = await _super(req, data); 
      if (result.color === 'grey') {
        result.color = 'gray';
      }
      return result;
    }
  }),

  // Typically used to adjust `options` before the base class sees it, if you need
  // to do that programmatically in ways the `options` property doesn't allow for.
  // Pretty rare now that `fields` is available

  beforeSuperClass(self, options) {
    // ...
  },

  // Add features to database queries, i.e. the objects returned by self.find() in this module.
  // `self` is the module, `query` is the individual query object
  queries(self, query) {
    return {
      // Query builders. These are chainable methods; they get a chainable setter method for free,
      // you only need to specify what happens when the query is about to execute
      builders: {
        free: {
          finalize() {
            const free = query.get('free');
            const criteria = query.get('criteria');
            query.set('criteria', { $and: [ criteria, { price: free ? 0 : { $gte: 0 } } ] });
          }
        }
      },
      methods: {
        // Return a random object matching the query
        async toRandomObject() {
          const subquery = query.clone();
          const count = await subquery.toCount();
          query.skip(Math.floor(count * Math.random));
          query.limit(1);
          const results = await query.toArray();
          return results[0];
        }
      }
    };
  },
};

Why do we think this is better?

  • An explicit self, options function for each section that contains functions provides a scope with access to the module. Separating those functions by section is a little wordy, but attempting to merge them in a single function leads to significant confusion. For instance, you can’t pass a method as the function for a route unless methods are initialized first in a separate call. And properties like extend must be sniffable before construction of the object begins, which means we can’t just export one big function that returns one object, or else we’d have to invoke it twice; the first time it would be without a meaningful self or options, leading to obvious potential for bugs.
  • methods is a simple and descriptive name, familiar from Vue, which has been very successful in achieving developer acceptance, even though Vue also does not use ES6 classes for not entirely dissimilar reasons. In general, Vue components have been designed with simple and descriptive language wherever possible, and we can learn from that and avoid inside baseball jargon.
  • extendMethods is a similar, however here each method’s first argument is _super, where _super is a reference to the method we’re overriding from a parent class. We now have complete freedom to call _super first, last, or in the middle in our new function. It is much less verbose than our current super pattern. Organizing all of these extensions in extendMethods makes the intent very clear. Note that if you just want to “override” (replace) a method, you declare it in methods and that straight up crushes the inherited method. extendMethods is for scenarios where you need reuse of the original method as part of the new one. We use _super because super is a reserved word.
  • handlers and extendHandlers provide similar structure for promise event handlers. Again, these get grouped together, making them easier to find, just like Vue groups together computed, methods, etc. As always, handlers must be named. Handlers for the same event are grouped beneath it. This is loosely similar to Vue lifecycle hooks, but intentionally not identical because Apostrophe involves inheritance, and everything needs to be named uniquely so it can be overridden or extended easily.
  • helpers and extendHelpers: you get the idea. For nunjucks helpers.
  • apiRoutes and extendApiRoutes: you’d actually be able to add apiRoutes, htmlRoutes and plain old routes. see recent Apostrophe changelogs if this is unfamiliar. Note subproperties separating routes of the same name with different HTTP methods.
  • fields: just… just look at it. This clobbers addFields/removeFields with tons of beforeConstruct boilerplate.
  • middleware and extendMiddleware: replaces the current expressMiddleware property, which is a bit of a hack, with a clear way to define and activate middleware globally. Note the before option which covers some of the less common but most important uses of expressMiddleware in 2.x. As for middleware that is only used by certain routes, methods are a good way to deliver that, as shown here. Note that methods will completely initialize, from base class through to child class, before routes start to initialize, so they will see the final version of each method.
  • init is a common name in other frameworks for a function that runs as soon as the module is fully constructed and ready to support method calls, etc. This replaces the inside-baseball name afterConstruct.
  • beforeSuperClass is very explicit and hopefully, finally, clarifies when this function runs: before the base class is constructed. It is the only function that runs “bottom to top,” i.e. the one in the subclass goes first. We used to call it beforeConstruct which says nothing about the timing relative to the base class. It is used to manipulate options before the parent class sees them, however most 2.x use cases have been eliminated by the introduction of the fields section.
  • queries replaces what people used to put in cursor.js files, specifically addFilters calls as well as custom methods. It obviates the need for a separate moog type for cursors, moog is now used only to instantiate modules and ceases to have its own “brand.” queries only makes sense in a module that inherits from apostrophe-doc-type-manager.

“What about breaking a module into multiple files?” Well that’s a good question, we do this now and it’s a good thing. But, nobody’s stopping anyone from using require in here. It would work like it does today, you’d pass in self or self, options to a function in the required file.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:5
  • Comments:36 (36 by maintainers)

github_iconTop GitHub Comments

1reaction
boutellcommented, Jun 21, 2019

Now that these will just be “apostrophe types”, the “moogBundle” property can just become “bundle”.

1reaction
boutellcommented, Jun 14, 2019

I changed startup to init because it’s just as familiar a word, perhaps even more so for this purpose, and it is already used by Apostrophe (afterInit events for example).

I am merging moog into a3 rather than releasing another separate npm version of moog, which has never generated much interest by itself, so it makes sense to drop an npm dependency. moog-require too.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Please help us evaluate a new syntax for Apostrophe modules ...
This is a big change, and we'd really like your input! ... module inheritance (moog) is difficult to master, developers don't know where...
Read more >
Modules vs Inheritance in Object Oriented Programming
The first of which, Inheritance, is based on the idea that you are making a new class based on an existing one.
Read more >
Interview with designers: Émilie Gillet (Mutable Instruments)
I'm very meticulous with coding and there have been very few occurrences of bugs getting into the modules once they are released.One thing...
Read more >
Clean Code: Smells and Heuristics - Hugh Nguyen
Good software developers learn to limit what they expose at the interfaces of their classes and modules. The fewer methods a class has,...
Read more >
modular programming features
Today's music: "Giorgio By Moroder" by Daft Punk Te Moog modular synthesizer Review . ... You'll learn how to create module definitions, setup...
Read more >

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found