Article model

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

Article model

The safe rule applied to a files attribute does not perform any checks. It only means that a value of 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',
        'category',
        'date',
        'title',
        'content',
        'mainPhotoId',
        'createdAt',
        'updatedAt'
      ],
      RULES: [
        [['title', 'content', 'status', 'date'], 'required'],
        ['title', 'string', {min: 3, max: 128}],
        ['title', 'unique'],
        ['content', 'string', {min: 10, max: 16128}],
        ['date', 'date'],
        ['category', 'id'],
        ['status', 'range', {range: [
          this.STATUS_DRAFT,
          this.STATUS_PUBLISHED,
          this.STATUS_ARCHIVED,
          this.STATUS_BLOCKED
        ]}],
        ['status', 'default', {value: this.STATUS_DRAFT}],
        ['files', 'safe'],
        ['tags', 'validateTags', {skipOnAnyError: true}]
      ],
      BEHAVIORS: {
        'timestamp': require('areto/behavior/TimestampBehavior')
      },
      DELETE_ON_UNLINK: [
        'comments',
        'photos'
      ],
      UNLINK_ON_DELETE: [
        'tags'
      ],
      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 a specified text in title.

module/admin/model/Article.js

findBySearch (text) {
  const 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 articles. The asRaw modifier sets the result as simple JavaScript objects.

module/admin/model/Article.js

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 a 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'));
}

Asynchronous method resolveFiles finds models of uploaded files. Identifiers are received in a files serialized array of 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 method creates Photo models for uploaded files and links them to the current article. If you do not set a main article photo (mainPhotoId), the first of created models will be assigned as the main.

module/admin/model/Article.js

createPhotos (files) {
  if (!(files instanceof Array)) {
    return false;
  }
  const photos = [];
  for (const file of files) {
    const 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) {
  const photo = this.spawn(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. Method validateTags filters the empty and non-unique values, breaks off the current relations and creates new ones from 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 (const 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 this.spawn(Tag).findByName(name).one();
  if (model) {
    return this.link('tags', model);
  }
  model = this.spawn(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 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 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);
}

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