zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
355 lines • 17.8 kB
JavaScript
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
;