@mikro-orm/core
Version:
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.
217 lines (216 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MetadataValidator = void 0;
const utils_1 = require("../utils");
const errors_1 = require("../errors");
const enums_1 = require("../enums");
/**
* @internal
*/
class MetadataValidator {
/**
* Validate there is only one property decorator. This disallows using `@Property()` together with e.g. `@ManyToOne()`
* on the same property. One should use only `@ManyToOne()` in such case.
* We allow the existence of the property in metadata if the reference type is the same, this should allow things like HMR to work.
*/
static validateSingleDecorator(meta, propertyName, reference) {
if (meta.properties[propertyName] && meta.properties[propertyName].kind !== reference) {
throw errors_1.MetadataError.multipleDecorators(meta.className, propertyName);
}
}
validateEntityDefinition(metadata, name, options) {
const meta = metadata.get(name);
if (meta.virtual || meta.expression) {
for (const prop of utils_1.Utils.values(meta.properties)) {
if (![enums_1.ReferenceKind.SCALAR, enums_1.ReferenceKind.EMBEDDED, enums_1.ReferenceKind.MANY_TO_ONE, enums_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
throw new errors_1.MetadataError(`Only scalars, embedded properties and to-many relations are allowed inside virtual entity. Found '${prop.kind}' in ${meta.className}.${prop.name}`);
}
if (prop.primary) {
throw new errors_1.MetadataError(`Virtual entity ${meta.className} cannot have primary key ${meta.className}.${prop.name}`);
}
}
return;
}
// entities have PK
if (!meta.embeddable && (!meta.primaryKeys || meta.primaryKeys.length === 0)) {
throw errors_1.MetadataError.fromMissingPrimaryKey(meta);
}
this.validateVersionField(meta);
this.validateDuplicateFieldNames(meta, options);
this.validateIndexes(meta, meta.indexes ?? [], 'index');
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
for (const prop of utils_1.Utils.values(meta.properties)) {
if (prop.kind !== enums_1.ReferenceKind.SCALAR) {
this.validateReference(meta, prop, metadata, options);
this.validateBidirectional(meta, prop, metadata);
}
else if (metadata.has(prop.type)) {
throw errors_1.MetadataError.propertyTargetsEntityType(meta, prop, metadata.get(prop.type));
}
}
}
validateDiscovered(discovered, options) {
if (discovered.length === 0 && options.warnWhenNoEntities) {
throw errors_1.MetadataError.noEntityDiscovered();
}
const duplicates = utils_1.Utils.findDuplicates(discovered.map(meta => meta.className));
if (duplicates.length > 0 && options.checkDuplicateEntities) {
throw errors_1.MetadataError.duplicateEntityDiscovered(duplicates);
}
const tableNames = discovered.filter(meta => !meta.abstract && meta === meta.root && (meta.tableName || meta.collection) && meta.schema !== '*');
const duplicateTableNames = utils_1.Utils.findDuplicates(tableNames.map(meta => {
const tableName = meta.tableName || meta.collection;
return (meta.schema ? '.' + meta.schema : '') + tableName;
}));
if (duplicateTableNames.length > 0 && options.checkDuplicateTableNames && options.checkDuplicateEntities) {
throw errors_1.MetadataError.duplicateEntityDiscovered(duplicateTableNames, 'table names');
}
// validate we found at least one entity (not just abstract/base entities)
if (discovered.filter(meta => meta.name).length === 0 && options.warnWhenNoEntities) {
throw errors_1.MetadataError.onlyAbstractEntitiesDiscovered();
}
const unwrap = (type) => type
.replace(/Array<(.*)>/, '$1') // unwrap array
.replace(/\[]$/, '') // remove array suffix
.replace(/\((.*)\)/, '$1'); // unwrap union types
const name = (p) => {
if (typeof p === 'function') {
return utils_1.Utils.className(p());
}
return utils_1.Utils.className(p);
};
const pivotProps = new Map();
// check for not discovered entities
discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
if (prop.kind !== enums_1.ReferenceKind.SCALAR && !unwrap(prop.type).split(/ ?\| ?/).every(type => discovered.find(m => m.className === type))) {
throw errors_1.MetadataError.fromUnknownEntity(prop.type, `${meta.className}.${prop.name}`);
}
if (prop.pivotEntity) {
const props = pivotProps.get(name(prop.pivotEntity)) ?? [];
props.push({ meta, prop });
pivotProps.set(name(prop.pivotEntity), props);
}
}));
pivotProps.forEach(props => {
// if the pivot entity is used in more than one property, check if they are linked
if (props.length > 1 && props.every(p => !p.prop.mappedBy && !p.prop.inversedBy)) {
throw errors_1.MetadataError.invalidManyToManyWithPivotEntity(props[0].meta, props[0].prop, props[1].meta, props[1].prop);
}
});
}
validateReference(meta, prop, metadata, options) {
// references do have types
if (!prop.type) {
throw errors_1.MetadataError.fromWrongTypeDefinition(meta, prop);
}
const targetMeta = metadata.find(prop.type);
// references do have type of known entity
if (!targetMeta) {
throw errors_1.MetadataError.fromWrongTypeDefinition(meta, prop);
}
if (targetMeta.abstract && !targetMeta.discriminatorColumn && !targetMeta.embeddable) {
throw errors_1.MetadataError.targetIsAbstract(meta, prop);
}
if ([enums_1.ReferenceKind.MANY_TO_ONE, enums_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.persist === false && targetMeta.compositePK && options.checkNonPersistentCompositeProps) {
throw errors_1.MetadataError.nonPersistentCompositeProp(meta, prop);
}
}
validateBidirectional(meta, prop, metadata) {
if (prop.inversedBy) {
const inverse = metadata.get(prop.type).properties[prop.inversedBy];
this.validateOwningSide(meta, prop, inverse, metadata);
}
else if (prop.mappedBy) {
const inverse = metadata.get(prop.type).properties[prop.mappedBy];
this.validateInverseSide(meta, prop, inverse, metadata);
}
else {
// 1:m property has `mappedBy`
if (prop.kind === enums_1.ReferenceKind.ONE_TO_MANY && !prop.mappedBy) {
throw errors_1.MetadataError.fromMissingOption(meta, prop, 'mappedBy');
}
}
}
validateOwningSide(meta, prop, inverse, metadata) {
// has correct `inversedBy` on owning side
if (!inverse) {
throw errors_1.MetadataError.fromWrongReference(meta, prop, 'inversedBy');
}
const targetClassName = metadata.find(inverse.type)?.root.className;
// has correct `inversedBy` reference type
if (inverse.type !== meta.className && targetClassName !== meta.root.className) {
throw errors_1.MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
}
// inverse side is not defined as owner
if (inverse.inversedBy || inverse.owner) {
throw errors_1.MetadataError.fromWrongOwnership(meta, prop, 'inversedBy');
}
}
validateInverseSide(meta, prop, owner, metadata) {
// has correct `mappedBy` on inverse side
if (prop.mappedBy && !owner) {
throw errors_1.MetadataError.fromWrongReference(meta, prop, 'mappedBy');
}
// has correct `mappedBy` reference type
if (owner.type !== meta.className && metadata.find(owner.type)?.root.className !== meta.root.className) {
throw errors_1.MetadataError.fromWrongReference(meta, prop, 'mappedBy', owner);
}
// owning side is not defined as inverse
if (owner.mappedBy) {
throw errors_1.MetadataError.fromWrongOwnership(meta, prop, 'mappedBy');
}
// owning side is not defined as inverse
const valid = [
{ owner: enums_1.ReferenceKind.MANY_TO_ONE, inverse: enums_1.ReferenceKind.ONE_TO_MANY },
{ owner: enums_1.ReferenceKind.MANY_TO_MANY, inverse: enums_1.ReferenceKind.MANY_TO_MANY },
{ owner: enums_1.ReferenceKind.ONE_TO_ONE, inverse: enums_1.ReferenceKind.ONE_TO_ONE },
];
if (!valid.find(spec => spec.owner === owner.kind && spec.inverse === prop.kind)) {
throw errors_1.MetadataError.fromWrongReferenceKind(meta, owner, prop);
}
if (prop.primary) {
throw errors_1.MetadataError.fromInversideSidePrimary(meta, owner, prop);
}
}
validateIndexes(meta, indexes, type) {
for (const index of indexes) {
for (const propName of utils_1.Utils.asArray(index.properties)) {
const prop = meta.root.properties[propName];
if (!prop && !Object.values(meta.root.properties).some(p => propName.startsWith(p.name + '.'))) {
throw errors_1.MetadataError.unknownIndexProperty(meta, propName, type);
}
}
}
}
validateDuplicateFieldNames(meta, options) {
const candidates = Object.values(meta.properties)
.filter(prop => prop.persist !== false && !prop.inherited && prop.fieldNames?.length === 1 && (prop.kind !== enums_1.ReferenceKind.EMBEDDED || prop.object))
.map(prop => prop.fieldNames[0]);
const duplicates = utils_1.Utils.findDuplicates(candidates);
if (duplicates.length > 0 && options.checkDuplicateFieldNames) {
const pairs = duplicates.flatMap(name => {
return Object.values(meta.properties)
.filter(p => p.fieldNames?.[0] === name)
.map(prop => {
return [prop.embedded ? prop.embedded.join('.') : prop.name, prop.fieldNames[0]];
});
});
throw errors_1.MetadataError.duplicateFieldName(meta.className, pairs);
}
}
validateVersionField(meta) {
if (!meta.versionProperty) {
return;
}
const props = Object.values(meta.properties).filter(p => p.version);
if (props.length > 1) {
throw errors_1.MetadataError.multipleVersionFields(meta, props.map(p => p.name));
}
const prop = meta.properties[meta.versionProperty];
const type = prop.runtimeType ?? prop.columnTypes?.[0] ?? prop.type;
if (type !== 'number' && type !== 'Date' && !type.startsWith('timestamp') && !type.startsWith('datetime')) {
throw errors_1.MetadataError.invalidVersionFieldType(meta);
}
}
}
exports.MetadataValidator = MetadataValidator;