Initial commit - Event Planner application

This commit is contained in:
mberlin
2026-03-18 14:55:56 -03:00
commit 86d779eb4d
7548 changed files with 1006324 additions and 0 deletions

49
node_modules/@mikro-orm/sql/plugin/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,49 @@
import {
type KyselyPlugin,
type PluginTransformQueryArgs,
type PluginTransformResultArgs,
type QueryResult,
type RootOperationNode,
type UnknownRow,
} from 'kysely';
import type { SqlEntityManager } from '../SqlEntityManager.js';
/** Configuration options for the MikroKyselyPlugin. */
export interface MikroKyselyPluginOptions {
/**
* Use database table names ('table') or entity names ('entity') in queries.
*
* @default 'table'
*/
tableNamingStrategy?: 'table' | 'entity';
/**
* Use database column names ('column') or property names ('property') in queries.
*
* @default 'column'
*/
columnNamingStrategy?: 'column' | 'property';
/**
* Automatically process entity `onCreate` hooks in INSERT queries.
*
* @default false
*/
processOnCreateHooks?: boolean;
/**
* Automatically process entity `onUpdate` hooks in UPDATE queries.
*
* @default false
*/
processOnUpdateHooks?: boolean;
/**
* Convert JavaScript values to database-compatible values (e.g., Date to timestamp, custom types).
*
* @default false
*/
convertValues?: boolean;
}
/** Kysely plugin that transforms queries and results to use MikroORM entity/property naming conventions. */
export declare class MikroKyselyPlugin implements KyselyPlugin {
#private;
constructor(em: SqlEntityManager, options?: MikroKyselyPluginOptions);
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
}

50
node_modules/@mikro-orm/sql/plugin/index.js generated vendored Normal file
View File

