Build a blog with Areto Node.js framework

Article model

This class inherited from the Article of the blog's frontend.

Article model

The safe rule applied to the files attribute does not perform any checks. It only means that a value of the form data can be assigned to the attribute.

module/admin/model/Article.js

const Base = require('../../../model/Article');
module.exports = class Article extends Base {
  static getConstants () {
    return {
      ATTRS: [
        'status',
        'authorId',
        'date',
        'title',
        'content',
        'mainPhotoId'
      ],
      RULES: [
        [['title', 'content', 'status', 'date'], 'required'],
        ['title', 'string', {min: 3, max: 128}],
        ['title', 'unique'],
        ['content', 'string', {min: 10, max: 16128}],
        ['date', 'date'],
        ['status', 'range', {range: [
          this.STATUS_DRAFT,
          this.STATUS_PUBLISHED,
          this.STATUS_ARCHIVED,
          this.STATUS_BLOCKED
        ]}],
        ['files', 'safe'],
        ['tags', 'validateTags', {skipOnAnyError: true}]
      ],
      UNLINK_ON_REMOVE: ['photos', 'comments'],
      ATTR_VALUE_LABELS: {
        'status': {
          [this.STATUS_DRAFT]: 'Draft',
          [this.STATUS_PUBLISHED]: 'Published',
          [this.STATUS_ARCHIVED]: 'Archived',
          [this.STATUS_BLOCKED]: 'Blocked'
        }
      }
    };
  }
  // place methods here
};
module.exports.init(module);
const ArrayHelper = require('areto/helper/ArrayHelper');
const Comment = require('./Comment');
const Tag = require('./Tag');
const File = require('./File');
const Photo = require('./Photo');
const User = require('./User');

The findBySearch method searches for articles that contain the specified text in the title.

module/admin/model/Article.js

static findBySearch (text) {
  let query = this.find();
  if (typeof text === 'string' && /[a-z0-9\-\s]{1,32}/i.test(text)) {
    query.and(['LIKE','title', `%${text}%`]);
  }
  return query;
}

The findToSelect method returns a query to select all titles of the articles. The asRaw modifier sets the result as simple JavaScript objects.

module/admin/model/Article.js

static findToSelect () {
  return this.find().select(['title']).asRaw();
}

By default, a new article gets the "draft" status.

module/admin/model/Article.js

constructor (config) {
  super(config);
  this.set('status', this.STATUS_DRAFT);
}

Photo processing

The beforeValidate handler is called before validation of the model. You must call the asynchronous parent method first to event system to work properly.

module/admin/model/Article.js

async beforeValidate () {
  await super.beforeValidate();
  await this.resolveFiles(this.get('files'));
}

The afterSave handler is called after a successful saving. The insert argument contains a flag that defines create or update a model by current action.

module/admin/model/Article.js

async afterSave (insert) {
  await super.afterSave(insert);
  await this.createPhotos(this.get('files'));
}

The resolveFiles asynchronous method finds models of the uploaded files. The identifiers are received in the files serialized array of a form.

module/admin/model/Article.js

async resolveFiles (files) {
  if (files && typeof files === 'string') {
    this.set('files', await File.findById(files.split(',')).all());
    await PromiseHelper.setImmediate();
  }
}

The createPhotos async method creates the Photo models for the uploaded files and links them to the current article. If you do not set the mainPhotoId main article photo, the first of the created models will be assigned as the main.

module/admin/model/Article.js

createPhotos (files) {
  if (!(files instanceof Array)) {
    return false;
  }
  let photos = [];
  for (let file of files) {
    let photo = await this.createPhoto(file);
    if (photo) {
      photos.push(photo);
    }
    await PromiseHelper.setImmediate();
  }
  if (photos.length && this.get('mainPhotoId')) {
    // set first photo as main
    this.set('mainPhotoId', photos[0].getId());
    this.set('files', null);
    await this.forceSave();
  }
}

async createPhoto (file) {
  let photo = new Photo;
  photo.set('articleId', this.getId());
  photo.set('file', file);
  try {
    if (await photo.save()) {
      return photo;
    }
  } catch (err) {
    this.log('error', err);
  }
}

Tag processing

A serialized list of tag ids that are associated with an article is sent form. The validateTags method filters the empty and non-unique values, breaks off the current relations and creates new ones from the received list.

module/admin/model/Article.js

async validateTags (attr, params) {
  let items = this.get(attr);
  if (typeof items !== 'string') {
    return;
  }
  items = items.split(',').map(item => item.trim()).filter(item => item);
  items = ArrayHelper.unique(items);
  await this.unlinkAll('tags');
  for (let item of items) {
    await this.resolveTag(item);
  }
}

The resolveTag method finds or creates a new tag model by name and links it to the current article.

module/admin/model/Article.js

resolveTag (name) {
  let model = await Tag.findByName(name).one();
  if (model) {
    return this.link('tags', model);
  }
  model = new Tag;
  model.set('name', name);
  if (await model.save()) {
    await this.link('tags', model);
  }
}

Article relations

The relAuthor relation defines article's author.

The relPhotos relation defines article's photos.

The relMainPhoto relation defines the main photos of the article.

The relComments relation defines article's comments. The last argument in the hasMany function responsible for deleting comment's model at break of relations with the article.

The relTags relation defines tags related to an article. The rel_article_tag junction table is used to link.

module/admin/model/Article.js

relAuthor () {
  return this.hasOne(User, User.PK, 'authorId');
}

relPhotos () {
  return this.hasMany(Photo, 'articleId', this.PK);
}

relMainPhoto () {
  return this.hasOne(Photo, Photo.PK, 'mainPhotoId');
}

relComments () {
  return this.hasMany(Comment, 'articleId', this.PK)
    .removeOnUnlink();
}

relTags () {
  return this.hasMany(Tag, Tag.PK, 'tagId')
    .viaTable('rel_article_tag', 'articleId', this.PK)
    .removeOnUnlink();
}