# Services

Complex controllers that contain a lot of domain logic is a classic anti-pattern that can lead to duplicated code and difficult-to-read controllers, among other issues. However, Nodewood's Models lack database access, meaning the tradition ActiveRecord pattern of performing all your saving/loading on them won't work. This is where Services come into play.

A Service contains a collection of actions commonly taken in that domain. In the case of the "users" feature, users can be created, logged in, updated, etc. These are actions in the UsersService that are called from the UsersController and return one or many UserModels.

Multiple services can be used per Controller, as necessary. A Controller may need to deal with both Users and Comments, and might call a CommentsService action to save a comment and a UsersService action to update the total number of comments made by a user.

# Database

Nodewood makes use of the MassiveJS database library, as it fits the service pattern perfectly - you build queries on tables that return objects which are parsed into Nodewood models when they are passed into the constructor. However Nodewood services include a few convenience methods and members to make working with MassiveJs a bit easier.

# db

This is the MassiveJs instance that you would use to execute queries against: const users = await this.db.users.findOne(1);.

# model

This is the model class this service returns from its database functions.

# table()

This is a shortcut to the correct table to use for the current Service. This allows you to start all database calls the same: const users = await this.table().findOne(1);. Further, if you are in a transaction, you can pass the current transaction to table(tx) to ensure that the call happens inside the transaction.

This abstraction helps power the other service helpers.

# find()

Finds one model by its id field.

# findBy()

Finds a collection of models by a simple query. Accepts pagination parameters page and per, as well as any other options values supported by MassiveJS.

# count()

Counts the total collection of models in the database for a query. Used in conjunction with findBy() to identify the total number of pages when listing.

# update()

Updates a model as defined by an ID with a collection of values. An array of keys are passed to be updated are passed in as well, so that a form's body can be used for the values parameter, while keys can be a predetermined list of fields you're willing to accept. This allows you to write simple code that also prevents attackers from attempting to update fields they're not allowed to (like account_type, etc):

const { ADMIN_USER_EDIT_FORM_FIELDS, } = require('#features/users/lib/validators/UserValidator');

await this.usersService.update(tx, req.params.id, req.body, ADMIN_USER_EDIT_FORM_FIELDS);

# delete()

Deletes an instance by ID.

# Transactions

If you examine the source code for the main Service class (located at wood/api/src/Service.js), you'll notice the first parameter for all these function is tx, which means you can wrap these calls in a transaction in case you need to call multiple service actions to achieve your controller's ends - especially useful when getting the count for a pagination query:

await this.withTransaction(async (tx) => {
  const {
    page = DEFAULT_PAGE,
    per = DEFAULT_PER,
    search = '',
  } = req.query;

  const pages = Math.ceil((await this.usersService.count(tx, {})) / per);
  const users = await this.usersService.getSearchPage(tx, req.user, search, Math.min(page, pages), per);
});

Any errors that are thrown inside a withTranaction() call will be caught and handled as normal, but will cause any database calls that have been initiated with the provided tx object to be rolled back safely.

If you don't wish to have a service action happen inside a transaction (for example, to update a "failed" count for a user before throwing an error that will roll back all transaction-based queries), you can pass null for the tx parameter, and the query will be executed outside the transaction.

# Email

Nodewood uses Nodemailer to send transactional emails. Nodemailer is very versatile and allows you to use simple SMTP to send email or more exotic transports.

It is not recommended to use Nodewood to send campaign (i.e. mailing list) emails. A great amount of work and care go into ensuring deliverability for campaign emails, and you are far, far better off engaging the services of a dedicated campaign mailer than trying to replicate them on your own. In addition, you do not want to mix the IP addresses you use for sending transactional mail and campaign mail, in case a over-zealous ISP adds your campaign mail IP address to a blacklist. You would be unable to send transactional mail to those customers as well, which could include payment alerts or other critical and time-sensitive notifications.

# Configuration

Your email transport (i.e. the connection you use to send email) is configured in #config/email. A common "alternative" mailer is Amazon's SES, which you could configure to use as follows (once finished setting up AWS on your VM in the usual way:

module.exports = {
  transportConfig: {
    SES: new aws.SES({ apiVersion: '2010-12-01' }),
  },
};

Consult Nodemailer's documentation for how to set up other mail transports.

# Sending

#lib/Email provides a sendMail() function that allows you to send mail. The first parameter to this function is the mailer object configured above and passed into services via the constructor. The second parameter is a configuration object used to configure who to send to, who to send from, add a message subject, body, etc.

You can either pass an html key to this configuration object and use that HTML directly, or you can pass a template key containing the full path to an EJS template to use and a data key containing data to substitute into the template. Typically templates are stored in a feature's api/emails folder.

You can always pass a text key to this configuration object to define a text-only email body to display to email readers that don't display HTML.

await sendMail(this.mailer, {
  from: `${appName} <${appEmail}>`,
  to: user.email,
  subject: `${appName}: Your weekly results`,
  template: resolve(__dirname, '../emails/WeeklyResults.ejs'),
  data: {
    total: results.total,
    userName: user.name,
    appName,
    appUrl,
  },
});

# Adding a new service

To add a new service with some sample code to get you rolling, run:

nodewood add service FEATURE NAME

Replace FEATURE with the name of the feature you wish to add a service for, and NAME with the name of the service you wish to create.