Files
evento/node_modules/@mikro-orm/sql/schema/DatabaseSchema.js
2026-03-18 14:55:56 -03:00

361 lines
13 KiB
JavaScript

import { ReferenceKind, isRaw } from '@mikro-orm/core';
import { DatabaseTable } from './DatabaseTable.js';
/**
* @internal
*/
export class DatabaseSchema {
name;
#tables = [];
#views = [];
#namespaces = new Set();
#nativeEnums = {}; // for postgres
#platform;
constructor(platform, name) {
this.name = name;
this.#platform = platform;
}
addTable(name, schema, comment) {
const namespaceName = schema ?? this.name;
const table = new DatabaseTable(this.#platform, name, namespaceName);
table.nativeEnums = this.#nativeEnums;
table.comment = comment;
this.#tables.push(table);
if (namespaceName != null) {
this.#namespaces.add(namespaceName);
}
return table;
}
getTables() {
return this.#tables;
}
/** @internal */
setTables(tables) {
this.#tables = tables;
}
/** @internal */
setNamespaces(namespaces) {
this.#namespaces = namespaces;
}
getTable(name) {
return this.#tables.find(t => t.name === name || `${t.schema}.${t.name}` === name);
}
hasTable(name) {
return !!this.getTable(name);
}
addView(name, schema, definition, materialized, withData) {
const namespaceName = schema ?? this.name;
const view = { name, schema: namespaceName, definition, materialized, withData };
this.#views.push(view);
if (namespaceName != null) {
this.#namespaces.add(namespaceName);
}
return view;
}
getViews() {
return this.#views;
}
/** @internal */
setViews(views) {
this.#views = views;
}
getView(name) {
return this.#views.find(v => v.name === name || `${v.schema}.${v.name}` === name);
}
hasView(name) {
return !!this.getView(name);
}
setNativeEnums(nativeEnums) {
this.#nativeEnums = nativeEnums;
for (const nativeEnum of Object.values(nativeEnums)) {
if (nativeEnum.schema && nativeEnum.schema !== '*') {
this.#namespaces.add(nativeEnum.schema);
}
}
}
getNativeEnums() {
return this.#nativeEnums;
}
getNativeEnum(name) {
return this.#nativeEnums[name];
}
hasNamespace(namespace) {
return this.#namespaces.has(namespace);
}
hasNativeEnum(name) {
return name in this.#nativeEnums;
}
getNamespaces() {
return [...this.#namespaces];
}
static async create(connection, platform, config, schemaName, schemas, takeTables, skipTables, skipViews) {
const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema') ?? platform.getDefaultSchemaName());
const allTables = await platform.getSchemaHelper().getAllTables(connection, schemas);
const parts = config.get('migrations').tableName.split('.');
const migrationsTableName = parts[1] ?? parts[0];
const migrationsSchemaName = parts.length > 1 ? parts[0] : config.get('schema', platform.getDefaultSchemaName());
const tables = allTables.filter(
t =>
this.isTableNameAllowed(t.table_name, takeTables, skipTables) &&
(t.table_name !== migrationsTableName || (t.schema_name && t.schema_name !== migrationsSchemaName)),
);
await platform
.getSchemaHelper()
.loadInformationSchema(schema, connection, tables, schemas && schemas.length > 0 ? schemas : undefined);
// Load views from database
await platform.getSchemaHelper().loadViews(schema, connection);
// Load materialized views (PostgreSQL only)
if (platform.supportsMaterializedViews()) {
await platform.getSchemaHelper().loadMaterializedViews(schema, connection, schemaName);
}
// Filter out skipped views
if (skipViews && skipViews.length > 0) {
schema.#views = schema.#views.filter(v => this.isNameAllowed(v.name, skipViews));
}
return schema;
}
static fromMetadata(metadata, platform, config, schemaName, em) {
const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema'));
const nativeEnums = {};
const skipColumns = config.get('schemaGenerator').skipColumns || {};
for (const meta of metadata) {
// Skip view entities when collecting native enums
if (meta.view) {
continue;
}
for (const prop of meta.props) {
if (prop.nativeEnumName) {
let key = prop.nativeEnumName;
let enumName = prop.nativeEnumName;
let enumSchema = meta.schema ?? schema.name;
if (key.includes('.')) {
const [explicitSchema, ...parts] = prop.nativeEnumName.split('.');
enumName = parts.join('.');
key = enumName;
enumSchema = explicitSchema;
}
if (enumSchema && enumSchema !== '*' && enumSchema !== platform.getDefaultSchemaName()) {
key = enumSchema + '.' + key;
}
nativeEnums[key] = {
name: enumName,
schema: enumSchema,
items: prop.items?.map(val => '' + val) ?? [],
};
}
}
}
schema.setNativeEnums(nativeEnums);
for (const meta of metadata) {
// Handle view entities separately
if (meta.view) {
const viewDefinition = this.getViewDefinition(meta, em, platform);
if (viewDefinition) {
schema.addView(
meta.collection,
this.getSchemaName(meta, config, schemaName),
viewDefinition,
meta.materialized,
meta.withData,
);
}
continue;
}
const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName));
table.comment = meta.comment;
// For TPT child entities, only use ownProps (properties defined in this entity only)
// For all other entities (including TPT root), use all props
const propsToProcess =
meta.inheritanceType === 'tpt' && meta.tptParent && meta.ownProps ? meta.ownProps : meta.props;
for (const prop of propsToProcess) {
if (!this.shouldHaveColumn(meta, prop, skipColumns)) {
continue;
}
table.addColumnFromProperty(prop, meta, config);
}
// For TPT child entities, always include the PK columns (they form the FK to parent)
if (meta.inheritanceType === 'tpt' && meta.tptParent) {
const pkProps = meta.primaryKeys.map(pk => meta.properties[pk]);
for (const pkProp of pkProps) {
// Only add if not already added (it might be in ownProps if defined in this entity)
if (!propsToProcess.includes(pkProp)) {
table.addColumnFromProperty(pkProp, meta, config);
}
// Child PK must not be autoincrement — it references the parent PK via FK
for (const field of pkProp.fieldNames) {
const col = table.getColumn(field);
if (col) {
col.autoincrement = false;
}
}
}
// Add FK from child PK to parent PK with ON DELETE CASCADE
this.addTPTForeignKey(table, meta, config, platform);
}
meta.indexes.forEach(index => table.addIndex(meta, index, 'index'));
meta.uniques.forEach(index => table.addIndex(meta, index, 'unique'));
// For TPT child entities, the PK is also defined here
const pkPropsForIndex =
meta.inheritanceType === 'tpt' && meta.tptParent
? meta.primaryKeys.map(pk => meta.properties[pk])
: meta.props.filter(prop => prop.primary);
table.addIndex(meta, { properties: pkPropsForIndex.map(prop => prop.name) }, 'primary');
for (const check of meta.checks) {
const columnName = check.property ? meta.properties[check.property].fieldNames[0] : undefined;
const expression = isRaw(check.expression)
? platform.formatQuery(check.expression.sql, check.expression.params)
: check.expression;
table.addCheck({
name: check.name,
expression,
definition: `check (${expression})`,
columnName,
});
}
}
return schema;
}
static getViewDefinition(meta, em, platform) {
if (typeof meta.expression === 'string') {
return meta.expression;
}
// Expression is a function, need to evaluate it
/* v8 ignore next */
if (!em) {
return undefined;
}
const result = meta.expression(em, {}, {});
// Async expressions are not supported for view entities
if (result && typeof result.then === 'function') {
throw new Error(
`View entity ${meta.className} expression returned a Promise. Async expressions are not supported for view entities.`,
);
}
/* v8 ignore next */
if (typeof result === 'string') {
return result;
}
/* v8 ignore next */
if (isRaw(result)) {
return platform.formatQuery(result.sql, result.params);
}
// Check if it's a QueryBuilder (has getFormattedQuery method)
if (result && typeof result.getFormattedQuery === 'function') {
return result.getFormattedQuery();
}
/* v8 ignore next - fallback for unknown result types */
return undefined;
}
static getSchemaName(meta, config, schema) {
return (meta.schema === '*' ? schema : meta.schema) ?? config.get('schema');
}
/**
* Add a foreign key from a TPT child entity's PK to its parent entity's PK.
* This FK uses ON DELETE CASCADE to ensure child rows are deleted when parent is deleted.
*/
static addTPTForeignKey(table, meta, config, platform) {
const parent = meta.tptParent;
const pkColumnNames = meta.primaryKeys.flatMap(pk => meta.properties[pk].fieldNames);
const parentPkColumnNames = parent.primaryKeys.flatMap(pk => parent.properties[pk].fieldNames);
// Determine the parent table name with schema
const parentSchema =
parent.schema === '*' ? undefined : (parent.schema ?? config.get('schema', platform.getDefaultSchemaName()));
const parentTableName = parentSchema ? `${parentSchema}.${parent.tableName}` : parent.tableName;
// Create FK constraint name
const constraintName = platform.getIndexName(table.name, pkColumnNames, 'foreign');
// Add the foreign key to the table
const fks = table.getForeignKeys();
fks[constraintName] = {
constraintName,
columnNames: pkColumnNames,
localTableName: table.getShortestName(false),
referencedColumnNames: parentPkColumnNames,
referencedTableName: parentTableName,
deleteRule: 'cascade', // TPT always uses cascade delete
updateRule: 'cascade', // TPT always uses cascade update
};
}
static matchName(name, nameToMatch) {
return typeof nameToMatch === 'string'
? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
: nameToMatch.test(name);
}
static isNameAllowed(name, skipNames) {
return !(skipNames?.some(pattern => this.matchName(name, pattern)) ?? false);
}
static isTableNameAllowed(tableName, takeTables, skipTables) {
return (
(takeTables?.some(tableNameToMatch => this.matchName(tableName, tableNameToMatch)) ?? true) &&
this.isNameAllowed(tableName, skipTables)
);
}
static shouldHaveColumn(meta, prop, skipColumns) {
if (prop.persist === false || (prop.columnTypes?.length ?? 0) === 0) {
return false;
}
// Check if column should be skipped
if (skipColumns) {
const tableName = meta.tableName;
const tableSchema = meta.schema;
const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName;
// Check for skipColumns by table name or fully qualified table name
const columnsToSkip = skipColumns[tableName] || skipColumns[fullTableName];
if (columnsToSkip) {
for (const fieldName of prop.fieldNames) {
if (columnsToSkip.some(pattern => this.matchName(fieldName, pattern))) {
return false;
}
}
}
}
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
return true;
}
const getRootProperty = prop => (prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop);
const rootProp = getRootProperty(prop);
if (rootProp.kind === ReferenceKind.EMBEDDED) {
return prop === rootProp || !rootProp.object;
}
return (
[ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind) ||
(prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner)
);
}
toJSON() {
return {
name: this.name,
namespaces: [...this.#namespaces],
tables: this.#tables,
views: this.#views,
nativeEnums: this.#nativeEnums,
};
}
prune(schema, wildcardSchemaTables) {
const hasWildcardSchema = wildcardSchemaTables.length > 0;
this.#tables = this.#tables.filter(table => {
return (
(!schema && !hasWildcardSchema) || // no schema specified and we don't have any multi-schema entity
table.schema === schema || // specified schema matches the table's one
(!schema && !wildcardSchemaTables.includes(table.name))
); // no schema specified and the table has fixed one provided
});
this.#views = this.#views.filter(view => {
/* v8 ignore next */
return (
(!schema && !hasWildcardSchema) ||
view.schema === schema ||
(!schema && !wildcardSchemaTables.includes(view.name))
);
});
// remove namespaces of ignored tables and views
for (const ns of this.#namespaces) {
if (
!this.#tables.some(t => t.schema === ns) &&
!this.#views.some(v => v.schema === ns) &&
!Object.values(this.#nativeEnums).some(e => e.schema === ns)
) {
this.#namespaces.delete(ns);
}
}
}
}