UNPKG

zenstack

Version:

FullStack enhancement for Prisma ORM: seamless integration from database to UI

350 lines 15.9 kB
"use strict"; 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