apostrophe
Version:
The Apostrophe Content Management System.
1,022 lines (945 loc) • 39.1 kB
JavaScript
// 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 `@apostrophecms/asset`,
// 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.
//
// ## Options
//
// `csrfExceptions` can be set to an array of URLs or route names
// to be excluded from CSRF protection.
//
// `i18n` can be set to an object. If so the project is expected to contain
// translation JSON files in an `i18n` subdirectory. This object
// may have an `ns` property. If so those translations are considered to
// be part of the given namespace, otherwise they are considered to be
// part of the default namespace. npm modules should always declare a
// namespace, and use it via the `:` i18next syntax when localizing their
// phrases. Multiple modules may contribute phrases to the same namespace. If
// the object has a `browser: true` property, then the phrases will also be
// available in the browser for use in the Vue-based admin UI when a user is
// logged in.
const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
const _ = require('lodash');
const fs = require('fs');
module.exports = {
init(self) {
self.apos = self.options.apos;
const capturedSections = [
'queries',
'extendQueries',
'icons'
];
for (const section of capturedSections) {
// Unparsed sections are now captured in __meta, promote
// these to the top level to maintain bc. For new unparsed
// sections we'll leave them in `__meta` to avoid bc breaks
// with project-level properties of the module
self[section] = self.__meta[section];
}
// all apostrophe modules are properties of self.apos.modules.
// Those with an alias are also properties of self.apos
self.apos.modules[self.__meta.name] = self;
if (self.options.alias) {
if (_.has(self.apos, self.options.alias)) {
throw new Error('The module ' + self.__meta.name + ' has an alias, ' + self.options.alias + ', that conflicts with a module registered earlier or a core Apostrophe feature.');
}
self.apos[self.options.alias] = self;
}
self.__helpers = {};
self.templateData = self.options.templateData || {};
self.__structuredLoggingEnabled = false;
if (self.apos.asset) {
if (!self.apos.asset.chains) {
self.apos.asset.chains = {};
}
_.each(self.__meta.chain, function(meta, i) {
self.apos.asset.chains[meta.name] = self.__meta.chain.slice(0, i + 1);
});
}
// The URL for routes relating to this module is based on the
// module name unless they are registered with a leading /.
// self.action is used to implement this
self.enableAction();
// Routes in their final ready-to-add-to-Express form
self._routes = [];
// Enable structured logging after util module is initialized.
if (self.apos.util && (self.apos.util !== self)) {
self.__structuredLoggingEnabled = true;
}
// Add i18next phrases if we started up after the i18n module,
// which will call this for us if we start up before it
if (self.apos.i18n && (self.apos.i18n !== self)) {
self.apos.i18n.addResourcesForModule(self);
}
},
async afterAllSections(self) {
self.addHelpers(self.helpers || {});
self.addHandlers(self.handlers || {});
await self.executeAfterModuleInitTask();
},
methods(self) {
return {
// `self.logInfo`, `self.logError`, etc. available for every module except
// `error`, `util` and the `log` module itself.
...require('./lib/log')(self),
compileSectionRoutes(section) {
_.each(self[section] || {}, function(routes, method) {
_.each(routes, function(config, name) {
let route;
if (((typeof config) === 'object') && (!Array.isArray(config))) {
// Route with extra config like `before`,
// get to the actual route function
route = config.route;
} else {
route = config;
}
// TODO we must set up this array based on the new route middleware
// section at some point
const url = self.getRouteUrl(name);
if (Array.isArray(route)) {
let routeFn = route[route.length - 1];
if (self.routeWrappers[section]) {
routeFn = self.routeWrappers[section](name, routeFn);
route[route.length - 1] = routeFn;
}
self._routes.push({
before: config.before,
method,
url,
route: (req, res) => {
// Invoke the middleware functions, then the route function,
// which is on the same array. This is an async for loop.
// We use async/await nearly everywhere but the Express-style
// middleware pattern doesn't call for it
let i = 0;
next();
function next() {
route[i++](req, res, (i < route.length) ? next : null);
}
}
});
} else {
if (self.routeWrappers[section]) {
route = self.routeWrappers[section](name, route);
}
self._routes.push({
before: config.before,
method,
url,
route
});
}
});
});
},
compileRestApiRoutesToApiRoutes() {
if (self.restApiRoutes.getAll) {
self.apiRoutes.get = self.apiRoutes.get || {};
self.apiRoutes.get[''] = self.restApiRoutes.getAll;
}
if (self.restApiRoutes.getOne) {
self.apiRoutes.get = self.apiRoutes.get || {};
self.apiRoutes.get[':_id'] = wrapId(self.restApiRoutes.getOne);
}
if (self.restApiRoutes.delete) {
self.apiRoutes.delete = self.apiRoutes.delete || {};
self.apiRoutes.delete[':_id'] = wrapId(self.restApiRoutes.delete);
}
if (self.restApiRoutes.patch) {
self.apiRoutes.patch = self.apiRoutes.patch || {};
self.apiRoutes.patch[':_id'] = wrapId(self.restApiRoutes.patch);
}
if (self.restApiRoutes.put) {
self.apiRoutes.put = self.apiRoutes.put || {};
self.apiRoutes.put[':_id'] = wrapId(self.restApiRoutes.put);
}
if (self.restApiRoutes.post) {
self.apiRoutes.post = self.apiRoutes.post || {};
self.apiRoutes.post[''] = self.restApiRoutes.post;
}
function wrapId(route) {
if (Array.isArray(route)) {
// Allow middleware, last fn is route
return route
.slice(0, route.length - 1)
.concat([ wrapId(route[route.length - 1]) ]);
}
return async req => route(req, req.params._id);
}
},
routeWrappers: {
apiRoutes(name, fn) {
return async function(req, res) {
try {
const result = await fn(req);
if (req.method === 'GET' && req.user) {
res.header('Cache-Control', 'no-store');
}
res.status(200);
res.send(result);
} catch (err) {
return self.routeSendError(req, err);
}
};
},
renderRoutes(name, fn) {
return async function(req, res) {
try {
const result = await fn(req);
const markup = await self.render(req, name, result);
return res.send(markup);
} catch (err) {
return self.routeSendError(req, err);
}
};
}
// There is no htmlRoute because in 3.x, even data-oriented apiRoutes
// use standard status codes and respond simply without a wrapper
// object. So they are suited for both markup fragments and JSON data.
},
// Part of the implementation of `apiRoutes` and `renderRoutes`, this
// method is also handy if you wish to send an error the way `apiRoute`
// would upon catching an exception in middleware, etc.
routeSendError(req, err) {
if (!(req && req.res)) {
self.apos.util.error('Looks like you did not pass req to self.routeSendError, you should not have to call this method yourself,\nit is usually called for you by self.apiRoute, self.htmlRoute or self.renderRoute', (new Error()).stack);
return;
}
if (Array.isArray(err)) {
err = self.apos.error('invalid', {
errors: err.map(err => {
const response = getResponse(err);
return {
name: response.name,
message: response.message,
path: err.path,
code: response.code,
data: response.data
};
})
});
}
const response = getResponse(err);
logError(req, response, err);
req.res.status(response.code);
return req.res.send({
name: response.name,
data: response.data,
message: response.message
});
function logError(req, response, error) {
const typeTrail = response.code === 500 ? '' : `-${response.name}`;
// Log the actual error, not the message meant for the browser.
const msg = response.code === 500
? err.message
: response.message;
try {
self.logError(req, `api-error${typeTrail}`, msg, {
name: response.name,
status: response.code,
stack: (error.stack || '').split('\n').slice(1).map(line => line.trim()),
cause: error.cause,
errorPath: response.path,
data: response.data
});
} catch (e) {
// We can't afford to throw here, it would hang the response.
e.message = 'Structured logging error: ' + e.message;
console.error(e);
}
}
function getResponse(err) {
let name, data, code, fn, message, path;
if (err && err.name && self.apos.http.errors[err.name]) {
data = err.data || {};
code = self.apos.http.errors[err.name];
fn = self.apos.util.info;
name = err.name;
message = err.message;
path = err.path;
} else {
code = 500;
fn = self.apos.util.error;
name = 'error';
data = {};
message = 'An error occurred.';
path = err.path;
}
if ((name === 'invalid') && Array.isArray(data.errors)) {
// Sub-errors must get the same cleansing treatment
// before sending to the browser
data.errors = data.errors.map(error => {
const response = getResponse(error);
return {
// Omitting fn
name: response.name,
code: response.code,
message: response.message,
data: response.data,
path: response.path
};
});
}
return {
name,
data,
code,
path,
fn,
message
};
}
},
// Automatically called for you to add the helpers in the "helpers"
// section of your module.
addHelpers(object) {
Object.assign(self.__helpers, object);
},
// Automatically called for you to add the event handlers in the
// "handlers" section of your module.
addHandlers(object) {
Object.keys(object).forEach(eventName => {
Object.keys(object[eventName]).forEach(handlerName => {
self.on(eventName, handlerName, object[eventName][handlerName]);
});
});
},
// Prepepnd/append nodes, rendered to HTML, to a given location. Supports
// the same locations as `apos.template.prepend()` and `apos.template.append()`.
// The rendered markup is automatically escaped and injected into the
// appropriate location in the HTML document.
// The `method` argument is the name of existing method in the current module.
// It should be string, passing a function will throw an error.
// Example:
// ```
// self.prependNodes('head', 'myMethod');
// self.appendNodes('body', 'anotherMethod');
// ```
// In the example above, `myMethod` and `anotherMethod` should be defined
// in the current module, and they should return an array of node objects.
// ```
// methods(self) {
// return {
// myMethod(req) {
// return [
// {
// name: 'meta',
// attributes: {
// name: 'my-meta',
// content: 'my content'
// }
// }
// ];
// },
// anotherMethod(req) {
// return [
// {
// tag: 'h4',
// body: [
// {
// comment: 'Start Heading text'
// },
// {
// text: 'Heading text'
// }
// {
// comment: 'End Heading text'
// }
// {
// name: 'script`,
// body: [
// {
// raw: 'console.log("This is not escaped, be careful!");'
// }
// ]
// }
// ]
// }
// ];
// }
// };
// }
// ```
// Node object SHOULD have either `name`, `text`, `raw` or `comment` property.
// A node with `name` can have `attrs` (array of element attributes)
// and `body` (array of child nodes, recursion).
// `text` nodes are rendered as text (no HTML tags), the value is always a string.
// `comment` nodes are rendered as HTML comments, the value is always a string.
// `raw` nodes are rendered as is, no escaping, the value is always a string.
prependNodes(location, method) {
return self.apos.template
.prependNodes(location, self.__meta.name, method);
},
appendNodes(location, method) {
return self.apos.template
.appendNodes(location, self.__meta.name, method);
},
// Render a template. Template overrides are respected; the
// project level 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.
//
// 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` is
// provided for convenience.
//
// The data argument may be omitted.
//
// This method is `async` in 3.x and must be awaited.
async render(req, name, data) {
if (!(req && req.res)) {
throw new Error('The first argument to self.render must be req.');
}
if (!data) {
data = {};
}
return self.apos.template.renderForModule(req, name, data, self);
},
// Similar to `render`, however this method then sends the
// rendered content as a response to the request. You may
// await this function, but since the response has already been
// sent you typically will not have any further work to do.
async send(req, name, data) {
return req.res.send(await self.render(req, name, data));
},
// Render a template in a string (not from a file), looking for
// includes, etc. in our preferred places.
//
// Otherwise the same as `render`.
async renderString(req, s, data) {
if (!data) {
data = {};
}
return self.apos.template.renderStringForModule(req, s, data, self);
},
// TIP: more often you will want `self.sendPage`, which 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).
//
// `template` is a nunjucks template name, relative
// to this module's views/ folder.
//
// `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:
//
// `@apostrophecms/template:outerLayout.html`
//
// Or:
//
// `@apostrophecms/template: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.
//
// If `req.query.aposRefresh` is `'1'`,
// `refreshLayout.html` is used in place of `outerLayout.html`.
//
// These default properties are provided on
// the `data` object in nunjucks:
//
// `data.user` (req.user)
// `data.query` (req.query)
//
// This method is async in 3.x and must be awaited.
//
// If the external front feature is in use for the request, then
// self.apos.template.annotateDataForExternalFront and
// self.apos.template.pruneDataForExternalFront are called
// and the data is returned, in place of normal Nunjucks rendering.
//
// No longer deprecated because it is a useful override point
// for this part of the behavior of sendPage.
async renderPage(req, template, data) {
await self.apos.page.emit('beforeSend', req);
await self.apos.area.loadDeferredWidgets(req);
if (req.aposExternalFront) {
// Use the correct scene downstream
if (!req.scene && req.user) {
req.scene = 'apos';
}
data = self.apos.template.getRenderDataArgs(req, data, self);
await self.apos.template.annotateDataForExternalFront(
req,
template,
data,
self.__meta.name
);
self.apos.template.pruneDataForExternalFront(
req,
template,
data,
self.__meta.name
);
// Reply with JSON
return data;
}
return self.apos.template.renderPageForModule(req, template, data, self);
},
// This method generates and sends a complete HTML page to the browser.
//
// `template` is a nunjucks template name, relative
// to this module's views/ folder.
//
// `data` is provided to the template, with additional
// default properties as described below.
//
// `outerLayout` is set to:
//
// `@apostrophecms/template:outerLayout.html`
//
// Or:
//
// `@apostrophecms/template: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.
//
// If `req.query.aposRefresh` is `'1'`,
// `refreshLayout.html` is used in place of `outerLayout.html`.
//
// These default properties are provided on
// the `data` object in nunjucks:
//
// `data.user` (req.user)
// `data.query` (req.query)
// `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, the `@apostrophecms/page` module emits a `beforeSend` event.
// Handlers receive `req`, allowing them to modify `req.data`, set
// `req.redirect` to a URL, set `req.statusCode`, etc.
//
// This method is async and may be awaited although you should bear
// in mind that a response has already been sent to the browser at
// that point.
async sendPage(req, template, data) {
const telemetry = self.apos.telemetry;
const spanName = `${self.__meta.name}:sendPage`;
await telemetry.startActiveSpan(spanName, async (span) => {
span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'sendPage');
span.setAttribute(SemanticAttributes.CODE_NAMESPACE, self.__meta.name);
span.setAttribute(telemetry.Attributes.TEMPLATE, template);
try {
const result = await self.renderPage(req, template, data);
req.res.send(result);
span.setStatus({ code: telemetry.api.SpanStatusCode.OK });
} catch (err) {
telemetry.handleError(span, err);
throw err;
} finally {
span.end();
}
});
},
// A cookie in session doesn't mean we can't cache, nor an empty flash or
// passport object. Other session properties must be assumed to be
// specific to the user, with a possible impact on the response, and thus
// mean this request must not be cached. Same rule as in [express-cache-on-demand](https://github.com/apostrophecms/express-cache-on-demand/blob/master/index.js#L102)
isSafeToCache(req) {
if (req.user) {
return false;
}
if (req.res.statusCode >= 400) {
// Don't cache errors
return false;
}
return Object.entries(req.session).every(([ key, val ]) =>
key === 'cookie' || (
(key === 'flash' || key === 'passport') && _.isEmpty(val)
)
);
},
setMaxAge(req, maxAge) {
if (typeof maxAge !== 'number') {
self.apos.util.warnDev(`"maxAge" property must be defined as a number in the "${self.__meta.name}" module's cache options"`);
return;
}
const cacheControlValue = self.isSafeToCache(req) ? `max-age=${maxAge}` : 'no-store';
req.res.header('Cache-Control', cacheControlValue);
},
generateETagParts(req, doc) {
const context = doc || req.data.piece || req.data.page;
if (!context || !context.cacheInvalidatedAt) {
return null;
}
const releaseId = self.apos.asset.getReleaseId();
const cacheInvalidatedAtTimestamp = (new Date(context.cacheInvalidatedAt))
.getTime()
.toString();
return [ releaseId, cacheInvalidatedAtTimestamp ];
},
setETag(req, eTagParts) {
req.res.header('ETag', eTagParts.join(':'));
},
checkETag(req, doc, maxAge) {
const eTagParts = self.generateETagParts(req, doc);
if (!eTagParts || !self.isSafeToCache(req)) {
return false;
}
const clientETagParts = req.headers['if-none-match'] ? req.headers['if-none-match'].split(':') : [];
const doesETagMatch = clientETagParts[0] === eTagParts[0] &&
clientETagParts[1] === eTagParts[1];
const now = Date.now();
const clientETagAge = (now - clientETagParts[2]) / 1000;
if (!doesETagMatch || clientETagAge > maxAge) {
self.setETag(req, [ ...eTagParts, now ]);
return false;
}
self.setETag(req, clientETagParts);
return true;
},
// Call from init once if this module implements the `getBrowserData`
// method. The data returned by `getBrowserData(req)` will then be
// available on `apos.modules['your-module-name']` in the browser.
//
// By default browser data is pushed only for the `apos` scene, so public
// site pages will not be cluttered with it, except on the /login page and
// other pages that opt into the `apos` scene. If `scene` is set to
// `public` then the data is available all the time.
//
// Be sure to use `extendMethods` when implementing `getBrowserData`
// as your base class may also implement `getBrowserData`.
enableBrowserData(scene = 'apos') {
self.enabledBrowserData = scene;
},
// Extend this method to return the appropriate browser data for
// your module. If you want browser data for the given req, return
// an object. That object is assigned to
// `apos.modules['your-module-name']` in the browser. Do not return huge
// data structures, as this will impact page load time and performance.
//
// If your module has an alias the data will also be accessible
// via `apos.yourAlias`.
//
// Modules derived from pieces, etc. already implement this method,
// so be sure to follow the super pattern if you want to add additional
// data.
//
// For performance, this method will only be invoked if
// `enableBrowserData` was called. See also `enableBrowserData` for more
// restrictions on when this method is called; if you want data for
// anonymous site visitors you must explicitly opt in.
getBrowserData(req) {
return {};
},
// Transform a route name into a route URL. If the name begins with `/`
// it is understood to already be a site-relative URL. Otherwise, if it
// contains : or *, it is considered to be module-relative but still
// already in URL format because the developer will have used a "/" to
// separate these parameters from the route name. If neither of these
// rules applies, the name is converted to "kebab case" and then treated
// as a module-relative URL, allowing method syntax to be used for most
// routes.
getRouteUrl(name) {
let url;
if (name.substring(0, 1) === '/') {
// Specifying our own site-relative URL
url = name;
} else {
if (name.match(/[:/*]/)) {
// If the name contains placeholder parameters or slashes, it is
// still "module-relative" but transforming to kebab case
// is not appropriate as the developer is already naming it
// with a URL in mind
url = self.action + '/' + name;
} else {
// Map from linter-friendly method names like `saveArea` to
// URL-friendly names like `save-area`
url = self.action + '/' + self.apos.util.cssName(name);
}
}
return url;
},
// A convenience method to fetch properties of `self.options`.
//
// `req` is required to provide extensibility; modules such as
// `@apostrophecms/workflow` and `@apostrophecms/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.
//
// In templates, `getOption` is a global function, not
// a property of each module. If you call it with an ordinary
// key, the option is located in the module that called
// render(). If you call it with a cross-module key,
// like `module-name:optionName`, the option is located
// in the specified module. You do not have to pass `req`.
getOption(req, dotPathOrArray, def) {
if ((!req) || (!req.res)) {
throw new Error('Looks like you forgot to pass req to the getOption method');
}
return _.get(self.options, dotPathOrArray, def);
},
// If `name` is `manager` and `options.components.manager` is set,
// return that string, otherwise return `def`. This is used to decide what
// Vue component to instantiate on the browser side.
getComponentName(name, def) {
return _.get(self.options, `components.${name}`, def);
},
// 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 `@apostrophecms/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](https://nodemailer.com).
//
// 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 `@apostrophecms/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.t(subject)`.
//
// This method returns `info`, per the Nodemailer documentation.
// With most transports, a successful return indicates the message was
// handed off but has not necessarily arrived yet and could still
// bounce back at some point.
async email(req, templateName, data, options) {
return self.apos.modules['@apostrophecms/email'].emailForModule(req, templateName, data, options, self);
},
// Given a Vue component name, such as AposDocsManager,
// return that name unless `options.components[name]` has been set to
// an alternate name. Overriding keys in the `components` option
// allows modules to provide alternative functionality for standard
// components while maintaining readable Vue code via the
// <component :is="..."> syntax.
getVueComponentName(name) {
return (self.options.components && self.options.components[name]) || name;
},
// When a CMS page is rendered, it will render the
// template name passed on the last call to this
// method during the processing of the request.
// This facilitates the implementation of separate
// templates for separate dispatch routes.
setTemplate(req, name) {
req.template = `${self.__meta.name}:${name}`;
},
// Sets `self.action` which is the base URL for all APIs of
// this module
enableAction() {
self.action = `/api/v1/${self.__meta.name}`;
},
async executeAfterModuleInitTask() {
return self.executeAfterModuleTask('afterModuleInit');
},
async executeAfterModuleTask(when) {
for (const [ name, info ] of Object.entries(self.tasks || {})) {
if (info[when]) {
// Execute a task like @apostrophecms/asset:build or
// @apostrophecms/db:reset which
// must run before most modules are awake
if (self.apos.argv._[0] === `${self.__meta.name}:${name}`) {
const telemetry = self.apos.telemetry;
const spanName = `task:${self.__meta.name}:${name}`;
// only this span can be sent to the backend, attach to the ROOT
await telemetry.startActiveSpan(
spanName,
async (span) => {
span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'executeAfterModuleInitTask');
span.setAttribute(SemanticAttributes.CODE_NAMESPACE, '@apostrophecms/module');
span.setAttribute(
telemetry.Attributes.TARGET_NAMESPACE, self.__meta.name
);
span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, name);
try {
await info.task(self.apos.argv);
span.setStatus({ code: telemetry.api.SpanStatusCode.OK });
} catch (err) {
telemetry.handleError(span, err);
throw err;
} finally {
span.end();
}
});
// In most cases we exit after running a task
if (info.exitAfter !== false) {
await self.apos._exit();
} else {
// Provision for @apostrophecms/db:reset which should be
// followed by normal initialization so all the collections
// and indexes are recreated as they would be on a first run
// Avoid double execution
self.apos.taskRan = true;
}
}
}
}
},
isShareDraftRequest(req) {
const { aposShareId, aposShareKey } = req.query;
return Boolean(
typeof aposShareId === 'string' &&
aposShareId.length &&
typeof aposShareKey === 'string' &&
aposShareKey.length
);
},
// Given a name such as "placeholder", look at the
// relevant options (nameImage and, as a fallback, nameUrl)
// and determine the appropriate asset URL. If nameImage is used,
// search the inheritance chain of the module
// for the best match, e.g. a file in a project-level override
// wins, followed by the original module in core or npm,
// followed by something in a base class that extends, etc.
// If no file is found, the method returns `undefined`.
//
// Even if `nameUrl is used, the method still corrects paths
// beginning with `/module` to account for the actual asset
// release URL (`nameUrl` is really an asset path, but for bc
// this is the naming pattern).
//
// In the above examples "name" should be replaced with the
// actual value of the name argument.
determineBestAssetUrl(name) {
let urlOption = self.options[`${name}Url`];
const imageOption = self.options[`${name}Image`];
if (!urlOption) {
// Webpack and the legacy asset pipeline
if (imageOption && !self.apos.asset.hasBuildModule()) {
const chain = [ ...self.__meta.chain ].reverse();
for (const entry of chain) {
const path = `${entry.dirname}/public/${name}.${imageOption}`;
if (fs.existsSync(path)) {
urlOption = `/modules/${entry.name}/${name}.${imageOption}`;
break;
}
}
}
// The new external module asset pipeline
if (imageOption && self.apos.asset.hasBuildModule()) {
urlOption = `/modules/${self.__meta.name}/${name}.${imageOption}`;
}
}
if (urlOption && urlOption.startsWith('/modules')) {
urlOption = self.apos.asset.url(urlOption);
}
if (urlOption) {
self.options[`${name}Url`] = urlOption;
}
},
// Modules that have REST APIs use this method
// to determine if a request is qualified to access
// it without restriction to the `publicApiProjection`
canAccessApi(req) {
if (self.options.guestApiAccess) {
return !!req.user;
} else {
return self.apos.permission.can(req, 'view-draft');
}
},
// Merge in the event emitter / responder capabilities
...require('./lib/events.js')(self)
};
},
handlers(self) {
return {
moduleReady: {
executeAfterModuleReadyTask() {
return self.executeAfterModuleTask('afterModuleReady');
}
},
'apostrophe:modulesRegistered': {
addHelpers() {
// We check this just to allow init in bootstrap tests that
// have no templates module
if (self.apos.template) {
self.apos.template.addHelpersForModule(self, self.__helpers);
}
}
},
'@apostrophecms/express:compileRoutes': {
compileAllRoutes() {
// Sections like `routes` don't populate `self.routes` until after
// init resolves. Call methods that bring those sections fully to life
// here
self.compileRestApiRoutesToApiRoutes();
self.compileSectionRoutes('routes');
self.compileSectionRoutes('renderRoutes');
// Put the api routes last so the REST api routes
// they contain, with their wildcards, don't
// block ordinary apiRoute names by matching them
// as _ids
self.compileSectionRoutes('apiRoutes');
}
},
...self.enabledBrowserData && {
'@apostrophecms/template:addBodyData': {
addBrowserDataToBody(req, data) {
let myData;
if (self.enabledBrowserData === 'apos') {
if (req.scene === 'apos') {
// apos scene only
myData = self.getBrowserData(req);
}
} else {
// All the time
myData = self.getBrowserData(req);
}
if (!myData) {
return;
}
data.modules[self.__meta.name] = myData;
if (self.options.alias) {
data.modules[self.__meta.name].alias = self.options.alias;
}
}
}
}
};
}
};