pull/2/head
ws 6 months ago
parent 2da99f65bb
commit d3fbccac3e

@ -5,14 +5,14 @@ const metrics = require('@tryghost/metrics');
const sentry = require('../../../shared/sentry');
const states = {
const states = { //定义数据库状态
READY: 0,
NEEDS_INITIALISATION: 1,
NEEDS_MIGRATION: 2,
ERROR: 3
};
const printState = ({state}) => {
const printState = ({state}) => { //打印当前数据库状态
if (state === states.READY) {
logging.info('Database is in a ready state.');
}
@ -37,12 +37,12 @@ class DatabaseStateManager {
});
}
async getState() {
async getState() { //获得当前数据库状态
let state = states.READY;
try {
await this.knexMigrator.isDatabaseOK();
return state;
} catch (error) {
} catch (error) { //对错误状态进行处理
// CASE: database has not yet been initialized
if (error.code === 'DB_NOT_INITIALISED') {
state = states.NEEDS_INITIALISATION;
@ -71,12 +71,12 @@ class DatabaseStateManager {
});
}
sentry.captureException(errorToThrow);
sentry.captureException(errorToThrow);//记录错误信息
throw errorToThrow;
}
}
async makeReady() {
async makeReady() { //将数据库状态设置为READY
try {
let state = await this.getState();
@ -111,7 +111,7 @@ class DatabaseStateManager {
state = await this.getState();
printState({state});
} catch (error) {
} catch (error) { //对错误状态进行处理
let errorToThrow = error;
if (!errors.utils.isGhostError(error)) {
errorToThrow = new errors.InternalServerError({

@ -9,13 +9,13 @@ const {sequence} = require('@tryghost/promise');
const messages = {
errorExportingData: 'Error exporting data'
};
//负责将数据库内容到处为可移植的json格式
const {
TABLES_ALLOWLIST,
SETTING_KEYS_BLOCKLIST
} = require('./table-lists');
const exportTable = function exportTable(tableName, options) {
const exportTable = function exportTable(tableName, options) {//单表导出函数
if (TABLES_ALLOWLIST.includes(tableName) ||
(options.include && _.isArray(options.include) && options.include.indexOf(tableName) !== -1)) {
const query = (options.transacting || db.knex)(tableName);
@ -24,14 +24,14 @@ const exportTable = function exportTable(tableName, options) {
}
};
const getSettingsTableData = function getSettingsTableData(settingsData) {
const getSettingsTableData = function getSettingsTableData(settingsData) { //数据过滤函数,移除黑名单中的设置项
return settingsData && settingsData.filter((setting) => {
return !SETTING_KEYS_BLOCKLIST.includes(setting.key);
});
};
const doExport = async function doExport(options) {
options = options || {include: []};
const doExport = async function doExport(options) {//导出主函数
options = options || {include: []};//默认选项,包含所有表
try {
const tables = await commands.getTables(options.transacting);
@ -41,7 +41,7 @@ const doExport = async function doExport(options) {
}));
const exportData = {
meta: {
meta: { //导出元数据包含导出时间和Ghost版本
exported_on: new Date().getTime(),
version: ghostVersion.full
},

@ -1,4 +1,5 @@
// NOTE: these tables can be optionally included to have full db-like export
//比较重要的表,需要导出进行操作
const BACKUP_TABLES = [
'actions',
'api_keys',

@ -4,7 +4,7 @@ const config = require('../../../../shared/config');
const urlUtils = require('../../../../shared/url-utils');
const storage = require('../../../adapters/storage');
let ImageHandler;
//各种类型文本的导入处理程序
ImageHandler = {
type: 'images',
extensions: config.get('uploads').images.extensions,

@ -52,10 +52,12 @@ let defaults = {
contentTypes: ['application/zip', 'application/x-zip-compressed'],
directories: []
};
/* Ghost
* 负责导入图片媒体文件内容文件Revue数据JSON数据Markdown数据等
*/
class ImportManager {
constructor() {
const mediaHandler = new ImporterContentFileHandler({
const mediaHandler = new ImporterContentFileHandler({//媒体文件导入处理程序
type: 'media',
// @NOTE: making the second parameter strict folder "content/media" brakes the glob pattern
// in the importer, so we need to keep it as general "content" unless
@ -69,7 +71,7 @@ class ImportManager {
storage: mediaStorage
});
const filesHandler = new ImporterContentFileHandler({
const filesHandler = new ImporterContentFileHandler({//文件导入处理程序
type: 'files',
// @NOTE: making the second parameter strict folder "content/files" brakes the glob pattern
// in the importer, so we need to keep it as general "content" unless
@ -82,12 +84,12 @@ class ImportManager {
urlUtils: urlUtils,
storage: fileStorage
});
const imageImporter = new ContentFileImporter({
//导入器初始化
const imageImporter = new ContentFileImporter({
type: 'images',
store: imageStorage
});
const mediaImporter = new ContentFileImporter({
const mediaImporter = new ContentFileImporter({
type: 'media',
store: mediaStorage
});
@ -98,25 +100,25 @@ class ImportManager {
});
/**
* @type {Importer[]} importers
* @type {Importer[]} importers 导入器数组包含图片导入器媒体文件导入器内容文件导入器Revue导入器和数据导入器
*/
this.importers = [imageImporter, mediaImporter, contentFilesImporter, RevueImporter, DataImporter];
/**
* @type {Handler[]}
* @type {Handler[]} handlers 处理程序数组包含图片处理程序媒体文件处理程序文件处理程序Revue处理程序和JSON处理程序
*/
this.handlers = [ImageHandler, mediaHandler, filesHandler, RevueHandler, JSONHandler, MarkdownHandler];
// Keep track of file to cleanup at the end
/**
* @type {?string}
* @type {?string} fileToDelete 待删除的文件路径初始值为null
*/
this.fileToDelete = null;
}
/**
* Get an array of all the file extensions for which we have handlers
* @returns {string[]}
* @returns {string[]} extensions 所有支持的文件扩展名数组
*/
getExtensions() {
return _.union(_.flatMap(this.handlers, 'extensions'), defaults.extensions);
@ -124,7 +126,7 @@ class ImportManager {
/**
* Get an array of all the mime types for which we have handlers
* @returns {string[]}
* @returns {string[]} contentTypes 所有支持的文件MIME类型数组
*/
getContentTypes() {
return _.union(_.flatMap(this.handlers, 'contentTypes'), defaults.contentTypes);
@ -132,7 +134,7 @@ class ImportManager {
/**
* Get an array of directories for which we have handlers
* @returns {string[]}
* @returns {string[]} directories 所有支持的文件目录数组
*/
getDirectories() {
return _.union(_.flatMap(this.handlers, 'directories'), defaults.directories);
@ -140,8 +142,8 @@ class ImportManager {
/**
* Convert items into a glob string
* @param {String[]} items
* @returns {String}
* @param {String[]} items 要转换的文件扩展名数组
* @returns {String} globPattern 转换后的文件扩展名glob模式字符串
*/
getGlobPattern(items) {
return '+(' + _.reduce(items, function (memo, ext) {
@ -150,9 +152,9 @@ class ImportManager {
}
/**
* @param {String[]} extensions
* @param {Number} [level]
* @returns {String}
* @param {String[]} extensions 要匹配的文件扩展名数组
* @param {Number} [level=ROOT_OR_SINGLE_DIR] 匹配级别默认值为ROOT_OR_SINGLE_DIR
* @returns {String} globPattern 转换后的文件扩展名glob模式字符串
*/
getExtensionGlob(extensions, level) {
const prefix = level === ALL_DIRS ? '**/*' :
@ -163,9 +165,9 @@ class ImportManager {
/**
*
* @param {String[]} directories
* @param {Number} [level]
* @returns {String}
* @param {String[]} directories 要匹配的文件目录数组
* @param {Number} [level=ROOT_OR_SINGLE_DIR] 匹配级别默认值为ROOT_OR_SINGLE_DIR
* @returns {String} globPattern 转换后的文件目录glob模式字符串
*/
getDirectoryGlob(directories, level) {
const prefix = level === ALL_DIRS ? '**/' :

@ -5,9 +5,9 @@ const {sequence} = require('@tryghost/promise');
const models = require('../../../models');
const baseUtils = require('../../../models/base/utils');
const moment = require('moment');
class FixtureManager {
const moment = require('moment');
//把写在 JSON/JS 里的“蓝图”变成真正的数据库记录,并建立好它们之间的关联。
class FixtureManager {
/**
* Create a new FixtureManager instance
*
@ -194,22 +194,23 @@ class FixtureManager {
* @param {String} objName
* @returns {Object} fixture relation
*/
/**
* 查找特定对象的权限关系
*
* 用于设置角色权限管理员对文章有所有权限
*/
findPermissionRelationsForObject(objName, role) {
// Make a copy and delete any entries we don't want
const foundRelation = _.cloneDeep(this.findRelationFixture('Role', 'Permission'));
const foundRelation = this.findRelationFixture('Role', 'Permission');
// 过滤只保留指定对象的权限
_.each(foundRelation.entries, (entry, key) => {
_.each(entry, (perm, obj) => {
if (obj !== objName) {
delete entry[obj];
delete entry[obj]; // 移除其他对象权限
}
});
if (_.isEmpty(entry) || (role && role !== key)) {
delete foundRelation.entries[key];
}
});
return foundRelation;
}
@ -382,52 +383,49 @@ class FixtureManager {
* @param {{from, to, entries}} relationFixture
* @returns {Promise<any>}
*/
/**
* 创建模型之间的关联关系
*
* 处理多对多一对多等复杂关系
* 避免重复关联检查是否已存在相同关系
*
* @param {{from, to, entries}} relationFixture - 关系配置
*/
async addFixturesForRelation(relationFixture, options) {
const ops = [];
let max = 0;
// 获取关联双方的现有数据
const data = await this.fetchRelationData(relationFixture, options);
_.each(relationFixture.entries, (entry, key) => {
const fromItem = data.from.find(FixtureManager.matchFunc(relationFixture.from.match, key));
// CASE: You add new fixtures e.g. a new role in a new release.
// As soon as an **older** migration script wants to add permissions for any resource, it iterates over the
// permissions for each role. But if the role does not exist yet, it won't find the matching db entry and breaks.
if (!fromItem) {
logging.warn('Skip: Target database entry not found for key: ' + key);
return Promise.resolve();
}
// 查找源模型
const fromItem = data.from.find(
FixtureManager.matchFunc(relationFixture.from.match, key)
);
_.each(entry, (value, entryKey) => {
let toItems = data.to.filter(FixtureManager.matchFunc(relationFixture.to.match, entryKey, value));
max += toItems.length;
// Remove any duplicates that already exist in the collection
// 查找目标模型
let toItems = data.to.filter(
FixtureManager.matchFunc(relationFixture.to.match, entryKey, value)
);
// 移除已存在的关联(避免重复)
toItems = _.reject(toItems, (item) => {
return fromItem
.related(relationFixture.from.relation)
.find((model) => {
const objectToMatch = FixtureManager.matchObj(relationFixture.to.match, item);
return Object.keys(objectToMatch).every((keyToCheck) => {
return model.get(keyToCheck) === objectToMatch[keyToCheck];
});
});
return fromItem.related(relationFixture.from.relation)
.find((model) => /* 检查是否已关联 */);
});
if (toItems && toItems.length > 0) {
ops.push(function addRelationItems() {
return baseUtils.attach(
models[relationFixture.from.Model || relationFixture.from.model],
fromItem.id,
relationFixture.from.relation,
toItems,
options
);
});
// 创建新关联
if (toItems.length > 0) {
ops.push(() => baseUtils.attach(
models[relationFixture.from.model],
fromItem.id,
relationFixture.from.relation,
toItems,
options
));
}
});
});
}
const result = await sequence(ops);
return {expected: max, done: _(result).map('length').sum()};
@ -472,4 +470,4 @@ class FixtureManager {
}
}
module.exports = FixtureManager;
module.exports = FixtureManager;

@ -116,45 +116,45 @@ module.exports = {
['slug', 'type'] // 唯一约束同一类型的文章不能有相同的slug
]
},
posts_meta: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', unique: true},
og_image: {type: 'string', maxlength: 2000, nullable: true},
og_title: {type: 'string', maxlength: 300, nullable: true},
og_description: {type: 'string', maxlength: 500, nullable: true},
twitter_image: {type: 'string', maxlength: 2000, nullable: true},
twitter_title: {type: 'string', maxlength: 300, nullable: true},
twitter_description: {type: 'string', maxlength: 500, nullable: true},
meta_title: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}},
meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}},
email_subject: {type: 'string', maxlength: 300, nullable: true},
frontmatter: {type: 'text', maxlength: 65535, nullable: true},
feature_image_alt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 191}}},
feature_image_caption: {type: 'text', maxlength: 65535, nullable: true},
email_only: {type: 'boolean', nullable: false, defaultTo: false}
posts_meta: { //用于存储文章的额外元数据
id: {type: 'string', maxlength: 24, nullable: false, primary: true},//主键ID唯一标识文章元数据
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', unique: true}, // 关联的文章ID唯一标识
og_image: {type: 'string', maxlength: 2000, nullable: true}, // Open Graph 图片URL
og_title: {type: 'string', maxlength: 300, nullable: true}, // Open Graph 标题最大300字符
og_description: {type: 'string', maxlength: 500, nullable: true}, // Open Graph 描述最大500字符
twitter_image: {type: 'string', maxlength: 2000, nullable: true}, // Twitter 图片URL
twitter_title: {type: 'string', maxlength: 300, nullable: true}, // Twitter 标题最大300字符
twitter_description: {type: 'string', maxlength: 500, nullable: true}, // Twitter 描述最大500字符
meta_title: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}},// SEO 标题最大300字符
meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}},// SEO 描述最大500字符
email_subject: {type: 'string', maxlength: 300, nullable: true},// 邮件主题最大300字符
frontmatter: {type: 'text', maxlength: 65535, nullable: true},// 文章的Frontmatter内容用于自定义元数据
feature_image_alt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 191}}},// 封面图替代文本最大191字符
feature_image_caption: {type: 'text', maxlength: 65535, nullable: true},// 封面图标题,用于图片描述
email_only: {type: 'boolean', nullable: false, defaultTo: false}, // 是否仅通过邮件发送,默认为否
},
// NOTE: this is the staff table
users: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
name: {type: 'string', maxlength: 191, nullable: false},
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
password: {type: 'string', maxlength: 60, nullable: false},
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
profile_image: {type: 'string', maxlength: 2000, nullable: true},
cover_image: {type: 'string', maxlength: 2000, nullable: true},
bio: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 250}}},
website: {type: 'string', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},
location: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 150}}},
facebook: {type: 'string', maxlength: 2000, nullable: true},
id: {type: 'string', maxlength: 24, nullable: false, primary: true},//主键ID唯一标识用户
name: {type: 'string', maxlength: 191, nullable: false}, // 用户名
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},//用户slug唯一标识
password: {type: 'string', maxlength: 60, nullable: false},//用户密码
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},//用户邮箱,唯一标识
profile_image: {type: 'string', maxlength: 2000, nullable: true},// 用户头像URL
cover_image: {type: 'string', maxlength: 2000, nullable: true},//用户封面图URL
bio: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 250}}},//用户简介最大250字符
website: {type: 'string', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},//用户网站URL
location: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 150}}},//用户位置最大150字符
facebook: {type: 'string', maxlength: 2000, nullable: true},//用户Facebook链接
twitter: {type: 'string', maxlength: 2000, nullable: true},
threads: {type: 'string', maxlength: 191, nullable: true},
bluesky: {type: 'string', maxlength: 191, nullable: true},
mastodon: {type: 'string', maxlength: 191, nullable: true},
tiktok: {type: 'string', maxlength: 191, nullable: true},
youtube: {type: 'string', maxlength: 191, nullable: true},
instagram: {type: 'string', maxlength: 191, nullable: true},
linkedin: {type: 'string', maxlength: 191, nullable: true},
accessibility: {type: 'text', maxlength: 65535, nullable: true},
threads: {type: 'string', maxlength: 191, nullable: true},//用户Threads链接
bluesky: {type: 'string', maxlength: 191, nullable: true},//用户Bluesky链接
mastodon: {type: 'string', maxlength: 191, nullable: true},//用户Mastodon链接
tiktok: {type: 'string', maxlength: 191, nullable: true},//用户TikTok链接
youtube: {type: 'string', maxlength: 191, nullable: true}, //用户YouTube链接
instagram: {type: 'string', maxlength: 191, nullable: true},//用户Instagram链接
linkedin: {type: 'string', maxlength: 191, nullable: true},//用户LinkedIn链接
accessibility: {type: 'text', maxlength: 65535, nullable: true},//用户可访问性设置
status: {
type: 'string',
maxlength: 50,
@ -173,7 +173,7 @@ module.exports = {
}
},
// NOTE: unused at the moment and reserved for future features
locale: {type: 'string', maxlength: 6, nullable: true},
locale: {type: 'string', maxlength: 6, nullable: true},//用户语言设置
visibility: {
type: 'string',
maxlength: 50,
@ -197,25 +197,25 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
posts_authors: {
posts_authors: { // 文章作者关联表
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id'},
author_id: {type: 'string', maxlength: 24, nullable: false, references: 'users.id'},
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
},
roles: {
roles: {// 角色
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
name: {type: 'string', maxlength: 50, nullable: false, unique: true},
description: {type: 'string', maxlength: 2000, nullable: true},
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
roles_users: {
roles_users: { // 角色用户关联表
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
role_id: {type: 'string', maxlength: 24, nullable: false},
user_id: {type: 'string', maxlength: 24, nullable: false}
},
permissions: {
permissions: {// 权限
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
name: {type: 'string', maxlength: 50, nullable: false, unique: true},
object_type: {type: 'string', maxlength: 50, nullable: false},
@ -224,17 +224,17 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
permissions_users: {
permissions_users: {// 权限用户关联表
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
user_id: {type: 'string', maxlength: 24, nullable: false},
permission_id: {type: 'string', maxlength: 24, nullable: false}
},
permissions_roles: {
permissions_roles: {// 权限角色关联表
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
role_id: {type: 'string', maxlength: 24, nullable: false},
permission_id: {type: 'string', maxlength: 24, nullable: false}
},
settings: {
settings: {// 设置
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
group: {
type: 'string',
@ -278,7 +278,7 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
tags: {
tags: {// 标签
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
name: {type: 'string', maxlength: 191, nullable: false, validations: {matches: /^([^,]|$)/}},
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
@ -307,7 +307,7 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
posts_tags: {
posts_tags: {// 文章标签关联表
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id'},
tag_id: {type: 'string', maxlength: 24, nullable: false, references: 'tags.id'},
@ -316,7 +316,7 @@ module.exports = {
['post_id','tag_id']
]
},
invites: {
invites: { // 邀请
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
role_id: {type: 'string', maxlength: 24, nullable: false},
status: {
@ -332,14 +332,14 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
brute: {
brute: {// 暴力破解
key: {type: 'string', maxlength: 191, primary: true},
firstRequest: {type: 'bigInteger'},
lastRequest: {type: 'bigInteger'},
lifetime: {type: 'bigInteger'},
count: {type: 'integer'}
},
sessions: {
sessions: {// 会话
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
session_id: {type: 'string', maxlength: 32, nullable: false, unique: true},
user_id: {type: 'string', maxlength: 24, nullable: false},
@ -347,7 +347,7 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
integrations: {
integrations: {// 集成
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
type: {
type: 'string',
@ -363,12 +363,12 @@ module.exports = {
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
webhooks: {
webhooks: {//用于实现事件驱动的外部系统集成当系统中发生特定时间的时候会自动向配置的外部url发送http通知
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
event: {type: 'string', maxlength: 50, nullable: false, validations: {isLowercase: true}},
target_url: {type: 'string', maxlength: 2000, nullable: false},
name: {type: 'string', maxlength: 191, nullable: true},
secret: {type: 'string', maxlength: 191, nullable: true},
event: {type: 'string', maxlength: 50, nullable: false, validations: {isLowercase: true}},// 事件类型
target_url: {type: 'string', maxlength: 2000, nullable: false},// 目标url
name: {type: 'string', maxlength: 191, nullable: true},// 名称
secret: {type: 'string', maxlength: 191, nullable: true},// 密钥
// @NOTE: the defaultTo does not make sense to set on DB layer as it leads to unnecessary maintenance every major release
// would be ideal if we can remove the default and instead have "isIn" validation checking if it's a valid version e.g: 'v3', 'v4', 'canary'
api_version: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'v2'},
@ -389,14 +389,14 @@ module.exports = {
nullable: false,
validations: {isIn: [['content', 'admin']]}
},
secret: {
secret: { //密钥字符串
type: 'string',
maxlength: 191,
nullable: false,
unique: true,
unique: true,//防止出现重复
validations: {isLength: {min: 26, max: 128}}
},
role_id: {type: 'string', maxlength: 24, nullable: true},
role_id: {type: 'string', maxlength: 24, nullable: true}, // 角色ID关联到roles表定义密钥的权限级别
// integration_id is nullable to allow "internal" API keys that don't show in the UI
integration_id: {type: 'string', maxlength: 24, nullable: true},
user_id: {type: 'string', maxlength: 24, nullable: true},
@ -427,7 +427,7 @@ module.exports = {
feature_image_caption: {type: 'text', maxlength: 65535, nullable: true},
custom_excerpt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}}
},
members: {
members: {// 会员
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false, unique: true, validations: {isUUID: true}},
transient_id: {type: 'string', maxlength: 191, nullable: false, unique: true},
@ -450,7 +450,7 @@ module.exports = {
last_commented_at: {type: 'dateTime', nullable: true},
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
'@@INDEXES@@': [
'@@INDEXES@@': [ // 索引
['email_disabled']
]
},

@ -25,28 +25,38 @@ const messages = {
*
* ## on model add
* - validate everything to catch required fields
*/
/**
* 数据库模式验证函数 - 根据schema定义验证模型数据的完整性
*
* 这个函数负责验证传入的模型数据是否符合数据库表的schema定义
* 包括数据类型长度限制必填字段等约束条件如果不通过就统一抛错误防止数据污染
*/
function validateSchema(tableName, model, options) {
function validateSchema(tableName, model, options) {
options = options || {};
const columns = _.keys(schema[tableName]);
let validationErrors = [];
const columns = _.keys(schema[tableName]); // 获取表中所有列名
let validationErrors = []; // 存储验证错误
_.each(columns, function each(columnKey) {
_.each(columns, function each(columnKey) { // 遍历表中的每一列进行验证
let message = ''; // KEEP: Validator.js only validates strings.
// 将字段值转换为字符串进行验证Validator.js只验证字符串
const strVal = _.toString(model.get(columnKey));
// 如果是更新操作且字段未改变,则跳过验证(优化性能)
if (options.method !== 'insert' && !_.has(model.changed, columnKey)) {
return;
}
// check nullable
}
// ==================== 必填字段验证 ====================
// 检查非空约束字段不可为空、不是text类型、没有默认值
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'nullable') &&
schema[tableName][columnKey].nullable !== true &&
schema[tableName][columnKey].nullable !== true && // 字段不可为空
Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type') &&
schema[tableName][columnKey].type !== 'text' &&
!Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo')
schema[tableName][columnKey].type !== 'text' && // 排除text类型允许空字符串
!Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo') // 没有默认值
) {
// 检查字段值是否为空
if (validator.isEmpty(strVal)) {
message = tpl(messages.valueCannotBeBlank, {
tableName: tableName,
@ -54,14 +64,16 @@ function validateSchema(tableName, model, options) {
});
validationErrors.push(new errors.ValidationError({
message: message,
context: tableName + '.' + columnKey
context: tableName + '.' + columnKey // 错误上下文:表名.字段名
}));
}
}
// validate boolean columns
}
// ==================== 布尔字段验证 ====================
// 验证布尔类型字段
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
&& schema[tableName][columnKey].type === 'boolean') {
// 检查值是否为有效的布尔值或空值
if (!(validator.isBoolean(strVal) || validator.isEmpty(strVal))) {
message = tpl(messages.valueMustBeBoolean, {
tableName: tableName,
@ -73,15 +85,18 @@ function validateSchema(tableName, model, options) {
}));
}
// CASE: ensure we transform 0|1 to false|true
// CASE: 确保将0|1转换为false|true数据标准化
if (!validator.isEmpty(strVal)) {
model.set(columnKey, !!model.get(columnKey));
model.set(columnKey, !!model.get(columnKey)); // 强制转换为布尔值
}
}
// TODO: check if mandatory values should be enforced
// TODO: 检查是否应该强制执行必填值
// 当字段值不为null或undefined时进行进一步验证
if (model.get(columnKey) !== null && model.get(columnKey) !== undefined) {
// check length
// ==================== 长度限制验证 ====================
// 检查字段最大长度限制
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'maxlength')) {
if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) {
message = tpl(messages.valueExceedsMaxLength,
@ -97,12 +112,16 @@ function validateSchema(tableName, model, options) {
}
}
// check validations objects
// ==================== 自定义验证规则 ====================
// 执行schema中定义的自定义验证规则
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'validations')) {
validationErrors = validationErrors.concat(validator.validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName));
validationErrors = validationErrors.concat(
validator.validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName)
);
}
// check type
// ==================== 数据类型验证 ====================
// 检查整数类型字段
if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')) {
if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) {
message = tpl(messages.valueIsNotInteger, {
@ -118,10 +137,13 @@ function validateSchema(tableName, model, options) {
}
});
// ==================== 验证结果处理 ====================
// 如果有验证错误使用Promise.reject返回错误数组
if (validationErrors.length !== 0) {
return Promise.reject(validationErrors);
}
// 验证通过返回成功的Promise
return Promise.resolve();
}
module.exports = validateSchema;
module.exports = validateSchema;
Loading…
Cancel
Save