@@ -0,0 +1,50 @@
import {
SelectQueryNode as SelectQueryNodeClass,
InsertQueryNode as InsertQueryNodeClass,
UpdateQueryNode as UpdateQueryNodeClass,
DeleteQueryNode as DeleteQueryNodeClass,
} from 'kysely';
import { MikroTransformer } from './transformer.js';
/** Kysely plugin that transforms queries and results to use MikroORM entity/property naming conventions. */
export class MikroKyselyPlugin {
static #queryNodeCache = new WeakMap();
#transformer;
#options;
constructor(em, options = {}) {
this.#options = options;
this.#transformer = new MikroTransformer(em, options);
}
transformQuery(args) {
this.#transformer.reset();
const result = this.#transformer.transformNode(args.node, args.queryId);
// Cache the entity map if it is one we can process (for use in transformResult)
if (
SelectQueryNodeClass.is(args.node) ||
InsertQueryNodeClass.is(args.node) ||
UpdateQueryNodeClass.is(args.node) ||
DeleteQueryNodeClass.is(args.node)
) {
// clone the entityMap because the transformer's internal map will be cleared and reused by the next query
const entityMap = new Map(this.#transformer.getOutputEntityMap());
MikroKyselyPlugin.#queryNodeCache.set(args.queryId, { entityMap });
}
return result;
}
async transformResult(args) {
// Only transform results if columnNamingStrategy is 'property' or convertValues is true
if (this.#options.columnNamingStrategy !== 'property' && !this.#options.convertValues) {
return args.result;
}
// Retrieve the cached query node and metadata
const cache = MikroKyselyPlugin.#queryNodeCache.get(args.queryId);
if (!cache) {
return args.result;
}
// Transform the result rows using the transformer
const transformedRows = this.#transformer.transformResult(args.result.rows ?? [], cache.entityMap);
return {
...args.result,
rows: transformedRows ?? [],
};
}
}

122
node_modules/@mikro-orm/sql/plugin/transformer.d.ts generated vendored Normal file
View File

@@ -0,0 +1,122 @@
import { type EntityMetadata, type EntityProperty } from '@mikro-orm/core';
import {
type CommonTableExpressionNameNode,
type DeleteQueryNode,
type IdentifierNode,
type InsertQueryNode,
type JoinNode,
type MergeQueryNode,
type QueryId,
type SelectQueryNode,
type UpdateQueryNode,
type WithNode,
ColumnNode,
OperationNodeTransformer,
TableNode,
} from 'kysely';
import type { MikroKyselyPluginOptions } from './index.js';
import type { SqlEntityManager } from '../SqlEntityManager.js';
export declare class MikroTransformer extends OperationNodeTransformer {
#private;
constructor(em: SqlEntityManager, options?: MikroKyselyPluginOptions);
reset(): void;
getOutputEntityMap(): Map<string, EntityMetadata>;
/** @internal */
getContextStack(): Map<string, EntityMetadata | undefined>[];
/** @internal */
getSubqueryAliasMap(): Map<string, EntityMetadata | undefined>;
transformSelectQuery(node: SelectQueryNode, queryId: QueryId): SelectQueryNode;
transformInsertQuery(node: InsertQueryNode, queryId?: QueryId): InsertQueryNode;
transformUpdateQuery(node: UpdateQueryNode, queryId?: QueryId): UpdateQueryNode;
transformDeleteQuery(node: DeleteQueryNode, queryId?: QueryId): DeleteQueryNode;
transformMergeQuery(node: MergeQueryNode, queryId?: QueryId): MergeQueryNode;
transformIdentifier(node: IdentifierNode, queryId: QueryId): IdentifierNode;
/**
* Find owner entity metadata for the current identifier in the context stack.
* Supports both aliased and non-aliased table references.
* Searches up the context stack to support correlated subqueries.
* Also checks subquery/CTE aliases to resolve to their source tables.
*/
findOwnerEntityInContext(): EntityMetadata | undefined;
processOnCreateHooks(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
mapColumnsToProperties(columns: readonly ColumnNode[], meta: EntityMetadata): (EntityProperty | undefined)[];
normalizeColumnName(identifier: IdentifierNode): string;
findProperty(meta: EntityMetadata | undefined, columnName?: string): EntityProperty | undefined;
shouldConvertValues(): boolean;
prepareInputValue(prop: EntityProperty | undefined, value: unknown, enabled: boolean): unknown;
/**
* Look up a table name/alias in the context stack.
* Searches from current scope (top of stack) to parent scopes (bottom).
* This supports correlated subqueries and references to outer query tables.
*/
lookupInContextStack(tableNameOrAlias: string): EntityMetadata | undefined;
/**
* Process WITH node (CTE definitions)
*/
processWithNode(withNode: WithNode, context: Map<string, EntityMetadata | undefined>): void;
/**
* Extract CTE name from CommonTableExpressionNameNode
*/
getCTEName(nameNode: CommonTableExpressionNameNode): string | undefined;
/**
* Process a FROM item (can be TableNode or AliasNode)
*/
processFromItem(
from: any, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
context: Map<string, EntityMetadata | undefined>,
): void;
/**
* Process a JOIN node
*/
processJoinNode(join: JoinNode, context: Map<string, EntityMetadata | undefined>): void;
/**
* Extract the primary source table from a SELECT query
* This helps resolve columns from subqueries to their original entity tables
*/
extractSourceTableFromSelectQuery(selectQuery: SelectQueryNode): EntityMetadata | undefined;
/**
* Extract alias name from an alias node
*/
extractAliasName(alias: any): string | undefined;
/**
* Extract table name from a TableNode
*/
getTableName(node: TableNode | undefined): string | undefined;
/**
* Find entity metadata by table name or entity name
*/
findEntityMetadata(name: string): EntityMetadata | undefined;
/**
* Transform result rows by mapping database column names to property names
* This is called for SELECT queries when columnNamingStrategy is 'property'
*/
transformResult(
rows: Record<string, any>[] | undefined,
entityMap: Map<string, EntityMetadata>,
): Record<string, any>[] | undefined;
buildGlobalFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, EntityProperty>;
buildGlobalRelationFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, string>;
/**
* Build a mapping from database field names to property objects
* Format: { 'field_name': EntityProperty }
*/
buildFieldToPropertyMap(meta: EntityMetadata, alias?: string): Record<string, EntityProperty>;
/**
* Build a mapping for relation fields
* For ManyToOne relations, we need to map from the foreign key field to the relation property
* Format: { 'foreign_key_field': 'relationPropertyName' }
*/
buildRelationFieldMap(meta: EntityMetadata, alias?: string): Record<string, string>;
/**
* Transform a single row by mapping column names to property names
*/
transformRow(
row: Record<string, any>,
fieldToPropertyMap: Record<string, EntityProperty>,
relationFieldMap: Record<string, string>,
): Record<string, any>;
prepareOutputValue(prop: EntityProperty | undefined, value: unknown): unknown;
}

901
node_modules/@mikro-orm/sql/plugin/transformer.js generated vendored Normal file
View File

@@ -0,0 +1,901 @@
import { ReferenceKind, isRaw } from '@mikro-orm/core';
import {
AliasNode,
ColumnNode,
ColumnUpdateNode,
OperationNodeTransformer,
PrimitiveValueListNode,
ReferenceNode,
SchemableIdentifierNode,
TableNode,
ValueListNode,
ValueNode,
ValuesNode,
} from 'kysely';
export class MikroTransformer extends OperationNodeTransformer {
/**
* Context stack to support nested queries (subqueries, CTEs)
* Each level of query scope has its own Map of table aliases/names to EntityMetadata
* Top of stack (highest index) is the current scope
*/
#contextStack = [];
/**
* Subquery alias map: maps subquery/CTE alias to its source table metadata
* Used to resolve columns from subqueries/CTEs to their original table definitions
*/
#subqueryAliasMap = new Map();
#metadata;
#platform;
/**
* Global map of all entities involved in the query.
* Populated during AST transformation and used for result transformation.
*/
#entityMap = new Map();
#em;
#options;
constructor(em, options = {}) {
super();
this.#em = em;
this.#options = options;
this.#metadata = em.getMetadata();
this.#platform = em.getDriver().getPlatform();
}
reset() {
this.#subqueryAliasMap.clear();
this.#entityMap.clear();
}
getOutputEntityMap() {
return this.#entityMap;
}
/** @internal */
getContextStack() {
return this.#contextStack;
}
/** @internal */
getSubqueryAliasMap() {
return this.#subqueryAliasMap;
}
transformSelectQuery(node, queryId) {
// Push a new context for this query scope (starts with inherited parent context)
const currentContext = new Map();
this.#contextStack.push(currentContext);
try {
// Process WITH clause (CTEs) first - they define names available in this scope
if (node.with) {
this.processWithNode(node.with, currentContext);
}
// Process FROM clause - main tables in this scope
if (node.from?.froms) {
for (const from of node.from.froms) {
this.processFromItem(from, currentContext);
}
}
// Process JOINs - additional tables joined into this scope
if (node.joins) {
for (const join of node.joins) {
this.processJoinNode(join, currentContext);
}
}
return super.transformSelectQuery(node, queryId);
} finally {
// Pop the context when exiting this query scope
this.#contextStack.pop();
}
}
transformInsertQuery(node, queryId) {
const currentContext = new Map();
this.#contextStack.push(currentContext);
try {
let entityMeta;
if (node.into) {
const tableName = this.getTableName(node.into);
if (tableName) {
const meta = this.findEntityMetadata(tableName);
if (meta) {
entityMeta = meta;
currentContext.set(meta.tableName, meta);
this.#entityMap.set(meta.tableName, meta);
}
}
}
const nodeWithHooks =
this.#options.processOnCreateHooks && entityMeta ? this.processOnCreateHooks(node, entityMeta) : node;
const nodeWithConvertedValues =
this.#options.convertValues && entityMeta ? this.processInsertValues(nodeWithHooks, entityMeta) : nodeWithHooks;
// Handle ON CONFLICT clause
let finalNode = nodeWithConvertedValues;
if (node.onConflict?.updates && entityMeta) {
// Create a temporary UpdateQueryNode to reuse processOnUpdateHooks and processUpdateValues
// We only care about the updates part
const tempUpdateNode = {
kind: 'UpdateQueryNode',
table: node.into, // Dummy table
updates: node.onConflict.updates,
};
const updatesWithHooks = this.#options.processOnUpdateHooks
? this.processOnUpdateHooks(tempUpdateNode, entityMeta).updates
: node.onConflict.updates;
const tempUpdateNodeWithHooks = {
...tempUpdateNode,
updates: updatesWithHooks,
};
const updatesWithConvertedValues = this.#options.convertValues
? this.processUpdateValues(tempUpdateNodeWithHooks, entityMeta).updates
: updatesWithHooks;
if (updatesWithConvertedValues && updatesWithConvertedValues !== node.onConflict.updates) {
// Construct the new OnConflictNode with updated values
finalNode = {
...finalNode,
onConflict: {
...node.onConflict,
updates: updatesWithConvertedValues,
},
};
}
}
return super.transformInsertQuery(finalNode, queryId);
} finally {
this.#contextStack.pop();
}
}
transformUpdateQuery(node, queryId) {
const currentContext = new Map();
this.#contextStack.push(currentContext);
try {
let entityMeta;
if (node.table && TableNode.is(node.table)) {
const tableName = this.getTableName(node.table);
if (tableName) {
const meta = this.findEntityMetadata(tableName);
if (meta) {
entityMeta = meta;
currentContext.set(meta.tableName, meta);
this.#entityMap.set(meta.tableName, meta);
}
}
}
// Process FROM clause in UPDATE queries (for UPDATE with JOIN)
if (node.from) {
for (const fromItem of node.from.froms) {
this.processFromItem(fromItem, currentContext);
}
}
// Also process JOINs in UPDATE queries
if (node.joins) {
for (const join of node.joins) {
this.processJoinNode(join, currentContext);
}
}
const nodeWithHooks =
this.#options.processOnUpdateHooks && entityMeta ? this.processOnUpdateHooks(node, entityMeta) : node;
const nodeWithConvertedValues =
this.#options.convertValues && entityMeta ? this.processUpdateValues(nodeWithHooks, entityMeta) : nodeWithHooks;
return super.transformUpdateQuery(nodeWithConvertedValues, queryId);
} finally {
this.#contextStack.pop();
}
}
transformDeleteQuery(node, queryId) {
const currentContext = new Map();
this.#contextStack.push(currentContext);
try {
const froms = node.from?.froms;
if (froms && froms.length > 0) {
const firstFrom = froms[0];
if (TableNode.is(firstFrom)) {
const tableName = this.getTableName(firstFrom);
if (tableName) {
const meta = this.findEntityMetadata(tableName);
if (meta) {
currentContext.set(meta.tableName, meta);
this.#entityMap.set(meta.tableName, meta);
}
}
}
}
// Also process JOINs in DELETE queries
if (node.joins) {
for (const join of node.joins) {
this.processJoinNode(join, currentContext);
}
}
return super.transformDeleteQuery(node, queryId);
} finally {
this.#contextStack.pop();
}
}
transformMergeQuery(node, queryId) {
const currentContext = new Map();
this.#contextStack.push(currentContext);
try {
return super.transformMergeQuery(node, queryId);
} finally {
this.#contextStack.pop();
}
}
transformIdentifier(node, queryId) {
node = super.transformIdentifier(node, queryId);
const parent = this.nodeStack[this.nodeStack.length - 2];
// Transform table names when tableNamingStrategy is 'entity'
if (this.#options.tableNamingStrategy === 'entity' && parent && SchemableIdentifierNode.is(parent)) {
const meta = this.findEntityMetadata(node.name);
if (meta) {
return {
...node,
name: meta.tableName,
};
}
}
// Transform column names when columnNamingStrategy is 'property'
// Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions)
if (
this.#options.columnNamingStrategy === 'property' &&
parent &&
(ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))
) {
const ownerMeta = this.findOwnerEntityInContext();
if (ownerMeta) {
const prop = ownerMeta.properties[node.name];
const fieldName = prop?.fieldNames?.[0];
if (fieldName) {
return {
...node,
name: fieldName,
};
}
}
}
return node;
}
/**
* Find owner entity metadata for the current identifier in the context stack.
* Supports both aliased and non-aliased table references.
* Searches up the context stack to support correlated subqueries.
* Also checks subquery/CTE aliases to resolve to their source tables.
*/
findOwnerEntityInContext() {
// Check if current column has a table reference (e.g., u.firstName)
const reference = this.nodeStack.find(it => ReferenceNode.is(it));
if (reference?.table && TableNode.is(reference.table)) {
const tableName = this.getTableName(reference.table);
if (tableName) {
// First, check in subquery alias map (for CTE/subquery columns)
if (this.#subqueryAliasMap.has(tableName)) {
return this.#subqueryAliasMap.get(tableName);
}
// Find entity metadata to get the actual table name
// Context uses table names (meta.tableName) as keys, not entity names
const entityMeta = this.findEntityMetadata(tableName);
if (entityMeta) {
// Search in context stack using the actual table name
const meta = this.lookupInContextStack(entityMeta.tableName);
if (meta) {
return meta;
}
// Also try with the entity name (for cases where context uses entity name)
const metaByEntityName = this.lookupInContextStack(tableName);
if (metaByEntityName) {
return metaByEntityName;
}
} else {
// If entity metadata not found, try direct lookup (for CTE/subquery cases)
const meta = this.lookupInContextStack(tableName);
if (meta) {
return meta;
}
}
}
}
// If no explicit table reference, use the first entity in current context
if (this.#contextStack.length > 0) {
const currentContext = this.#contextStack[this.#contextStack.length - 1];
for (const [alias, meta] of currentContext.entries()) {
if (meta) {
return meta;
}
// If the context value is undefined but the alias is in subqueryAliasMap,
// use the mapped metadata (for CTE/subquery cases)
if (!meta && this.#subqueryAliasMap.has(alias)) {
const mappedMeta = this.#subqueryAliasMap.get(alias);
if (mappedMeta) {
return mappedMeta;
}
}
}
}
return undefined;
}
processOnCreateHooks(node, meta) {
if (!node.columns || !node.values || !ValuesNode.is(node.values)) {
return node;
}
const existingProps = new Set();
for (const col of node.columns) {
const prop = this.findProperty(meta, this.normalizeColumnName(col.column));
if (prop) {
existingProps.add(prop.name);
}
}
const missingProps = meta.props.filter(prop => prop.onCreate && !existingProps.has(prop.name));
if (missingProps.length === 0) {
return node;
}
const newColumns = [...node.columns];
for (const prop of missingProps) {
newColumns.push(ColumnNode.create(prop.name));
}
const newRows = node.values.values.map(row => {
const valuesToAdd = missingProps.map(prop => {
const val = prop.onCreate(undefined, this.#em);
return val;
});
if (ValueListNode.is(row)) {
const newValues = [...row.values, ...valuesToAdd.map(v => ValueNode.create(v))];
return ValueListNode.create(newValues);
}
if (PrimitiveValueListNode.is(row)) {
const newValues = [...row.values, ...valuesToAdd];
return PrimitiveValueListNode.create(newValues);
}
return row;
});
return {
...node,
columns: Object.freeze(newColumns),
values: ValuesNode.create(newRows),
};
}
processOnUpdateHooks(node, meta) {
if (!node.updates) {
return node;
}
const existingProps = new Set();
for (const update of node.updates) {
if (ColumnNode.is(update.column)) {
const prop = this.findProperty(meta, this.normalizeColumnName(update.column.column));
if (prop) {
existingProps.add(prop.name);
}
}
}
const missingProps = meta.props.filter(prop => prop.onUpdate && !existingProps.has(prop.name));
if (missingProps.length === 0) {
return node;
}
const newUpdates = [...node.updates];
for (const prop of missingProps) {
const val = prop.onUpdate(undefined, this.#em);
newUpdates.push(ColumnUpdateNode.create(ColumnNode.create(prop.name), ValueNode.create(val)));
}
return {
...node,
updates: Object.freeze(newUpdates),
};
}
processInsertValues(node, meta) {
if (!node.columns?.length || !node.values || !ValuesNode.is(node.values)) {
return node;
}
const columnProps = this.mapColumnsToProperties(node.columns, meta);
const shouldConvert = this.shouldConvertValues();
let changed = false;
const convertedRows = node.values.values.map(row => {
if (ValueListNode.is(row)) {
if (row.values.length !== columnProps.length) {
return row;
}
const values = row.values.map((valueNode, idx) => {
if (!ValueNode.is(valueNode)) {
return valueNode;
}
const converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert);
if (converted === valueNode.value) {
return valueNode;
}
changed = true;
return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted);
});
return ValueListNode.create(values);
}
if (PrimitiveValueListNode.is(row)) {
if (row.values.length !== columnProps.length) {
return row;
}
const values = row.values.map((value, idx) => {
const converted = this.prepareInputValue(columnProps[idx], value, shouldConvert);
if (converted !== value) {
changed = true;
}
return converted;
});
return PrimitiveValueListNode.create(values);
}
return row;
});
if (!changed) {
return node;
}
return {
...node,
values: ValuesNode.create(convertedRows),
};
}
processUpdateValues(node, meta) {
if (!node.updates?.length) {
return node;
}
const shouldConvert = this.shouldConvertValues();
let changed = false;
const updates = node.updates.map(updateNode => {
if (!ValueNode.is(updateNode.value)) {
return updateNode;
}
const columnName = ColumnNode.is(updateNode.column)
? this.normalizeColumnName(updateNode.column.column)
: undefined;
const property = this.findProperty(meta, columnName);
const converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert);
if (converted === updateNode.value.value) {
return updateNode;
}
changed = true;
const newValueNode = updateNode.value.immediate
? ValueNode.createImmediate(converted)
: ValueNode.create(converted);
return {
...updateNode,
value: newValueNode,
};
});
if (!changed) {
return node;
}
return {
...node,
updates,
};
}
mapColumnsToProperties(columns, meta) {
return columns.map(column => {
const columnName = this.normalizeColumnName(column.column);
return this.findProperty(meta, columnName);
});
}
normalizeColumnName(identifier) {
const name = identifier.name;
if (!name.includes('.')) {
return name;
}
const parts = name.split('.');
return parts[parts.length - 1] ?? name;
}
findProperty(meta, columnName) {
if (!meta || !columnName) {
return undefined;
}
if (meta.properties[columnName]) {
return meta.properties[columnName];
}
return meta.props.find(prop => prop.fieldNames?.includes(columnName));
}
shouldConvertValues() {
return !!this.#options.convertValues;
}
prepareInputValue(prop, value, enabled) {
if (!enabled || !prop || value == null) {
return value;
}
if (typeof value === 'object' && value !== null) {
if (isRaw(value)) {
return value;
}
if ('kind' in value) {
return value;
}
}
if (prop.customType && !isRaw(value)) {
return prop.customType.convertToDatabaseValue(value, this.#platform, {
fromQuery: true,
key: prop.name,
mode: 'query-data',
});
}
if (value instanceof Date) {
return this.#platform.processDateProperty(value);
}
return value;
}
/**
* Look up a table name/alias in the context stack.
* Searches from current scope (top of stack) to parent scopes (bottom).
* This supports correlated subqueries and references to outer query tables.
*/
lookupInContextStack(tableNameOrAlias) {
// Search from top of stack (current scope) to bottom (parent scopes)
for (let i = this.#contextStack.length - 1; i >= 0; i--) {
const context = this.#contextStack[i];
if (context.has(tableNameOrAlias)) {
return context.get(tableNameOrAlias);
}
}
return undefined;
}
/**
* Process WITH node (CTE definitions)
*/
processWithNode(withNode, context) {
for (const cte of withNode.expressions) {
const cteName = this.getCTEName(cte.name);
if (cteName) {
// CTEs are not entities, so map to undefined
// They will be transformed recursively by transformSelectQuery
context.set(cteName, undefined);
// Also try to extract the source table from the CTE's expression
// This helps resolve columns in subsequent queries that use the CTE
if (cte.expression?.kind === 'SelectQueryNode') {
const sourceMeta = this.extractSourceTableFromSelectQuery(cte.expression);
if (sourceMeta) {
this.#subqueryAliasMap.set(cteName, sourceMeta);
// Add CTE to entityMap so it can be used for result transformation if needed
// (though CTEs usually don't appear in result rows directly, but their columns might)
this.#entityMap.set(cteName, sourceMeta);
}
}
}
}
}
/**
* Extract CTE name from CommonTableExpressionNameNode
*/
getCTEName(nameNode) {
if (TableNode.is(nameNode.table)) {
return this.getTableName(nameNode.table);
}
return undefined;
}
/**
* Process a FROM item (can be TableNode or AliasNode)
*/
processFromItem(
from, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
context,
) {
if (AliasNode.is(from)) {
if (TableNode.is(from.node)) {
// Regular table with alias
const tableName = this.getTableName(from.node);
if (tableName && from.alias) {
const meta = this.findEntityMetadata(tableName);
const aliasName = this.extractAliasName(from.alias);
if (aliasName) {
context.set(aliasName, meta);
if (meta) {
this.#entityMap.set(aliasName, meta);
}
// Also map the alias in subqueryAliasMap if the table name is a CTE
if (this.#subqueryAliasMap.has(tableName)) {
this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
}
}
}
} else if (from.node?.kind === 'SelectQueryNode') {
// Subquery with alias
const aliasName = this.extractAliasName(from.alias);
if (aliasName) {
context.set(aliasName, undefined);
// Try to extract the source table from the subquery
const sourceMeta = this.extractSourceTableFromSelectQuery(from.node);
if (sourceMeta) {
this.#subqueryAliasMap.set(aliasName, sourceMeta);
}
}
} else {
// Other types with alias
const aliasName = this.extractAliasName(from.alias);
if (aliasName) {
context.set(aliasName, undefined);
}
}
} else if (TableNode.is(from)) {
// Table without alias
const tableName = this.getTableName(from);
if (tableName) {
const meta = this.findEntityMetadata(tableName);
context.set(tableName, meta);
if (meta) {
this.#entityMap.set(tableName, meta);
}
}
}
}
/**
* Process a JOIN node
*/
processJoinNode(join, context) {
const joinTable = join.table;
if (AliasNode.is(joinTable)) {
if (TableNode.is(joinTable.node)) {
// Regular table with alias in JOIN
const tableName = this.getTableName(joinTable.node);
if (tableName && joinTable.alias) {
const meta = this.findEntityMetadata(tableName);
const aliasName = this.extractAliasName(joinTable.alias);
if (aliasName) {
context.set(aliasName, meta);
if (meta) {
this.#entityMap.set(aliasName, meta);
}
// Also map the alias in subqueryAliasMap if the table name is a CTE
if (this.#subqueryAliasMap.has(tableName)) {
this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
}
}
}
} else if (joinTable.node?.kind === 'SelectQueryNode') {
// Subquery with alias in JOIN
const aliasName = this.extractAliasName(joinTable.alias);
if (aliasName) {
context.set(aliasName, undefined);
// Try to extract the source table from the subquery
const sourceMeta = this.extractSourceTableFromSelectQuery(joinTable.node);
if (sourceMeta) {
this.#subqueryAliasMap.set(aliasName, sourceMeta);
}
}
} else {
// Other types with alias
const aliasName = this.extractAliasName(joinTable.alias);
if (aliasName) {
context.set(aliasName, undefined);
}
}
} else if (TableNode.is(joinTable)) {
// Table without alias in JOIN
const tableName = this.getTableName(joinTable);
if (tableName) {
const meta = this.findEntityMetadata(tableName);
// Use table name (meta.tableName) as key to match transformUpdateQuery behavior
if (meta) {
context.set(meta.tableName, meta);
this.#entityMap.set(meta.tableName, meta);
// Also set with entity name for backward compatibility
context.set(tableName, meta);
} else {
context.set(tableName, undefined);
}
}
}
}
/**
* Extract the primary source table from a SELECT query
* This helps resolve columns from subqueries to their original entity tables
*/
extractSourceTableFromSelectQuery(selectQuery) {
if (!selectQuery.from?.froms || selectQuery.from.froms.length === 0) {
return undefined;
}
// Get the first FROM table
const firstFrom = selectQuery.from.froms[0];
let sourceTable;
if (AliasNode.is(firstFrom) && TableNode.is(firstFrom.node)) {
sourceTable = firstFrom.node;
} else if (TableNode.is(firstFrom)) {
sourceTable = firstFrom;
}
if (sourceTable) {
const tableName = this.getTableName(sourceTable);
if (tableName) {
return this.findEntityMetadata(tableName);
}
}
return undefined;
}
/**
* Extract alias name from an alias node
*/
extractAliasName(alias) {
if (typeof alias === 'object' && 'name' in alias) {
return alias.name;
}
return undefined;
}
/**
* Extract table name from a TableNode
*/
getTableName(node) {
if (!node) {
return undefined;
}
if (TableNode.is(node) && SchemableIdentifierNode.is(node.table)) {
const identifier = node.table.identifier;
if (typeof identifier === 'object' && 'name' in identifier) {
return identifier.name;
}
}
return undefined;
}
/**
* Find entity metadata by table name or entity name
*/
findEntityMetadata(name) {
const byEntity = this.#metadata.getByClassName(name, false);
if (byEntity) {
return byEntity;
}
const allMetadata = Array.from(this.#metadata);
const byTable = allMetadata.find(m => m.tableName === name);
if (byTable) {
return byTable;
}
return undefined;
}
/**
* Transform result rows by mapping database column names to property names
* This is called for SELECT queries when columnNamingStrategy is 'property'
*/
transformResult(rows, entityMap) {
// Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data
if (
(this.#options.columnNamingStrategy !== 'property' && !this.#options.convertValues) ||
!rows ||
rows.length === 0
) {
return rows;
}
// If no entities found (e.g. raw query without known tables), return rows as is
if (entityMap.size === 0) {
return rows;
}
// Build a global mapping from database field names to property objects
const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap);
const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
// Transform each row
return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap));
}
buildGlobalFieldMap(entityMap) {
const map = {};
for (const [alias, meta] of entityMap.entries()) {
Object.assign(map, this.buildFieldToPropertyMap(meta, alias));
}
return map;
}
buildGlobalRelationFieldMap(entityMap) {
const map = {};
for (const [alias, meta] of entityMap.entries()) {
Object.assign(map, this.buildRelationFieldMap(meta, alias));
}
return map;
}
/**
* Build a mapping from database field names to property objects
* Format: { 'field_name': EntityProperty }
*/
buildFieldToPropertyMap(meta, alias) {
const map = {};
for (const prop of meta.props) {
if (prop.fieldNames && prop.fieldNames.length > 0) {
for (const fieldName of prop.fieldNames) {
if (!(fieldName in map)) {
map[fieldName] = prop;
}
if (alias) {
const dotted = `${alias}.${fieldName}`;
if (!(dotted in map)) {
map[dotted] = prop;
}
const underscored = `${alias}_${fieldName}`;
if (!(underscored in map)) {
map[underscored] = prop;
}
const doubleUnderscored = `${alias}__${fieldName}`;
if (!(doubleUnderscored in map)) {
map[doubleUnderscored] = prop;
}
}
}
}
if (!(prop.name in map)) {
map[prop.name] = prop;
}
}
return map;
}
/**
* Build a mapping for relation fields
* For ManyToOne relations, we need to map from the foreign key field to the relation property
* Format: { 'foreign_key_field': 'relationPropertyName' }
*/
buildRelationFieldMap(meta, alias) {
const map = {};
for (const prop of meta.props) {
// For ManyToOne/OneToOne relations, find the foreign key field
if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE) {
if (prop.fieldNames && prop.fieldNames.length > 0) {
const fieldName = prop.fieldNames[0];
map[fieldName] = prop.name;
if (alias) {
map[`${alias}.${fieldName}`] = prop.name;
map[`${alias}_${fieldName}`] = prop.name;
map[`${alias}__${fieldName}`] = prop.name;
}
}
}
}
return map;
}
/**
* Transform a single row by mapping column names to property names
*/
transformRow(row, fieldToPropertyMap, relationFieldMap) {
const transformed = { ...row };
// First pass: map regular fields from fieldName to propertyName and convert values
for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) {
if (!(fieldName in transformed)) {
continue;
}
const converted = this.prepareOutputValue(prop, transformed[fieldName]);
if (this.#options.columnNamingStrategy === 'property' && prop.name !== fieldName) {
if (!(prop.name in transformed)) {
transformed[prop.name] = converted;
} else {
transformed[prop.name] = converted;
}
delete transformed[fieldName];
continue;
}
if (this.#options.convertValues) {
transformed[fieldName] = converted;
}
}
// Second pass: handle relation fields
// Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
if (this.#options.columnNamingStrategy === 'property') {
for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) {
if (fieldName in transformed && !(relationPropertyName in transformed)) {
// Move the foreign key value to the relation property name
transformed[relationPropertyName] = transformed[fieldName];
delete transformed[fieldName];
}
}
}
return transformed;
}
prepareOutputValue(prop, value) {
if (!this.#options.convertValues || !prop || value == null) {
return value;
}
if (prop.customType) {
return prop.customType.convertToJSValue(value, this.#platform);
}
// Aligned with EntityComparator.getResultMapper logic
if (prop.runtimeType === 'boolean') {
// Use !! conversion like EntityComparator: value == null ? value : !!value
return value == null ? value : !!value;
}
if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
// Aligned with EntityComparator: exclude numeric timestamp properties
// If already Date instance or null, return as is
if (value == null || value instanceof Date) {
return value;
}
// Handle timezone like EntityComparator.parseDate
const tz = this.#platform.getTimezone();
if (!tz || tz === 'local') {
return this.#platform.parseDate(value);
}
// For non-local timezone, check if value already has timezone info
// Number (timestamp) doesn't need timezone handling, string needs check
if (
typeof value === 'number' ||
(typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))
) {
return this.#platform.parseDate(value);
}
// Append timezone if not present (only for string values)
return this.#platform.parseDate(value + tz);
}
// For all other runtimeTypes (number, string, bigint, Buffer, object, any, etc.)
// EntityComparator just assigns directly without conversion
return value;
}
}