# apostrophe-module
This "module" is the base class for all other modules. This module is never actually configured and used directly. Instead all other modules extend it (or a subclass of it) and benefit from its standard features, such as asset pushing.
New methods added here should be lightweight wrappers that invoke
an implementation provided in another module, such as apostrophe-assets
,
with sensible defaults for the current module. For instance,
any module can call self.render(req, 'show', { data... })
to
render the views/show.html
template of that module.
TODO: wrapper for adding command-line tasks.
In the meantime it is recommended that you always use
your module's name and a colon as the prefix for a task name when
calling self.apos.tasks.add
.
# Methods
# emit(*name *, , arg1, arg2...) [events]
Emit an event from this module. Returns a promise.
The promise will not resolve until all of the event handlers have resolved, in the order they were registered. Note that it is OK for event handlers to return a simple value rather than a promise, in which case they resolve immediately.
Any extra parameters passed after name
are passed to
the event handlers as parameters, in the order given.
See the on
method.
# on(name, methodName, fn) [events]
Register an event handler method in this module. The
given method name will be invoked when the given event
name is emitted with emit
. As a shortcut, you may
optionally pass a function as a third argument. That
function becomes a method of your module called methodName
.
This is exactly the same as defining it the normal way.
Your method may return a promise. If it does, the next
event handler method will not begin running until your
promise resolves. If your promise rejects, the chain
stops and the promise returned by the emit
method
also rejects.
"What about events of other modules?" Register them
with this name
syntax: module-name:methodName
. This is
similar to the "cross-module includes" syntax used
elsewhere in Apostrophe.
"Why do I need a method name? Why not a function alone?"
It should always be possible for subclasses to intentionally
override or extend your method via the super
pattern.
"Why can't I use a methodName that is identical to the event name?" Doing so sets you up for an accidental collision with other event handlers in subclasses. Your method name should describe what your method does in response to the event.
# callAllAndEmit(callAllName, eventName, callback, argument, ...) [events]
Invoke a callAll method and emit a new-style promise event. callAll
is invoked first,
and if its callback does not receive an error, emit
is invoked. When the promise
returned by emit
resolves, the final callback is invoked. A promise interface
is not provided here because this method should only be used to migrate away from
legacy callAll
invocations. New code should always emit a promise event and
avoid callAll
.
# route(method, path, responder, fn)
Add an Express route to apos.app. The path
argument is
appended to the "action" of this module, which is
/modules/modulename/
.
Calling this method again allows routes to be overridden, which you normally can't do in Express.
Syntax:
self.route('post', 'edit-monkey', function(req, res) { ... })
That is roughly equivalent to:
self.apos.app.post(self.action + '/edit-monkey', function(req, res) { ... })
You can also pass middleware in the usual way, after
the path
argument. Note that some standardized optional
middleware is available to pass in this way, i.e.
self.apos.middleware.files
for file uploads.
The omittable responder
argument is a string containing the
name of a method of this module used to send a response when given
(req, err, value)
. It is normally provided for you by a call to
self.apiRoute
or self.htmlRoute
. It is not invoked at all
unless the route invokes its third argument, next()
, with
an error (or null) and an optional value argument. next() cannot be
used in a route without a responder. (It behaves as normal for Express
in a middleware function.)
# apiRoute(method, path, fn)
Similar to route
, except that the route function receives
(req, res, next), and you may pass next
either an error or
(null, result)
where result
is a JSON-friendly object to be sent
to the browser; a status: 'ok'
property is automatically
added.
An exception: if result
is an array, it is sent without a
status property. This loophole is needed because arrays cannot have
extra properties in JSON and certain legacy APIs send back arrays
without an enclosing object.
If you do pass an error to next
, the status code is still 200
but the browser receives an object whose status
property is not ok
.
If the error is a string, it is sent as the status
, otherwise
the error is logged and the status is simply error
to avoid
revealing information that could be used maliciously.
An exception: mongodb errors relating to duplicate keys are sent
as the status unique
, which is useful for suggesting to users
that they may need to use a different username, etc.
If you require additional properties for the JSON object sent
for an error, you can pass an object containing them as the
third argument to next
. This is separate from the success value
to avoid accidentally disclosing information on errors.
In addition, if req.errorMessages
is set, it is
passed as the messages
property of the JSON object.
This is a helpful way to accommodate percolating detailed
error messages up from a nested function call through a
stack that otherwise only accommodates simple string errors.
# htmlRoute(method, path, fn)
Similar to route
, except that the route function receives
(req, res, next), and you may pass next
either an error or
(null, result)
where result
is an HTML string to be sent
to the browser; a 200
status is automatically used.
If you do pass an error to next
, and the error is a string,
the string is converted to an error code according to
the following rules: notfound
=> 404, invalid
=> 401,
forbidden
=> 403, error
=> 500, anything else => 500.
If the error is not a string, it is converted to a 500 error
and logged for the developer on the server side only,
to avoid revealing information that could be used maliciously.
# renderRoute(method, path, fn)
Similar to htmlRoute
, except that the route function receives
(req, res, next), and you may pass next
either an error or
(null, result)
where result
is an object with a template
property naming a template in the current module, and optionally
a data
property containing an object to be passed to the template.
The result of template rendering is sent directly to the client, with a
200 status code.
If you do pass an error to next
, and the error is a string,
the string is converted to an error code according to
the following rules: notfound
=> 404, invalid
=> 401,
forbidden
=> 403, error
=> 500, anything else => 500.
If the error is not a string, it is converted to a 500 error
and logged for the developer on the server side only,
to avoid revealing information that could be used maliciously.
# apiResponder(req, err, result, extraError)
See apiRoute for details. You should not call this directly.
# htmlResponder(req, err, result)
See htmlRoute for details. You should not call this directly.
# renderResponder(req, err, result)
See renderRoute for details. You should not call this directly.
# addHelpers(*object *, or name, value)
Add nunjucks template helpers in the namespace for our module. Typically called
with an object in which each property is a helper name and each value
is a helper function. Can also be called with name, function
to add
just one helper.
# addHelperShortcut(name)
Add a nunjucks template helper to the global namespace. This should
be used very sparingly, and pretty much never in npm modules. The
only exceptions in apostrophe core are apos.area
and apos.singleton
.
The helper must be added first with addHelpers
.
# pushAsset(type, name, options)
Push an asset to the browser. type
may be script
or stylesheet
.
The second argument is the name of the file, without the extension.
For stylesheets, if name
is editor
, then public/css/editor.css
is pushed. If public/css/editor.less
exists it is compiled as needed
to create the CSS version.
For scripts, if name
is editor
, then public/js/editor.js
is pushed.
For both scripts and stylesheets, if the module is subclassed, and the file exists in both the parent module and the subclass, both files are pushed, in that order.
If options.when
is set to always
or not specified, the asset is
included in every page regardless of whether the user is logged in. If
options.when
is set to user
, it is included only if the user is logged in.
# render(req, name, data)
Render a template. Template overrides are respected; the project level lib/modules/modulename/views folder wins if it has such a template, followed by the npm module, followed by its parent classes. If you subclass a module, your version wins if it exists.
You MUST pass req as the first argument. This allows internationalization/localization to work. If you are writing a Nunjucks helper function, use self.partial instead. This method is primarily used to implement routes that respond with HTML fragments.
All properties of data
appear in Nunjucks templates as
properties of the data
object. Nunjucks helper functions
can be accessed via the apos
object.
If not otherwise specified, data.user
and
data.permissions
are provided for convenience.
The data argument may be omitted.
# renderAndSend(req, name, data)
Similar to render
, but sends the response to the client, ending the
request. If an exception is thrown by self.render
, the error is
properly logged and a 500 error is correctly sent to the client.
Convenient in routes that simply send the markup for a modal, for instance,
especially if for legacy reasons they must use self.route rather tha
self.renderRoute.
# partial(name, data)
For use in Nunjucks helper functions. Renders a template,
in the context of the same request that started the
original call to Nunjucks. Otherwise the
same as render
.
# renderString(req, s, data)
Render a template in a string (not from a file), looking for includes, etc. in our preferred places.
Otherwise the same as render
.
# partialString(req, s, data)
For use in Nunjucks helper functions. Renders a template
found in a string (not a file), in the context of the
same request that started the original call to Nunjucks.
Otherwise the same as partial
.
# renderer(name, data)
Returns a function that can be used to invoke self.render at a later time. The returned function must be called with req. You may pass data now and also when invoking the function; data passed now serves as defaults for the object passed later.
# partialer(name, data)
Returns a function that can be used to invoke self.partial at a later time. You may pass data now and also when invoking the function; data passed now serves as defaults for the object passed later.
# renderPage(req, template, data)
TIP: you probably want self.sendPage
, which loads
data.home
for you and also sends the response to the browser.
This method generates a complete HTML page for transmission to the
browser. Returns HTML markup ready to send (but self.sendPage
is
more convenient).
If template
is a function it is passed a data object,
otherwise it is rendered as a nunjucks template relative
to this module via self.render.
data
is provided to the template, with additional
default properties as described below.
Depending on whether the request is an AJAX request,
outerLayout
is set to:
apostrophe-templates:outerLayout.html
Or:
apostrophe-templates:refreshLayout.html
This allows the template to handle either a content area refresh or a full page render just by doing this:
{% extend outerLayout %}
Note the lack of quotes.
Under the following conditions, refreshLayout.html
is used in place of outerLayout.html
:
req.xhr
is true (always set on AJAX requests by jQuery)
req.query.xhr
is set to simulate an AJAX request
req.decorate
is false
req.query.apos_refresh
is true
These default properties are provided on
the data
object in nunjucks:
data.user
(req.user)
data.query
(req.query)
data.permissions
(req.user._permissions)
data.calls
(javascript markup to insert all global and
request-specific calls pushed by server-side code)
# sendPage(req, template, data)
This method generates and sends a complete HTML page to the browser.
If template
is a function it is passed a data object,
otherwise it is rendered as a nunjucks template relative
to this module via self.render.
data
is provided to the template, with additional
default properties as described below.
outerLayout
is set to:
apostrophe-templates:outerLayout.html
Or:
apostrophe-templates:refreshLayout.html
This allows the template to handle either a content area refresh or a full page render just by doing this:
{% extend outerLayout %}
Note the lack of quotes.
Under the following conditions, refreshLayout.html
is used in place of outerLayout.html
:
req.xhr
is true (always set on AJAX requests by jQuery)
req.query.xhr
is set to simulate an AJAX request
req.decorate
is false
req.query.apos_refresh
is true
These default properties are provided on
the data
object in nunjucks:
data.user
(req.user)
data.query
(req.query)
data.permissions
(req.user._permissions)
data.calls
(javascript markup to insert all global and
request-specific calls pushed by server-side code)
data.home
(basic information about the home page, usually with ._children)
First, pageBeforeSend
is invoked on every module that
has such a method. It receives req
and an optional callback, and
can modify req.data
.
# pushCreateSingleton(req, when)
Push a browser call to instantiate an object with the same
moog type name as this module on the browser side, passing the
given options to the constructor. The singleton is given the
same alias on the apos
object as it has on the server, if any,
otherwise it is still available via apos.modules[full-module-name]
.
This method calls getCreateSingletonOptions(req)
to determine
what options object to pass to the browser-side singleton.
If the req
option is omitted or null, pushBrowserCall
is used;
this is adequate if you don't need different options depending
on the current user. getCreateSingletonOptions()
will also
get an undefined argument in this case.
If req
is given, req.browserCall
is used, and
the singleton is created only once per page lifetime in the browser,
even if an apos.change
event would otherwise cause it to be created again.
If req
is given and when
is not, the singleton is always created; it is assumed that
you are def the request to decide if it is needed before calling.
If when
is given, the singleton is created only for the specified scene
(always
or user
).
If neither is given, the singleton is created only for the user
scene
(a user is logged in, or req.scene
has been set to user
).
This method is not called automatically. Invoke it if your module
actually needs a singleton on the browser side. Defining the moog type
for that singleton is up to you (hint: look at various user.js
files).
# getCreateSingletonOptions(req)
pushCreateSingleton
calls this method to find out what options should
be passed to the singleton it creates on the browser side. These must be
in the form of a JSON-friendly object. By default, the action
property
is passed as the sole option, which is sometimes sufficient.
# defineRelatedType(tool, options)
Define a new moog type related to this module, autoloading its
definition from the appropriate file. This is very helpful when you
want to define another type of object, other than the module itself.
Apostrophe uses this method to define database cursor types related to modules.
The name of the related type will be the name of the module, followed by a
hyphen, followed by the value of tool
. The definition of the type will be
automatically loaded from the lib/tool.js
file of the module
(substitute the actual tool parameter for tool
, i.e. cursor.js
).
This is done recursively for all modules that this module
extends, whether or not they actually have a lib/tool.js
file.
If the file is missing, an empty definition is synthesized that
extends the next "parent class" in the chain.
If any of the types are already defined, execution stops at that
point. For instance, if apostrophe-images
has already called this
as a subclass of apostrophe-pieces
, then apostrophe-files
will
just define its own cursor, extending apostrophe-pieces-cursor
, and stop.
This prevents duplicate definitions when many types extend apostrophe-pieces
.
If options.stop
is set to the name of an ancestor module,
the chain stops after defining the related type for that module.
For instance, the module apostrophe-files
extends
apostrophe-pieces
, which extends apostrophe-doc-type-manager
.
So when that module calls:
self.defineRelatedType('cursor', {
stop: 'apostrophe-doc-type-manager'
});
We get:
apostrophe-files-cursor extends...
apostrophe-pieces-cursor which extends...
apostrophe-doc-type-manager-cursor which extends...
apostrophe-cursor (because cursor.js
for doc-type-manager says so)
# createRelatedType(tool, options, callback)
Create an object of a related type defined by this module.
See defineRelatedType
. A convenient wrapper for calling apos.create
.
For instance, if this module is apostrophe-images
, then
self.createRelatedType('cursor', { options... })
will
create an instance of apostrophe-images-cursor
.
As usual with moog, the callback is required only if
at least one construct
, beforeConstruct
or afterConstruct
function takes a callback.
# getOption(req, dotPathOrArray, def)
A convenience method to fetch properties of self.options
.
req
is required to provide extensibility; modules such as
apostrophe-workflow
and apostrophe-option-overrides
can use it to change the response based on the current page
and other factors tied to the request.
The second argument may be a dotPath, as in:
(req, 'flavors.grape.sweetness')
Or an array, as in:
(req, [ 'flavors', 'grape', 'sweetness' ])
The optional def
argument is returned if the
property, or any of its ancestors, does not exist.
If no third argument is given in this situation,
undefined
is returned.
Also available as a Nunjucks helper; often convenient to invoke
as module.getOption
. When calling the helper,
the req
argument is implied, just pass the path(s).
# logError(req, err)
Log the given error, with informative context based on the given request.
# email(req, templateName, data, options, callback)
Send email. Renders an HTML email message using the template
specified in templateName
, which receives data
as its
data
object (literally called data
in your templates,
just like with page templates).
The nodemailer
option of the apostrophe-email
module
must be configured before this method can be used. That
option's value is passed to Nodemailer's createTransport
method. See the Nodemailer documentation (opens new window).
A plaintext version is automatically generated for email clients that require or prefer it, including plaintext versions of links. So you do not need a separate plaintext template.
nodemailer
is used to deliver the email. The options
object
is passed on to nodemailer
, except that options.html
and
options.plaintext
are automatically provided via the template.
In particular, your options
object should contain
from
, to
and subject
. You can also configure a default
from
address, either globally by setting the from
option
of the apostrophe-email
module, or locally for this particular
module by setting the from
property of the email
option
to this module.
If you need to localize options.subject
, you can call
req.__ns('apostrophe', subject)
.
The callback receives (err, info)
, per the Nodemailer documentation.
With most transports, lack of an err
indicates the message was
handed off but has not necessarily arrived yet and could still
bounce back at some point.
If you do not provide a callback, a promise is returned.
# addTask(name, usage, fn)
Add an Apostrophe command line task to your module. The command line syntax will be:
node app name-of-module:name
Where name
is the name
argument given here (use hyphens).
The usage message is printed if the user asks for help with
the task.
fn
is invoked with (apos, argv, callback)
. You may
return a promise, in which case you must not invoke callback
.
To carry out actions requiring req
in your code, call
self.apos.tasks.getReq
to get a req
with unlimited admin permissions.