database-proxy
Version:
Through a set of access control rules configuration database access to realize the client directly access the database via HTTP.
263 lines (262 loc) • 11.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Constraint = exports.ConstraintTypes = void 0;
const console_1 = require("console");
const $ = __importStar(require("validator"));
const script_1 = require("./script");
var ConstraintTypes;
(function (ConstraintTypes) {
ConstraintTypes["REQUIRED"] = "required";
ConstraintTypes["IN"] = "in";
ConstraintTypes["DEFAULT"] = "default";
ConstraintTypes["LENGTH"] = "length";
ConstraintTypes["NUMBER"] = "number";
ConstraintTypes["UNIQUE"] = "unique";
ConstraintTypes["MATCH"] = "match";
ConstraintTypes["EXISTS"] = "exists";
ConstraintTypes["CONDITION"] = "condition";
ConstraintTypes["COND"] = "cond";
ConstraintTypes["NOT_EXISTS"] = "notExists";
})(ConstraintTypes || (exports.ConstraintTypes = ConstraintTypes = {}));
class Constraint {
constructor(context, data, ignoreConstraints) {
(0, console_1.assert)(context, 'context cannot be empty');
(0, console_1.assert)(data, 'data to be constrainted cannot be empty');
this.context = context;
this.data = data;
this.ignoreConstraints = ignoreConstraints !== null && ignoreConstraints !== void 0 ? ignoreConstraints : [];
}
/**
* 验证请求 query 中的一个字段
* 1. 如果请求 query 中缺省了该字段,除进行 required 和 default 检查外,则不进行额外约束,直接通过
* 2. 如果请求 query 中存在该字段,则对该字段进行所有约束检查,任意一条检查未通过则返回检查失败
* @param field 要验证的字段名
* @param constrains 此字段的约束配置
* @param context 请求上下文
* @returns
*/
async constraintField(field, constrains) {
(0, console_1.assert)(this.context, 'context cannot be empty');
(0, console_1.assert)(this.data, 'data to be constrainted cannot be empty');
const data = this.data;
if (typeof constrains === 'string') {
constrains = { condition: constrains };
}
if (typeof constrains !== 'object') {
return `config error: [${field}]'s constraint config must be an object`;
}
/**
* 当此字段缺省时,进行 required 和 default 约束检查
* 1. 如果存在 default 设置,则将缺省字段设置为默认值,跳过 required 以及后续约束检查,直接通过
* 2. 如果不存在 default,则进行 requied 约束检查,并跳过后续约束检查
*/
if (!this.ignored('default') && data[field] === undefined) {
// if default
if (constrains['default'] !== undefined) {
data[field] = data[field] = constrains['default'];
return null;
}
else if (!this.ignored('required')) {
const isRequired = constrains['required'] == true;
return isRequired ? `${field} is required` : null;
}
}
// 排除掉 required & default 约束
const constraintNames = Object.keys(constrains).filter((name) => [ConstraintTypes.REQUIRED, ConstraintTypes.DEFAULT].includes(name) ==
false);
// 对每个约束进行检查
for (const constraintName of constraintNames) {
if (this.ignored(constraintName)) {
continue;
}
const options = constrains[constraintName];
const error = await this.checkConstraint(constraintName, options, field);
if (error)
return error;
}
return null;
}
/**
* 对字段进行约束性检查
* @param constraintName 约束名
* @param constraintOption 约束配置
* @param fieldKey query 字段名
* @param context 请求上下文
* @returns error or null
*/
async checkConstraint(constraintName, constraintOption, fieldKey) {
(0, console_1.assert)(this.context, 'context cannot be empty');
(0, console_1.assert)(this.data, 'data to be constrainted cannot be empty');
const data = this.data;
if (!Constraint.ALLOWED_CONSTRAINTS.includes(constraintName)) {
return `config error: unknown rule [${constraintName}]`;
}
const value = data[fieldKey];
const option = constraintOption;
if (constraintName === ConstraintTypes.CONDITION) {
return await this.performCondition(option, fieldKey, value);
}
if (constraintName === ConstraintTypes.COND) {
return await this.performCondition(option, fieldKey, value);
}
if (constraintName === ConstraintTypes.IN) {
return this.performIn(option, fieldKey, value);
}
if (constraintName === ConstraintTypes.LENGTH) {
return this.performLength(option, fieldKey, value);
}
if (constraintName === ConstraintTypes.NUMBER) {
return this.performNumber(option, fieldKey, value);
}
if (constraintName === ConstraintTypes.MATCH) {
return this.performMatch(option, fieldKey, value);
}
// {"exists": "/users/id"},
if (constraintName === ConstraintTypes.EXISTS) {
return await this.performExists(option, fieldKey, value);
}
// {"notExists": "/users/id"},
if (constraintName === ConstraintTypes.NOT_EXISTS) {
return await this.performNotExists(option, fieldKey, value);
}
if (constraintName === ConstraintTypes.UNIQUE && constraintOption) {
return await this.performUnique(option, fieldKey, value);
}
return null;
}
/**
* 约束未被忽略
* @param constraintName 约束名
* @returns
*/
ignored(constraintName) {
return this.ignoreConstraints.includes(constraintName);
}
async performCondition(constraintOption, fieldKey, value) {
const { injections } = this.context;
const global = Object.assign(Object.assign({}, injections), { $value: value, $v: value });
const { result, error } = await (0, script_1.executeScript)(constraintOption, global, this.context, fieldKey);
if (error)
return error;
// 如果 result 为真,则通过验证
if (result)
return null;
return `condition evaluted to false`;
}
performIn(constraintOption, fieldKey, value) {
if (!(constraintOption instanceof Array)) {
return `config error: ${fieldKey}#in must be an array`;
}
if (!constraintOption.includes(value)) {
const str = constraintOption.join(',');
return `${fieldKey} should equal to one of [${str}]`;
}
}
performLength(constraintOption, fieldKey, value) {
if (!(constraintOption instanceof Array && constraintOption.length)) {
return `config error: ${fieldKey}#length must be an array with 1-2 integer element, ex. [3, 10]`;
}
const min = constraintOption[0];
const max = constraintOption.length >= 2 ? constraintOption[1] : undefined;
const ok = $.isLength(value, min, max);
if (!ok) {
let error = `length of ${fieldKey} should >= ${min}`;
if (max !== undefined)
error += ` and <= ${max}`;
return error;
}
}
performNumber(constraintOption, fieldKey, value) {
if (!(constraintOption instanceof Array && constraintOption.length)) {
return `config error: ${fieldKey}#number must be an array with 1-2 integer element, ex. [3, 10]`;
}
const min = constraintOption[0];
const max = constraintOption.length >= 2 ? constraintOption[1] : Infinity;
const ok = value >= min && value <= max;
if (!ok) {
let error = `${fieldKey} should >= ${min}`;
if (max !== Infinity)
error += ` and <= ${max}`;
return error;
}
}
performMatch(constraintOption, fieldKey, value) {
if (!(typeof constraintOption === 'string' && constraintOption.length)) {
return `config error: ${fieldKey}#match must be a string`;
}
try {
const partten = new RegExp(constraintOption);
const ok = partten.test(value);
if (!ok) {
return `${fieldKey} had invalid format`;
}
}
catch (error) {
return error;
}
}
async performExists(constraintOption, fieldKey, value) {
if (!(typeof constraintOption === 'string' && constraintOption.length)) {
return `config error: ${fieldKey}#exists must be a string`;
}
const { collection, field } = (0, script_1.parseQueryURI)(constraintOption);
const accessor = this.context.ruler.accessor;
const ret = await accessor.get(collection, { [field]: value });
if (!ret)
return `${fieldKey} not exists in ${collection}`;
}
async performNotExists(constraintOption, fieldKey, value) {
if (!(typeof constraintOption === 'string' && constraintOption.length)) {
return `config error: ${fieldKey}#notExists must be a string`;
}
const { collection, field } = (0, script_1.parseQueryURI)(constraintOption);
const accessor = this.context.ruler.accessor;
const ret = await accessor.get(collection, { [field]: value });
if (ret)
return `${fieldKey} already exists in ${collection}`;
}
async performUnique(_constraintOption, fieldKey, value) {
const accessor = this.context.ruler.accessor;
const collection = this.context.params.collection;
const ret = await accessor.get(collection, { [fieldKey]: value });
if (ret)
return `${fieldKey} already exists`;
}
}
exports.Constraint = Constraint;
Constraint.ALLOWED_CONSTRAINTS = [
ConstraintTypes.REQUIRED,
ConstraintTypes.IN,
ConstraintTypes.DEFAULT,
ConstraintTypes.LENGTH,
ConstraintTypes.NUMBER,
ConstraintTypes.UNIQUE,
ConstraintTypes.MATCH,
ConstraintTypes.EXISTS,
ConstraintTypes.CONDITION,
ConstraintTypes.COND,
ConstraintTypes.NOT_EXISTS,
];