UNPKG

zenstack

Version:

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

355 lines 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ast_1 = require("@zenstackhq/language/ast"); const sdk_1 = require("@zenstackhq/sdk"); const langium_1 = require("langium"); const ast_utils_1 = require("../../utils/ast-utils"); const constants_1 = require("../constants"); const utils_1 = require("../utils"); const attribute_application_validator_1 = require("./attribute-application-validator"); const utils_2 = require("./utils"); /** * Validates data model declarations. */ class DataModelValidator { validate(dm, accept) { this.validateBaseAbstractModel(dm, accept); this.validateBaseDelegateModel(dm, accept); (0, utils_2.validateDuplicatedDeclarations)(dm, (0, sdk_1.getModelFieldsWithBases)(dm), accept); this.validateAttributes(dm, accept); this.validateFields(dm, accept); if (dm.superTypes.length > 0) { this.validateInheritance(dm, accept); } } validateFields(dm, accept) { const allFields = (0, sdk_1.getModelFieldsWithBases)(dm); const idFields = allFields.filter((f) => f.attributes.find((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === '@id'; })); const uniqueFields = allFields.filter((f) => f.attributes.find((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === '@unique'; })); const modelLevelIds = (0, sdk_1.getModelIdFields)(dm); const modelUniqueFields = (0, sdk_1.getModelUniqueFields)(dm); if (!dm.isAbstract && idFields.length === 0 && modelLevelIds.length === 0 && uniqueFields.length === 0 && modelUniqueFields.length === 0) { accept('error', 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.', { node: dm, }); } else if (idFields.length > 0 && modelLevelIds.length > 0) { accept('error', 'Model cannot have both field-level @id and model-level @@id attributes', { node: dm, }); } else if (idFields.length > 1) { accept('error', 'Model can include at most one field with @id attribute', { node: dm, }); } else { const fieldsToCheck = idFields.length > 0 ? idFields : modelLevelIds; fieldsToCheck.forEach((idField) => { var _a; if (idField.type.optional) { accept('error', 'Field with @id attribute must not be optional', { node: idField }); } const isArray = idField.type.array; const isScalar = constants_1.SCALAR_TYPES.includes(idField.type.type); const isValidType = isScalar || (0, ast_1.isEnum)((_a = idField.type.reference) === null || _a === void 0 ? void 0 : _a.ref); if (isArray || !isValidType) { accept('error', 'Field with @id attribute must be of scalar or enum type', { node: idField }); } }); } dm.fields.forEach((field) => this.validateField(field, accept)); if (!dm.isAbstract) { allFields .filter((x) => { var _a; return (0, ast_1.isDataModel)((_a = x.type.reference) === null || _a === void 0 ? void 0 : _a.ref); }) .forEach((y) => { this.validateRelationField(dm, y, accept); }); } } validateField(field, accept) { var _a; if (field.type.array && field.type.optional) { accept('error', 'Optional lists are not supported. Use either `Type[]` or `Type?`', { node: field.type }); } if (field.type.unsupported && !(0, ast_1.isStringLiteral)(field.type.unsupported.value)) { accept('error', 'Unsupported type argument must be a string literal', { node: field.type.unsupported }); } field.attributes.forEach((attr) => (0, attribute_application_validator_1.validateAttributeApplication)(attr, accept)); if ((0, ast_1.isTypeDef)((_a = field.type.reference) === null || _a === void 0 ? void 0 : _a.ref)) { if (!(0, sdk_1.hasAttribute)(field, '@json')) { accept('error', 'Custom-typed field must have @json attribute', { node: field }); } } } validateAttributes(dm, accept) { dm.attributes.forEach((attr) => (0, attribute_application_validator_1.validateAttributeApplication)(attr, accept)); } parseRelation(field, accept) { var _a, _b, _c, _d, _e; const relAttr = field.attributes.find((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === '@relation'; }); let name; let fields; let references; let valid = true; if (!relAttr) { return { attr: relAttr, name, fields, references, valid: true }; } for (const arg of relAttr.args) { if (!arg.name || arg.name === 'name') { if ((0, ast_1.isStringLiteral)(arg.value)) { name = arg.value.value; } } else if (arg.name === 'fields') { fields = arg.value.items; if (fields.length === 0) { if (accept) { accept('error', `"fields" value cannot be empty`, { node: arg, }); } valid = false; } } else if (arg.name === 'references') { references = arg.value.items; if (references.length === 0) { if (accept) { accept('error', `"references" value cannot be empty`, { node: arg, }); } valid = false; } } } if (!fields && !references) { return { attr: relAttr, name, fields, references, valid: true }; } if (!fields || !references) { if (accept) { accept('error', `"fields" and "references" must be provided together`, { node: relAttr }); } } else { // validate "fields" and "references" typing consistency if (fields.length !== references.length) { if (accept) { accept('error', `"references" and "fields" must have the same length`, { node: relAttr }); } } else { for (let i = 0; i < fields.length; i++) { if (!field.type.optional && ((_a = fields[i].$resolvedType) === null || _a === void 0 ? void 0 : _a.nullable)) { // if relation is not optional, then fk field must not be nullable if (accept) { accept('error', `relation "${field.name}" is not optional, but field "${fields[i].target.$refText}" is optional`, { node: fields[i].target.ref }); } } if (!fields[i].$resolvedType) { if (accept) { accept('error', `field reference is unresolved`, { node: fields[i] }); } } if (!references[i].$resolvedType) { if (accept) { accept('error', `field reference is unresolved`, { node: references[i] }); } } if (((_b = fields[i].$resolvedType) === null || _b === void 0 ? void 0 : _b.decl) !== ((_c = references[i].$resolvedType) === null || _c === void 0 ? void 0 : _c.decl) || ((_d = fields[i].$resolvedType) === null || _d === void 0 ? void 0 : _d.array) !== ((_e = references[i].$resolvedType) === null || _e === void 0 ? void 0 : _e.array)) { if (accept) { accept('error', `values of "references" and "fields" must have the same type`, { node: relAttr, }); } } } } } return { attr: relAttr, name, fields, references, valid }; } isSelfRelation(field) { var _a; return ((_a = field.type.reference) === null || _a === void 0 ? void 0 : _a.ref) === field.$container; } validateRelationField(contextModel, field, accept) { var _a, _b, _c, _d, _e; const thisRelation = this.parseRelation(field, accept); if (!thisRelation.valid) { return; } if (this.isFieldInheritedFromDelegateModel(field, contextModel)) { // relation fields inherited from delegate model don't need opposite relation return; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const oppositeModel = field.type.reference.ref; // Use name because the current document might be updated let oppositeFields = (0, sdk_1.getModelFieldsWithBases)(oppositeModel, false).filter((f) => { var _a, _b; return ((_b = (_a = f.type.reference) === null || _a === void 0 ? void 0 : _a.ref) === null || _b === void 0 ? void 0 : _b.name) === contextModel.name; }); oppositeFields = oppositeFields.filter((f) => { const fieldRel = this.parseRelation(f); return fieldRel.valid && fieldRel.name === thisRelation.name; }); if (oppositeFields.length === 0) { const info = { node: field, code: constants_1.IssueCodes.MissingOppositeRelation, }; info.property = 'name'; const container = field.$container; const relationFieldDocUri = (0, langium_1.getDocument)(container).textDocument.uri; const relationDataModelName = container.name; const data = { relationFieldName: field.name, relationDataModelName, relationFieldDocUri, dataModelName: contextModel.name, }; info.data = data; accept('error', `The relation field "${field.name}" on model "${contextModel.name}" is missing an opposite relation field on model "${oppositeModel.name}"`, info); return; } else if (oppositeFields.length > 1) { oppositeFields .filter((f) => f.$container !== contextModel) .forEach((f) => { if (this.isSelfRelation(f)) { // self relations are partial // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations } else { accept('error', `Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${oppositeModel.name}" refer to the same relation to model "${field.$container.name}"`, { node: f }); } }); return; } const oppositeField = oppositeFields[0]; const oppositeRelation = this.parseRelation(oppositeField); let relationOwner; if (((_a = thisRelation === null || thisRelation === void 0 ? void 0 : thisRelation.references) === null || _a === void 0 ? void 0 : _a.length) && ((_b = thisRelation.fields) === null || _b === void 0 ? void 0 : _b.length)) { if ((oppositeRelation === null || oppositeRelation === void 0 ? void 0 : oppositeRelation.references) || (oppositeRelation === null || oppositeRelation === void 0 ? void 0 : oppositeRelation.fields)) { accept('error', '"fields" and "references" must be provided only on one side of relation field', { node: oppositeField, }); return; } else { relationOwner = oppositeField; } } else if (((_c = oppositeRelation === null || oppositeRelation === void 0 ? void 0 : oppositeRelation.references) === null || _c === void 0 ? void 0 : _c.length) && ((_d = oppositeRelation.fields) === null || _d === void 0 ? void 0 : _d.length)) { if ((thisRelation === null || thisRelation === void 0 ? void 0 : thisRelation.references) || (thisRelation === null || thisRelation === void 0 ? void 0 : thisRelation.fields)) { accept('error', '"fields" and "references" must be provided only on one side of relation field', { node: field, }); return; } else { relationOwner = field; } } else { // if both the field is array, then it's an implicit many-to-many relation if (!(field.type.array && oppositeField.type.array)) { [field, oppositeField].forEach((f) => { if (!this.isSelfRelation(f)) { accept('error', 'Field for one side of relation must carry @relation attribute with both "fields" and "references"', { node: f }); } }); } return; } if (!relationOwner.type.array && !relationOwner.type.optional) { accept('error', 'Relation field needs to be list or optional', { node: relationOwner, }); return; } if (relationOwner !== field && !relationOwner.type.array) { // one-to-one relation requires defining side's reference field to be @unique // e.g.: // model User { // id String @id @default(cuid()) // data UserData? // } // model UserData { // id String @id @default(cuid()) // user User @relation(fields: [userId], references: [id]) // userId String // } // // UserData.userId field needs to be @unique const containingModel = field.$container; const uniqueFieldList = (0, utils_1.getUniqueFields)(containingModel); // field is defined in the abstract base model if (containingModel !== contextModel) { uniqueFieldList.push(...(0, utils_1.getUniqueFields)(contextModel)); } (_e = thisRelation.fields) === null || _e === void 0 ? void 0 : _e.forEach((ref) => { const refField = ref.target.ref; if (refField) { if (refField.attributes.find((a) => { var _a, _b; return ((_a = a.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === '@id' || ((_b = a.decl.ref) === null || _b === void 0 ? void 0 : _b.name) === '@unique'; })) { return; } if (uniqueFieldList.some((list) => list.includes(refField))) { return; } accept('error', `Field "${refField.name}" on model "${containingModel.name}" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute`, { node: refField }); } }); } } // checks if the given field is inherited directly or indirectly from a delegate model isFieldInheritedFromDelegateModel(field, contextModel) { const basePath = (0, ast_utils_1.findUpInheritance)(contextModel, field.$container); if (basePath && basePath.some(sdk_1.isDelegateModel)) { return true; } else { return false; } } validateBaseAbstractModel(model, accept) { model.superTypes.forEach((superType, index) => { var _a, _b; if (!((_a = superType.ref) === null || _a === void 0 ? void 0 : _a.isAbstract) && !((_b = superType.ref) === null || _b === void 0 ? void 0 : _b.attributes.some((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === '@@delegate'; }))) accept('error', `Model ${superType.$refText} cannot be extended because it's neither abstract nor marked as "@@delegate"`, { node: model, property: 'superTypes', index, }); }); } validateBaseDelegateModel(model, accept) { if (model.superTypes.filter((base) => base.ref && (0, sdk_1.isDelegateModel)(base.ref)).length > 1) { accept('error', 'Extending from multiple delegate models is not supported', { node: model, property: 'superTypes', }); } } validateInheritance(dm, accept) { const seen = [dm]; const todo = dm.superTypes.map((superType) => superType.ref); while (todo.length > 0) { const current = todo.shift(); if (seen.includes(current)) { accept('error', `Circular inheritance detected: ${seen.map((m) => m.name).join(' -> ')} -> ${current.name}`, { node: dm, }); return; } seen.push(current); todo.push(...current.superTypes.map((superType) => superType.ref)); } } } exports.default = DataModelValidator; //# sourceMappingURL=datamodel-validator.js.map