@azteam/mongo-model
Version:
N/A
516 lines (467 loc) • 14.5 kB
JavaScript
import {minify} from 'html-minifier';
import _ from 'lodash';
import mongoose, {Schema} from 'mongoose';
import mongooseLeanVirtuals from 'mongoose-lean-virtuals';
import mongoosePaginate from 'mongoose-paginate-v2';
import sanitizeHtml from 'sanitize-html';
import {AVAILABLE_STATUS} from '@azteam/constant';
import {omitItem, toSlug} from '@azteam/util';
import validator, {
schemaArray,
schemaBoolean,
schemaEnum,
schemaJSON,
schemaNumber,
schemaObject,
schemaSlug,
schemaString,
schemaUrl,
} from '@azteam/validator';
const DataTypes = {
BOOLEAN: Boolean,
ID: mongoose.Types.ObjectId,
OBJECT: mongoose.Mixed,
STRING: String,
NUMBER: Number,
ARRAY: Array,
};
function validationRuleObjectId(optional = false) {
return {
type: 'objectID',
ObjectID: DataTypes.ID,
optional,
};
}
const DefaultAttributes = {
SLUG: {
slug: {
type: DataTypes.STRING,
unique: true,
guard: true,
allowCreate: true,
validationRule: schemaSlug(true),
},
related_slugs: {
type: DataTypes.ARRAY,
index: true,
default: [],
validationRule: schemaArray(schemaSlug(), true),
},
},
TRASH: {
deleted_at: {
type: DataTypes.NUMBER,
default: 0,
validationRule: schemaNumber(true),
guard: true,
},
deleted_id: {
type: DataTypes.ID,
validationRule: validationRuleObjectId(true),
guard: true,
},
},
META_DATA: {
indexing_domains: {
type: DataTypes.ARRAY,
default: [],
validationRule: schemaArray(
schemaObject({
domain: schemaString(),
indexed: schemaBoolean(),
}),
true
),
},
metadata_disable: {
type: DataTypes.NUMBER,
default: 0,
validationRule: schemaBoolean(true),
},
metadata_title: {
type: DataTypes.STRING,
validationRule: schemaString(0, 1000, true),
},
metadata_description: {
type: DataTypes.STRING,
validationRule: schemaString(0, 3000, true),
},
metadata_keywords: {
type: DataTypes.STRING,
validationRule: schemaString(0, 1000, true),
},
metadata_image_url: {
type: DataTypes.STRING,
validationRule: schemaUrl(true),
},
metadata_title_og: {
type: DataTypes.STRING,
validationRule: schemaString(0, 1000, true),
},
metadata_description_og: {
type: DataTypes.STRING,
validationRule: schemaString(0, 3000, true),
},
ld_json: {
type: DataTypes.STRING,
default: '{}',
validationRule: schemaJSON(true),
},
},
MODIFY: {
created_at: {
type: DataTypes.NUMBER,
default: 0,
validationRule: schemaNumber(true),
guard: true,
index: true,
},
created_id: {
type: DataTypes.ID,
validationRule: validationRuleObjectId(true),
guard: true,
},
modified_at: {
type: DataTypes.NUMBER,
default: 0,
validationRule: schemaNumber(true),
guard: true,
index: true,
},
modified_id: {
type: DataTypes.ID,
validationRule: validationRuleObjectId(true),
guard: true,
},
},
};
function sanitize(content) {
const sanitizeContent = sanitizeHtml(content, {
allowedTags: [
'img',
'span',
'address',
'article',
'aside',
'footer',
'header',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'nav',
'section',
'blockquote',
'figcaption',
'figure',
'hr',
'li',
'main',
'ol',
'p',
'pre',
'ul',
'a',
'b',
'br',
'code',
'em',
'i',
'kbd',
'mark',
'q',
's',
'small',
'span',
'strong',
'sub',
'sup',
'time',
'u',
'wbr',
'caption',
'col',
'colgroup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'video',
'iframe',
'svg',
'summary',
'details',
],
allowedAttributes: {
a: ['href', 'name', 'target', 'rel'],
img: ['src', 'alt', 'title'],
iframe: ['src'],
'*': ['style', 'itemscope', 'itemtype', 'itemprop'],
details: ['open'],
},
allowedStyles: {
'*': {
// Match HEX and RGB
// eslint-disable-next-line
color: [/^\#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/],
'text-align': [/^left$/, /^right$/, /^center$/, /^justify$/],
// Match any number with px, em, or %
'font-size': [/^\d+(?:px|em|%)$/],
'font-style': [/^.*$/],
'font-weight': [/^bold$/],
'font-family': [/^.*$/],
'max-width': [/^\d+(?:px|em|%)$/],
'max-height': [/^\d+(?:px|em|%)$/],
padding: [/^\d+(?:px|em|%)$/],
'padding-left': [/^\d+(?:px|em|%)$/],
'padding-right': [/^\d+(?:px|em|%)$/],
'padding-top': [/^\d+(?:px|em|%)$/],
'padding-bottom': [/^\d+(?:px|em|%)$/],
margin: [/^\d+(?:px|em|%)$/],
'margin-top': [/^\d+(?:px|em|%)$/],
'margin-left': [/^\d+(?:px|em|%)$/],
'margin-right': [/^\d+(?:px|em|%)$/],
'margin-bottom': [/^\d+(?:px|em|%)$/],
'border-radius': [/^\d+(?:px|em|%)$/],
'vertical-align': [
/^length$/,
/^%$/,
/^sub/,
/^super$/,
/^top$/,
/^text-top$/,
/^middle/,
/^bottom$/,
/^text-bottom$/,
/^initial$/,
/^inherit$/,
],
colspan: [/^\d+/],
},
},
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
allowedSchemesAppliedToAttributes: ['href', 'src'],
allowedIframeHostnames: ['www.youtube.com'],
});
return minify(sanitizeContent.trim(), {
collapseWhitespace: true,
});
}
function createSchema(Model, colAttributes, isTrash) {
const validationRules = {};
const newColAttributes = _.cloneDeep(colAttributes);
const newIndexes = _.cloneDeep(Model.indexes);
const decimals = [];
const guardLoadData = [];
const allowCreateLoadData = [];
_.map(newColAttributes, (col, key) => {
if (isTrash) {
delete col.index;
delete col.unique;
delete col.text;
}
if (col.type) {
if (col.type.name === 'String') {
col.trim = true;
}
}
if (col.validationRule) {
validationRules[key] = col.validationRule;
delete col.validationRule;
}
if (col.guard) {
guardLoadData.push(key);
delete col.guard;
}
if (col.allowCreate) {
allowCreateLoadData.push(key);
delete col.allowCreate;
}
});
const schema = new Schema(newColAttributes, {
toJSON: {
virtuals: true,
transform: (doc, ret) => {
decimals.map((key) => {
// eslint-disable-next-line no-param-reassign
ret[key] = parseInt(ret[key], 10);
return true;
});
return ret;
},
},
});
schema.plugin(mongoosePaginate);
schema.pre('validate', function (next) {
validator(this.toJSON(), validationRules);
next();
});
if (isTrash) {
schema.index({deleted_at: -1});
} else {
newIndexes.map((idx) => {
return schema.index(idx.index, idx.opt);
});
}
schema.pre('save', function (next) {
if (this.constructor.name !== 'EmbeddedDocument') {
const now = Math.floor(Date.now() / 1000);
this.wasNew = false;
this.modifiedFields = [];
if (this.isNew) {
this.wasNew = true;
if (this.beforeCreate) {
this.beforeCreate();
}
if (!this.created_at) {
this.created_at = now;
}
if (this.indexing_domains) {
this.indexing_domains = [];
}
}
if (this.isForceModify) {
this.modified_at = now;
}
if (this.isModified()) {
this.modifiedFields = this.modifiedPaths();
if (this.beforeModify) {
this.beforeModify();
}
this.increment();
if (!this.isNoModify && (!this.wasNew || !this.modified_at)) {
this.modified_at = now;
}
}
}
next();
});
schema.post('save', function (doc, next) {
if (this.constructor.name !== 'EmbeddedDocument') {
if (this.wasNew) {
if (this.afterCreate) {
this.afterCreate();
}
}
if (this.afterModify) {
this.afterModify();
}
}
next();
});
schema.methods.loadData = function (data, allows = []) {
let newAllows = allows;
if (this.isNew) {
newAllows = [...allows, ...allowCreateLoadData];
}
const guardData = omitItem(data, ['_id', '__v', ...guardLoadData], newAllows);
_.map(guardData, (value, key) => {
if (value !== null && value !== undefined) {
if (!_.isObject(data[key]) || !_.isEqual(this[key], value)) {
this[key] = value;
}
}
if (value === this._DELETE) {
this[key] = undefined;
}
});
return this;
};
schema.virtual('id').get(function () {
return this._id;
});
schema.plugin(mongooseLeanVirtuals);
schema.loadClass(Model);
return schema;
}
class Model {
static _DELETE = '__DELETE__';
static isModify = true;
static isMetadata = false;
static indexes = [];
static schema = {};
static register(connection, isTrash = false) {
const schemaModel = createSchema(
this,
{
...this.schema,
message: {
type: DataTypes.STRING,
},
status: {
type: DataTypes.NUMBER,
default: AVAILABLE_STATUS.WAITING,
validationRule: schemaEnum(_.values(AVAILABLE_STATUS), true),
index: true,
},
...(this.isModify ? DefaultAttributes.MODIFY : {}),
...(this.isMetadata ? DefaultAttributes.META_DATA : {}),
...(isTrash ? DefaultAttributes.TRASH : {}),
},
isTrash
);
this.connection = connection;
const tableName = this.tableName ? this.tableName : this.name;
const newModel = this.connection.model(this.name, schemaModel, isTrash ? `Trash${tableName}` : tableName);
newModel.isModify = this.isModify;
newModel.isMetadata = this.isMetadata;
return newModel;
}
generateMeta(title, description, keywords, image = null) {
this.metadata_title = this.metadata_title ? this.metadata_title : title;
this.metadata_title_og = this.metadata_title_og ? this.metadata_title_og : title;
this.metadata_description = this.metadata_description ? this.metadata_description : description;
this.metadata_description_og = this.metadata_description_og ? this.metadata_description_og : description;
this.metadata_keywords = this.metadata_keywords ? this.metadata_keywords : keywords;
if (image) {
this.metadata_image_url = this.metadata_image_url ? this.metadata_image_url : image;
}
}
forceGenerateMeta(title, description, keywords, image = null) {
this.metadata_title = title;
this.metadata_title_og = title;
this.metadata_description = description;
this.metadata_description_og = description;
this.metadata_keywords = keywords;
if (image) {
this.metadata_image_url = image;
}
}
beforeCreate() {
if (!this.slug) {
if (this.title) {
this.slug = toSlug(this.title);
} else if (this.name) {
this.slug = toSlug(this.name);
}
}
}
beforeModify() {
if (typeof this.message === 'string') {
this.message = this.message.substring(0, 500);
}
if (this.html_content) {
this.html_content = sanitize(this.html_content);
}
}
afterCreate() {}
afterModify() {}
modify(data = {}, allows = []) {
if (data.userId) {
this.modified_id = data.userId;
}
this.loadData(data, allows);
return this.save();
}
fix(data = {}, allows = []) {
this.isNoModify = true;
this.loadData(data, allows);
return this.save();
}
}
export {Model, DataTypes, validationRuleObjectId, DefaultAttributes, Schema};