UNPKG

@azteam/mongo-model

Version:

N/A

516 lines (467 loc) 14.5 kB
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};