zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
350 lines • 15.9 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateAttributeApplication = validateAttributeApplication;
const ast_1 = require("@zenstackhq/language/ast");
const sdk_1 = require("@zenstackhq/sdk");
const langium_1 = require("langium");
const pluralize_1 = __importDefault(require("pluralize"));
const utils_1 = require("./utils");
// a registry of function handlers marked with @check
const attributeCheckers = new Map();
// function handler decorator
function check(name) {
return function (_target, _propertyKey, descriptor) {
if (!attributeCheckers.get(name)) {
attributeCheckers.set(name, descriptor);
}
return descriptor;
};
}
/**
* Validates function declarations.
*/
class AttributeApplicationValidator {
validate(attr, accept) {
const decl = attr.decl.ref;
if (!decl) {
return;
}
const targetDecl = attr.$container;
if (decl.name === '@@@targetField' && !(0, ast_1.isAttribute)(targetDecl)) {
accept('error', `attribute "${decl.name}" can only be used on attribute declarations`, { node: attr });
return;
}
if ((0, ast_1.isDataModelField)(targetDecl) && !isValidAttributeTarget(decl, targetDecl)) {
accept('error', `attribute "${decl.name}" cannot be used on this type of field`, { node: attr });
}
if ((0, ast_1.isTypeDefField)(targetDecl) && !(0, sdk_1.hasAttribute)(decl, '@@@supportTypeDef')) {
accept('error', `attribute "${decl.name}" cannot be used on type declaration fields`, { node: attr });
}
if ((0, ast_1.isTypeDef)(targetDecl) && !(0, sdk_1.hasAttribute)(decl, '@@@supportTypeDef')) {
accept('error', `attribute "${decl.name}" cannot be used on type declarations`, { node: attr });
}
const filledParams = new Set();
for (const arg of attr.args) {
let paramDecl;
if (!arg.name) {
paramDecl = decl.params.find((p) => p.default && !filledParams.has(p));
if (!paramDecl) {
accept('error', `Unexpected unnamed argument`, {
node: arg,
});
return;
}
}
else {
paramDecl = decl.params.find((p) => p.name === arg.name);
if (!paramDecl) {
accept('error', `Attribute "${decl.name}" doesn't have a parameter named "${arg.name}"`, {
node: arg,
});
return;
}
}
if (!assignableToAttributeParam(arg, paramDecl, attr)) {
accept('error', `Value is not assignable to parameter`, {
node: arg,
});
return;
}
if (filledParams.has(paramDecl)) {
accept('error', `Parameter "${paramDecl.name}" is already provided`, { node: arg });
return;
}
filledParams.add(paramDecl);
arg.$resolvedParam = paramDecl;
}
const missingParams = decl.params.filter((p) => !p.type.optional && !filledParams.has(p));
if (missingParams.length > 0) {
accept('error', `Required ${(0, pluralize_1.default)('parameter', missingParams.length)} not provided: ${missingParams
.map((p) => p.name)
.join(', ')}`, { node: attr });
return;
}
// run checkers for specific attributes
const checker = attributeCheckers.get(decl.name);
if (checker) {
checker.value.call(this, attr, accept);
}
}
_checkModelLevelPolicy(attr, accept) {
const kind = (0, utils_1.getStringLiteral)(attr.args[0].value);
if (!kind) {
accept('error', `expects a string literal`, { node: attr.args[0] });
return;
}
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept);
// @encrypted fields cannot be used in policy rules
this.rejectEncryptedFields(attr, accept);
}
_checkFieldLevelPolicy(attr, accept) {
const kind = (0, utils_1.getStringLiteral)(attr.args[0].value);
if (!kind) {
accept('error', `expects a string literal`, { node: attr.args[0] });
return;
}
const kindItems = this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept);
const expr = attr.args[1].value;
if ((0, langium_1.streamAst)(expr).some((node) => (0, sdk_1.isFutureExpr)(node))) {
accept('error', `"future()" is not allowed in field-level policy rules`, { node: expr });
}
// 'update' rules are not allowed for relation fields
if (kindItems.includes('update') || kindItems.includes('all')) {
const field = attr.$container;
if ((0, sdk_1.isRelationshipField)(field)) {
accept('error', `Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.`, { node: attr });
}
}
// @encrypted fields cannot be used in policy rules
this.rejectEncryptedFields(attr, accept);
}
_checkValidate(attr, accept) {
var _a;
const condition = (_a = attr.args[0]) === null || _a === void 0 ? void 0 : _a.value;
if (condition &&
(0, langium_1.streamAst)(condition).some((node) => { var _a; return (0, sdk_1.isDataModelFieldReference)(node) && (0, ast_1.isDataModel)((_a = node.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl); })) {
accept('error', `\`@@validate\` condition cannot use relation fields`, { node: condition });
}
}
_checkUnique(attr, accept) {
var _a;
const fields = (_a = attr.args[0]) === null || _a === void 0 ? void 0 : _a.value;
if (fields && (0, ast_1.isArrayExpr)(fields)) {
fields.items.forEach((item) => {
if (!(0, ast_1.isReferenceExpr)(item)) {
accept('error', `Expecting a field reference`, { node: item });
return;
}
if (!(0, ast_1.isDataModelField)(item.target.ref)) {
accept('error', `Expecting a field reference`, { node: item });
return;
}
if (item.target.ref.$container !== attr.$container && (0, sdk_1.isDelegateModel)(item.target.ref.$container)) {
accept('error', `Cannot use fields inherited from a polymorphic base model in \`@@unique\``, {
node: item,
});
}
});
}
else {
accept('error', `Expected an array of field references`, { node: fields });
}
}
_checkRegex(attr, accept) {
var _a, _b;
const regex = (0, utils_1.getStringLiteral)((_a = attr.args[0]) === null || _a === void 0 ? void 0 : _a.value);
if (regex === undefined) {
accept('error', `Expecting a string literal`, { node: (_b = attr.args[0]) !== null && _b !== void 0 ? _b : attr });
return;
}
try {
new RegExp(regex);
}
catch (e) {
accept('error', `${e}`, { node: attr.args[0] });
}
}
rejectEncryptedFields(attr, accept) {
(0, langium_1.streamAllContents)(attr).forEach((node) => {
if ((0, sdk_1.isDataModelFieldReference)(node) && (0, sdk_1.hasAttribute)(node.target.ref, '@encrypted')) {
accept('error', `Encrypted fields cannot be used in policy rules`, { node });
}
});
}
validatePolicyKinds(kind, candidates, attr, accept) {
const items = kind.split(',').map((x) => x.trim());
items.forEach((item) => {
if (!candidates.includes(item)) {
accept('error', `Invalid policy rule kind: "${item}", allowed: ${candidates.map((c) => '"' + c + '"').join(', ')}`, { node: attr });
}
});
return items;
}
}
exports.default = AttributeApplicationValidator;
__decorate([
check('@@allow'),
check('@@deny')
], AttributeApplicationValidator.prototype, "_checkModelLevelPolicy", null);
__decorate([
check('@allow'),
check('@deny')
], AttributeApplicationValidator.prototype, "_checkFieldLevelPolicy", null);
__decorate([
check('@@validate')
], AttributeApplicationValidator.prototype, "_checkValidate", null);
__decorate([
check('@@unique')
], AttributeApplicationValidator.prototype, "_checkUnique", null);
__decorate([
check('@regex')
], AttributeApplicationValidator.prototype, "_checkRegex", null);
function assignableToAttributeParam(arg, param, attr) {
var _a, _b, _c, _d;
const argResolvedType = arg.$resolvedType;
if (!argResolvedType) {
return false;
}
let dstType = param.type.type;
let dstIsArray = param.type.array;
if (dstType === 'ContextType') {
// ContextType is inferred from the attribute's container's type
if ((0, ast_1.isDataModelField)(attr.$container)) {
// If the field is Typed JSON, and the param is @default, the argument must be a string
const dstIsTypedJson = (0, sdk_1.hasAttribute)(attr.$container, '@json');
if (dstIsTypedJson && param.default) {
return argResolvedType.decl === 'String';
}
dstIsArray = attr.$container.type.array;
}
}
const dstRef = param.type.reference;
if (dstType === 'Any' && !dstIsArray) {
return true;
}
if (argResolvedType.decl === 'Any') {
// arg is any type
if (!argResolvedType.array) {
// if it's not an array, it's assignable to any type
return true;
}
else {
// otherwise it's assignable to any array type
return argResolvedType.array === dstIsArray;
}
}
// destination is field reference or transitive field reference, check if
// argument is reference or array or reference
if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') {
if (dstIsArray) {
return ((0, ast_1.isArrayExpr)(arg.value) &&
!arg.value.items.find((item) => !(0, ast_1.isReferenceExpr)(item) || !(0, ast_1.isDataModelField)(item.target.ref)));
}
else {
return (0, ast_1.isReferenceExpr)(arg.value) && (0, ast_1.isDataModelField)(arg.value.target.ref);
}
}
if ((0, ast_1.isEnum)(argResolvedType.decl)) {
// enum type
let attrArgDeclType = dstRef === null || dstRef === void 0 ? void 0 : dstRef.ref;
if (dstType === 'ContextType' && (0, ast_1.isDataModelField)(attr.$container) && ((_b = (_a = attr.$container) === null || _a === void 0 ? void 0 : _a.type) === null || _b === void 0 ? void 0 : _b.reference)) {
// attribute parameter type is ContextType, need to infer type from
// the attribute's container
attrArgDeclType = (0, sdk_1.resolved)(attr.$container.type.reference);
dstIsArray = attr.$container.type.array;
}
return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array;
}
else if (dstType) {
// scalar type
if (typeof (argResolvedType === null || argResolvedType === void 0 ? void 0 : argResolvedType.decl) !== 'string') {
// destination type is not a reference, so argument type must be a plain expression
return false;
}
if (dstType === 'ContextType') {
// attribute parameter type is ContextType, need to infer type from
// the attribute's container
if ((0, ast_1.isDataModelField)(attr.$container)) {
if (!((_d = (_c = attr.$container) === null || _c === void 0 ? void 0 : _c.type) === null || _d === void 0 ? void 0 : _d.type)) {
return false;
}
dstType = (0, utils_1.mapBuiltinTypeToExpressionType)(attr.$container.type.type);
dstIsArray = attr.$container.type.array;
}
else {
dstType = 'Any';
}
}
return (0, utils_1.typeAssignable)(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array;
}
else {
// reference type
return ((dstRef === null || dstRef === void 0 ? void 0 : dstRef.ref) === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array;
}
}
function isValidAttributeTarget(attrDecl, targetDecl) {
var _a, _b;
const targetField = attrDecl.attributes.find((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === '@@@targetField'; });
if (!targetField) {
// no field type constraint
return true;
}
const fieldTypes = targetField.args[0].value.items.map((item) => { var _a; return (_a = item.target.ref) === null || _a === void 0 ? void 0 : _a.name; });
let allowed = false;
for (const allowedType of fieldTypes) {
switch (allowedType) {
case 'StringField':
allowed = allowed || targetDecl.type.type === 'String';
break;
case 'IntField':
allowed = allowed || targetDecl.type.type === 'Int';
break;
case 'BigIntField':
allowed = allowed || targetDecl.type.type === 'BigInt';
break;
case 'FloatField':
allowed = allowed || targetDecl.type.type === 'Float';
break;
case 'DecimalField':
allowed = allowed || targetDecl.type.type === 'Decimal';
break;
case 'BooleanField':
allowed = allowed || targetDecl.type.type === 'Boolean';
break;
case 'DateTimeField':
allowed = allowed || targetDecl.type.type === 'DateTime';
break;
case 'JsonField':
allowed = allowed || targetDecl.type.type === 'Json';
break;
case 'BytesField':
allowed = allowed || targetDecl.type.type === 'Bytes';
break;
case 'ModelField':
allowed = allowed || (0, ast_1.isDataModel)((_a = targetDecl.type.reference) === null || _a === void 0 ? void 0 : _a.ref);
break;
case 'TypeDefField':
allowed = allowed || (0, ast_1.isTypeDef)((_b = targetDecl.type.reference) === null || _b === void 0 ? void 0 : _b.ref);
break;
default:
break;
}
if (allowed) {
break;
}
}
return allowed;
}
function validateAttributeApplication(attr, accept) {
new AttributeApplicationValidator().validate(attr, accept);
}
//# sourceMappingURL=attribute-application-validator.js.map
;