@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.
621 lines (620 loc) • 31.4 kB
JavaScript
import { Utils } from '../utils/Utils.js';
import { normalizePartitionNameForComparison, splitCommaSeparatedIdentifiers } from '../utils/partition-utils.js';
import { MetadataError } from '../errors.js';
import { ReferenceKind } from '../enums.js';
/**
* List of property names that could lead to prototype pollution vulnerabilities.
* These names should never be used as entity property names because they could
* allow malicious code to modify object prototypes when property values are assigned.
*
* - `__proto__`: Could modify the prototype chain
* - `constructor`: Could modify the constructor property
* - `prototype`: Could modify the prototype object
*
* @internal
*/
const DANGEROUS_PROPERTY_NAMES = ['__proto__', 'constructor', 'prototype'];
/**
* @internal
*/
export class MetadataValidator {
validateEntityDefinition(metadata, name, options) {
const meta = metadata.get(name);
// View entities (expression with view flag) behave like regular tables but are read-only
// They can have primary keys and are created as actual database views
if (meta.view) {
this.validateViewEntity(meta);
return;
}
// Virtual entities (expression without view flag) have restrictions - no PKs, limited relation types
// Note: meta.virtual is set later in sync(), so we check for expression && !view here
if (meta.virtual || (meta.expression && !meta.view)) {
if (meta.partitionBy) {
throw new MetadataError(`Virtual entity ${meta.className} cannot define partitionBy`);
}
for (const prop of Utils.values(meta.properties)) {
if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED, ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
throw new 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 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 MetadataError.fromMissingPrimaryKey(meta);
}
this.validateVersionField(meta);
this.validateDuplicateFieldNames(meta, options);
this.validateIndexes(meta, meta.indexes ?? [], 'index');
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
this.validatePartitioning(meta);
this.validatePropertyNames(meta);
for (const prop of Utils.values(meta.properties)) {
if (prop.kind !== ReferenceKind.SCALAR) {
this.validateReference(meta, prop, options);
this.validateBidirectional(meta, prop);
}
else if (metadata.getByClassName(prop.type, false)) {
throw MetadataError.propertyTargetsEntityType(meta, prop, metadata.getByClassName(prop.type));
}
}
}
validateRoutineDefinition(routine) {
if (typeof routine.name !== 'string' || routine.name.trim() === '') {
throw new MetadataError(`Routine is missing the required 'name' option.`);
}
if (!routine.type) {
throw new MetadataError(`Routine ${routine.name} is missing the required 'type' option ('procedure' | 'function').`);
}
if (routine.body != null && routine.expression != null) {
throw new MetadataError(`Routine ${routine.name} defines both 'body' and 'expression'. Use one or the other.`);
}
if (routine.body == null && routine.expression == null && routine.bodyJs == null) {
throw new MetadataError(`Routine ${routine.name} must define a 'body', 'expression', or 'bodyJs'.`);
}
if (routine.type === 'function' && routine.returns == null) {
throw new MetadataError(`Function routine ${routine.name} must declare a 'returns' option.`);
}
if (routine.type === 'procedure' && routine.bodyJs != null) {
throw new MetadataError(`Routine ${routine.name} declares 'bodyJs' on a procedure. JS fallbacks are only supported for functions — SQLite has no analog for stored procedures.`);
}
for (const param of routine.params) {
const dir = param.direction;
if (dir !== 'in' && dir !== 'out' && dir !== 'inout') {
throw new MetadataError(`Routine ${routine.name}.${param.name} has invalid direction '${dir}'. Expected 'in', 'out', or 'inout'.`);
}
if ((dir === 'out' || dir === 'inout') && !param.ref) {
throw new MetadataError(`Routine ${routine.name}.${param.name} is declared as '${dir}' but missing 'ref: true'. OUT/INOUT parameters must be passed as ScalarReference.`);
}
if (dir === 'in' && param.ref) {
throw new MetadataError(`Routine ${routine.name}.${param.name} declares 'ref: true' on an IN parameter. ScalarReference wrapping is only meaningful for OUT/INOUT parameters.`);
}
if (routine.type === 'function' && dir !== 'in') {
throw new MetadataError(`Function routine ${routine.name}.${param.name} declares direction '${dir}'. Functions only support IN parameters — use a procedure for OUT/INOUT semantics.`);
}
}
}
validateDiscovered(discovered, options) {
if (discovered.length === 0 && options.warnWhenNoEntities) {
throw MetadataError.noEntityDiscovered();
}
// Validate no mixing of STI and TPT in the same hierarchy
this.validateInheritanceStrategies(discovered);
const tableNames = discovered.filter(meta => !meta.abstract &&
!meta.embeddable &&
meta === meta.root &&
(meta.tableName || meta.collection) &&
meta.schema !== '*');
const duplicateTableNames = Utils.findDuplicates(tableNames.map(meta => {
const tableName = meta.tableName || meta.collection;
return (meta.schema ? '.' + meta.schema : '') + tableName;
}));
if (duplicateTableNames.length > 0 && options.checkDuplicateTableNames) {
throw MetadataError.duplicateEntityDiscovered(duplicateTableNames);
}
// validate we found at least one entity (not just abstract/base entities)
if (discovered.filter(meta => meta.name).length === 0 && options.warnWhenNoEntities) {
throw 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' && !p.prototype) {
return Utils.className(p());
}
return Utils.className(p);
};
const pivotProps = new Map();
// check for not discovered entities
discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
if (prop.kind !== ReferenceKind.SCALAR &&
!unwrap(prop.type)
.split(/ ?\| ?/)
.every(type => discovered.find(m => m.className === type))) {
throw 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 MetadataError.invalidManyToManyWithPivotEntity(props[0].meta, props[0].prop, props[1].meta, props[1].prop);
}
});
}
validatePartitioning(meta) {
if (!meta.partitionBy) {
return;
}
if (!this.hasPartitionExpression(meta.partitionBy.expression)) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: missing expression`);
}
// Inheritance (STI/TPT) and partitioning both drive table layout, so combining them would
// require non-trivial DDL coordination that the schema generator does not produce today.
const hasInheritance = !!meta.root.discriminatorColumn || meta.root.inheritanceType === 'tpt' || meta.root.inheritance === 'tpt';
if (hasInheritance) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: combining partitioning with inheritance is not supported`);
}
this.validatePartitionKeyConstraints(meta);
if (meta.partitionBy.type === 'hash') {
const { partitions } = meta.partitionBy;
if (Array.isArray(partitions)) {
if (partitions.length === 0) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition name list must not be empty`);
}
const blank = partitions.find(name => typeof name !== 'string' || !name.trim());
if (blank !== undefined) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition names must be non-empty strings`);
}
const ambiguous = partitions.find(name => !this.hasValidPartitionName(name));
if (ambiguous) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguous}' contains more than one '.' — use at most one '.' to separate schema from table`);
}
const duplicate = this.findDuplicatePartitionName(partitions);
if (duplicate !== undefined) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate hash partition name '${duplicate}'`);
}
return;
}
if (typeof partitions !== 'number' || !Number.isInteger(partitions) || partitions < 1) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition count must be a positive integer`);
}
return;
}
if (!Array.isArray(meta.partitionBy.partitions) || meta.partitionBy.partitions.length === 0) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: list/range partitions must be a non-empty array`);
}
if (meta.partitionBy.partitions.some(partition => !partition.values?.trim())) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: every partition must define values`);
}
const ambiguousName = meta.partitionBy.partitions.find(partition => partition.name != null && !this.hasValidPartitionName(partition.name));
if (ambiguousName) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguousName.name}' contains more than one '.' — use at most one '.' to separate schema from table`);
}
// Include auto-generated default names (`${tableName}_${index}`, matching
// `createExplicitPartitions` in the sql package) so an explicit name that collides with
// an unnamed peer's default is caught here rather than at DDL execution time.
const resolvedNames = meta.partitionBy.partitions.map((partition, index) => partition.name ?? `${meta.tableName}_${index}`);
const duplicate = this.findDuplicatePartitionName(resolvedNames);
if (duplicate !== undefined) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate partition name '${duplicate}'`);
}
}
/**
* Find the first partition name whose normalized form (case-folded for unquoted segments,
* quoted segments preserved) has already been seen. Returns the offending name in its
* original form for the error message.
*/
findDuplicatePartitionName(names) {
const seen = new Set();
for (const name of names) {
const normalized = normalizePartitionNameForComparison(name);
if (seen.has(normalized)) {
return name;
}
seen.add(normalized);
}
return undefined;
}
/**
* Partition names may be bare (`child`), schema-qualified (`schema.child`), or use quoted
* identifiers (`"my.schema"."child"`). Reject anything with more than one unquoted `.`.
*/
hasValidPartitionName(name) {
let depth = 0;
let dots = 0;
for (let i = 0; i < name.length; i++) {
const ch = name[i];
if (ch === '"') {
if (name[i + 1] === '"') {
i++;
continue;
}
depth = depth === 0 ? 1 : 0;
continue;
}
if (ch === '.' && depth === 0) {
dots++;
}
}
return dots <= 1;
}
hasPartitionExpression(expression) {
if (expression == null) {
return false;
}
if (typeof expression === 'function') {
return true;
}
if (Array.isArray(expression)) {
return expression.length > 0 && expression.every(key => typeof key === 'string' && key.trim().length > 0);
}
return String(expression).trim().length > 0;
}
validatePartitionKeyConstraints(meta) {
const partitionFields = this.getPartitionKeyFields(meta);
if (!partitionFields?.length) {
return;
}
const primaryKeyFields = meta.root.getPrimaryProps().flatMap(prop => prop.fieldNames);
if (partitionFields.some(field => !primaryKeyFields.includes(field))) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: primary key must include partition key columns '${partitionFields.join("', '")}'`);
}
for (const prop of Object.values(meta.root.properties)) {
if (!prop.unique || !prop.fieldNames?.length) {
continue;
}
if (partitionFields.some(field => !prop.fieldNames.includes(field))) {
throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: unique property ${meta.root.className}.${prop.name} must include partition key columns '${partitionFields.join("', '")}'`);
}
}
for (const unique of meta.root.uniques ?? []) {
const fields = this.getConstraintFields(meta.root, unique.properties);
if (!fields?.length) {
continue;
}
if (partitionFields.some(field => !fields.includes(field))) {
const constraint = unique.name ? `unique constraint '${unique.name}'` : 'unique constraint';
throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: ${constraint} must include partition key columns '${partitionFields.join("', '")}'`);
}
}
}
/**
* Returns the list of physical field names that a partition expression references, or
* `undefined` when the expression is opaque (callback, or raw SQL like `date_trunc('day', x)`
* that we cannot statically parse). Opaque expressions intentionally bypass the primary-key /
* unique-constraint coverage checks — users are trusted to ensure the referenced columns are
* part of the partition key, since PostgreSQL will surface the violation at DDL execution.
*/
getPartitionKeyFields(meta) {
const expression = meta.partitionBy?.expression;
if (!expression || typeof expression === 'function') {
return undefined;
}
if (Array.isArray(expression)) {
return expression.map(key => this.resolvePartitionKeyField(meta, key));
}
const keys = splitCommaSeparatedIdentifiers(String(expression).trim());
if (!keys) {
return undefined;
}
return keys.map(key => this.resolvePartitionKeyField(meta, key));
}
resolvePartitionKeyField(meta, key) {
const trimmed = key.trim().replaceAll('"', '');
if (!trimmed) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: empty partition key`);
}
const prop = meta.root.properties[trimmed] ??
Object.values(meta.root.properties).find(candidate => candidate.fieldNames?.length === 1 && candidate.fieldNames[0] === trimmed);
if (!prop) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: unknown partition key '${key.trim()}'`);
}
if (prop.fieldNames?.length !== 1) {
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition key '${key.trim()}' maps to multiple columns ('${prop.fieldNames?.join("', '")}'); list them explicitly as partition keys`);
}
return prop.fieldNames[0];
}
getConstraintFields(meta, properties) {
if (!properties) {
return undefined;
}
const fields = Utils.asArray(properties).flatMap(propName => {
const prop = meta.root.properties[propName];
return prop?.fieldNames ?? [];
});
return fields.length > 0 ? fields : undefined;
}
validateReference(meta, prop, options) {
// references do have types
if (!prop.type) {
throw MetadataError.fromWrongTypeDefinition(meta, prop);
}
// Polymorphic relations have multiple targets, validate PK compatibility
if (prop.polymorphic && prop.polymorphTargets) {
this.validatePolymorphicTargets(meta, prop);
return;
}
const targetMeta = prop.targetMeta;
// references do have type of known entity
if (!targetMeta) {
throw MetadataError.fromWrongTypeDefinition(meta, prop);
}
if (targetMeta.abstract && !targetMeta.root?.inheritanceType && !targetMeta.embeddable) {
throw MetadataError.targetIsAbstract(meta, prop);
}
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
prop.persist === false &&
targetMeta.compositePK &&
options.checkNonPersistentCompositeProps) {
throw MetadataError.nonPersistentCompositeProp(meta, prop);
}
this.validateTargetKey(meta, prop, targetMeta);
}
validateTargetKey(meta, prop, targetMeta) {
if (!prop.targetKey) {
return;
}
// targetKey is not supported for ManyToMany relations
if (prop.kind === ReferenceKind.MANY_TO_MANY) {
throw MetadataError.targetKeyOnManyToMany(meta, prop);
}
// targetKey must point to an existing property
const targetProp = targetMeta.properties[prop.targetKey];
if (!targetProp) {
throw MetadataError.targetKeyNotFound(meta, prop);
}
// targetKey must point to a unique property (composite unique is not sufficient)
if (!this.isPropertyUnique(targetProp, targetMeta)) {
throw MetadataError.targetKeyNotUnique(meta, prop);
}
}
/**
* Checks if a property has a unique constraint (either via `unique: true` or single-property `@Unique` decorator).
* Composite unique constraints are not sufficient for targetKey.
*/
isPropertyUnique(prop, meta) {
if (prop.unique || (prop.primary && meta.primaryKeys.length === 1)) {
return true;
}
// Check for single-property unique constraint via @Unique decorator
return !!meta.uniques?.some(u => {
const props = Utils.asArray(u.properties);
return props.length === 1 && props[0] === prop.name && !u.options;
});
}
validatePolymorphicTargets(meta, prop) {
const targets = prop.polymorphTargets;
// Union-target M:N stores one scalar target FK per pivot row, so composite-PK targets
// can't round-trip through this schema.
if (prop.kind === ReferenceKind.MANY_TO_MANY && targets.length > 1) {
for (const target of targets) {
if (target.compositePK) {
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `${target.className} has a composite primary key; union-target polymorphic M:N does not support composite-PK targets.`);
}
}
}
// Validate targetKey exists and is compatible across all targets
if (prop.targetKey) {
for (const target of targets) {
const targetProp = target.properties[prop.targetKey];
if (!targetProp) {
throw MetadataError.targetKeyNotFound(meta, prop, target);
}
// targetKey must point to a unique property (composite unique is not sufficient)
if (!this.isPropertyUnique(targetProp, target)) {
throw MetadataError.targetKeyNotUnique(meta, prop, target);
}
}
}
const firstPKs = targets[0].getPrimaryProps();
for (let i = 1; i < targets.length; i++) {
const target = targets[i];
const targetPKs = target.getPrimaryProps();
if (targetPKs.length !== firstPKs.length) {
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, 'different number of primary keys');
}
for (let j = 0; j < firstPKs.length; j++) {
const firstPK = firstPKs[j];
const targetPK = targetPKs[j];
if (firstPK.runtimeType !== targetPK.runtimeType) {
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `incompatible primary key types: ${firstPK.name} (${firstPK.runtimeType}) vs ${targetPK.name} (${targetPK.runtimeType})`);
}
}
}
}
validateBidirectional(meta, prop) {
if (prop.inversedBy) {
this.validateOwningSide(meta, prop);
}
else if (prop.mappedBy) {
this.validateInverseSide(meta, prop);
}
else if (prop.kind === ReferenceKind.ONE_TO_MANY && !prop.mappedBy) {
// 1:m property has `mappedBy`
throw MetadataError.fromMissingOption(meta, prop, 'mappedBy');
}
}
validateOwningSide(meta, prop) {
// For polymorphic relations, inversedBy may point to multiple entity types
if (prop.polymorphic && prop.polymorphTargets?.length) {
// For polymorphic relations, validate inversedBy against each target
// The inverse property should exist on the target entities and reference back to this property
for (const targetMeta of prop.polymorphTargets) {
const inverse = targetMeta.properties[prop.inversedBy];
// The inverse property is optional - some targets may not have it
if (!inverse) {
continue;
}
// Validate the inverse property
if (inverse.targetMeta?.root.class !== meta.root.class) {
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
}
// inverse side is not defined as owner
if (inverse.inversedBy || inverse.owner) {
throw MetadataError.fromWrongOwnership(meta, prop, 'inversedBy');
}
}
return;
}
const inverse = prop.targetMeta.properties[prop.inversedBy];
// has correct `inversedBy` on owning side
if (!inverse) {
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy');
}
const targetClass = inverse.targetMeta?.root.class;
// has correct `inversedBy` reference type
if (inverse.type !== meta.className && targetClass !== meta.root.class) {
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
}
// inverse side is not defined as owner
if (inverse.inversedBy || inverse.owner) {
throw MetadataError.fromWrongOwnership(meta, prop, 'inversedBy');
}
}
validateInverseSide(meta, prop) {
const owner = prop.targetMeta.properties[prop.mappedBy];
// has correct `mappedBy` on inverse side
if (prop.mappedBy && !owner) {
throw MetadataError.fromWrongReference(meta, prop, 'mappedBy');
}
// has correct `mappedBy` reference type
// For polymorphic relations, check if this entity is one of the polymorphic targets
const isValidPolymorphicInverse = owner.polymorphic && owner.polymorphTargets?.some(target => target.class === meta.root.class);
if (!isValidPolymorphicInverse &&
owner.type !== meta.className &&
owner.targetMeta?.root.class !== meta.root.class) {
throw MetadataError.fromWrongReference(meta, prop, 'mappedBy', owner);
}
// owning side is not defined as inverse
if (owner.mappedBy) {
throw MetadataError.fromWrongOwnership(meta, prop, 'mappedBy');
}
// owning side is not defined as inverse
const valid = [
{ owner: ReferenceKind.MANY_TO_ONE, inverse: ReferenceKind.ONE_TO_MANY },
{ owner: ReferenceKind.MANY_TO_MANY, inverse: ReferenceKind.MANY_TO_MANY },
{ owner: ReferenceKind.ONE_TO_ONE, inverse: ReferenceKind.ONE_TO_ONE },
];
if (!valid.find(spec => spec.owner === owner.kind && spec.inverse === prop.kind)) {
throw MetadataError.fromWrongReferenceKind(meta, owner, prop);
}
if (prop.primary) {
throw MetadataError.fromInversideSidePrimary(meta, owner, prop);
}
}
validateIndexes(meta, indexes, type) {
for (const index of indexes) {
for (const propName of Utils.asArray(index.properties)) {
const prop = meta.properties[propName] ?? meta.root.properties[propName];
if (!prop &&
!Object.values(meta.properties).some(p => propName.startsWith(p.name + '.')) &&
!Object.values(meta.root.properties).some(p => propName.startsWith(p.name + '.'))) {
throw 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 !== ReferenceKind.EMBEDDED || prop.object))
.map(prop => prop.fieldNames[0]);
const duplicates = 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 MetadataError.duplicateFieldName(meta.class, pairs);
}
}
validateVersionField(meta) {
if (!meta.versionProperty) {
return;
}
const props = Object.values(meta.properties).filter(p => p.version);
if (props.length > 1) {
throw 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 MetadataError.invalidVersionFieldType(meta);
}
}
/**
* Validates that entity properties do not use dangerous names that could lead to
* prototype pollution vulnerabilities. This validation ensures that property names
* cannot be exploited to modify object prototypes when values are assigned during
* entity hydration or persistence operations.
*
* @internal
*/
validatePropertyNames(meta) {
for (const prop of Utils.values(meta.properties)) {
if (DANGEROUS_PROPERTY_NAMES.includes(prop.name)) {
throw MetadataError.dangerousPropertyName(meta, prop);
}
}
}
/**
* Validates view entity configuration.
* View entities must have an expression.
*/
validateViewEntity(meta) {
// View entities must have an expression
if (!meta.expression) {
throw MetadataError.viewEntityWithoutExpression(meta);
}
// Views are not partitionable - reject explicitly instead of silently ignoring
if (meta.partitionBy) {
throw new MetadataError(`View entity ${meta.className} cannot define partitionBy`);
}
// Validate indexes if present
this.validateIndexes(meta, meta.indexes ?? [], 'index');
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
// Validate property names
this.validatePropertyNames(meta);
}
/**
* Validates that STI and TPT are not mixed in the same inheritance hierarchy.
* An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
* but not both.
*
* Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
* so we check the raw `inheritance` option from the decorator/schema.
*/
validateInheritanceStrategies(discovered) {
const checkedRoots = new Set();
for (const meta of discovered) {
if (meta.embeddable) {
continue;
}
const root = meta.root;
if (checkedRoots.has(root)) {
continue;
}
checkedRoots.add(root);
const hasSTI = !!root.discriminatorColumn;
const hasTPT = root.inheritanceType === 'tpt' || root.inheritance === 'tpt';
if (hasSTI && hasTPT) {
throw MetadataError.mixedInheritanceStrategies(root, meta);
}
}
}
}