@mikro-orm/knex 7.0.0-dev.81 → 7.0.0-dev.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SqlEntityManager.d.ts +2 -1
- package/SqlEntityManager.js +10 -1
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/plugin/index.d.ts +53 -0
- package/plugin/index.js +42 -0
- package/plugin/transformer.d.ts +115 -0
- package/plugin/transformer.js +883 -0
- package/typings.d.ts +7 -6
package/SqlEntityManager.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type { QueryBuilder } from './query/QueryBuilder.js';
|
|
|
5
5
|
import type { SqlEntityRepository } from './SqlEntityRepository.js';
|
|
6
6
|
import type { Kysely } from 'kysely';
|
|
7
7
|
import type { InferKyselyDB } from './typings.js';
|
|
8
|
-
|
|
8
|
+
import { type MikroKyselyPluginOptions } from './plugin/index.js';
|
|
9
|
+
export interface GetKyselyOptions extends MikroKyselyPluginOptions {
|
|
9
10
|
type?: ConnectionType;
|
|
10
11
|
}
|
|
11
12
|
/**
|
package/SqlEntityManager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EntityManager, } from '@mikro-orm/core';
|
|
2
|
+
import { MikroKyselyPlugin } from './plugin/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* @inheritDoc
|
|
4
5
|
*/
|
|
@@ -20,7 +21,15 @@ export class SqlEntityManager extends EntityManager {
|
|
|
20
21
|
* Returns configured Kysely instance.
|
|
21
22
|
*/
|
|
22
23
|
getKysely(options = {}) {
|
|
23
|
-
|
|
24
|
+
let kysely = this.getConnection(options.type).getClient();
|
|
25
|
+
if (options.columnNamingStrategy != null
|
|
26
|
+
|| options.tableNamingStrategy != null
|
|
27
|
+
|| options.processOnCreateHooks != null
|
|
28
|
+
|| options.processOnUpdateHooks != null
|
|
29
|
+
|| options.convertValues != null) {
|
|
30
|
+
kysely = kysely.withPlugin(new MikroKyselyPlugin(this, options));
|
|
31
|
+
}
|
|
32
|
+
return kysely;
|
|
24
33
|
}
|
|
25
34
|
async execute(query, params = [], method = 'all', loggerContext) {
|
|
26
35
|
return this.getDriver().execute(query, params, method, this.getContext(false).getTransactionContext(), loggerContext);
|
package/index.d.ts
CHANGED
|
@@ -14,5 +14,6 @@ export { raw } from './query/index.js';
|
|
|
14
14
|
export * from './schema/index.js';
|
|
15
15
|
export * from './dialects/index.js';
|
|
16
16
|
export * from './typings.js';
|
|
17
|
+
export * from './plugin/index.js';
|
|
17
18
|
export { SqlEntityManager as EntityManager } from './SqlEntityManager.js';
|
|
18
19
|
export { SqlEntityRepository as EntityRepository } from './SqlEntityRepository.js';
|
package/index.js
CHANGED
|
@@ -14,5 +14,6 @@ export { raw } from './query/index.js';
|
|
|
14
14
|
export * from './schema/index.js';
|
|
15
15
|
export * from './dialects/index.js';
|
|
16
16
|
export * from './typings.js';
|
|
17
|
+
export * from './plugin/index.js';
|
|
17
18
|
export { SqlEntityManager as EntityManager } from './SqlEntityManager.js';
|
|
18
19
|
export { SqlEntityRepository as EntityRepository } from './SqlEntityRepository.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/knex",
|
|
3
|
-
"version": "7.0.0-dev.
|
|
3
|
+
"version": "7.0.0-dev.83",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"@mikro-orm/core": "^6.6.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@mikro-orm/core": "7.0.0-dev.
|
|
60
|
+
"@mikro-orm/core": "7.0.0-dev.83"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type KyselyPlugin, type PluginTransformQueryArgs, type PluginTransformResultArgs, type QueryResult, type RootOperationNode, type UnknownRow } from 'kysely';
|
|
2
|
+
import { MikroTransformer } from './transformer.js';
|
|
3
|
+
import type { SqlEntityManager } from '../SqlEntityManager.js';
|
|
4
|
+
import type { EntityMetadata } from '@mikro-orm/core';
|
|
5
|
+
/**
|
|
6
|
+
* Cache for query transformation data
|
|
7
|
+
* Stores the query node and metadata about tables/aliases
|
|
8
|
+
*/
|
|
9
|
+
interface QueryTransformCache {
|
|
10
|
+
entityMap: Map<string, EntityMetadata>;
|
|
11
|
+
}
|
|
12
|
+
export interface MikroKyselyPluginOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Use database table names ('table') or entity names ('entity') in queries.
|
|
15
|
+
*
|
|
16
|
+
* @default 'table'
|
|
17
|
+
*/
|
|
18
|
+
tableNamingStrategy?: 'table' | 'entity';
|
|
19
|
+
/**
|
|
20
|
+
* Use database column names ('column') or property names ('property') in queries.
|
|
21
|
+
*
|
|
22
|
+
* @default 'column'
|
|
23
|
+
*/
|
|
24
|
+
columnNamingStrategy?: 'column' | 'property';
|
|
25
|
+
/**
|
|
26
|
+
* Automatically process entity `onCreate` hooks in INSERT queries.
|
|
27
|
+
*
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
processOnCreateHooks?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Automatically process entity `onUpdate` hooks in UPDATE queries.
|
|
33
|
+
*
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
processOnUpdateHooks?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Convert JavaScript values to database-compatible values (e.g., Date to timestamp, custom types).
|
|
39
|
+
*
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
convertValues?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare class MikroKyselyPlugin implements KyselyPlugin {
|
|
45
|
+
protected readonly em: SqlEntityManager;
|
|
46
|
+
protected readonly options: MikroKyselyPluginOptions;
|
|
47
|
+
protected static queryNodeCache: WeakMap<any, QueryTransformCache>;
|
|
48
|
+
protected readonly transformer: MikroTransformer;
|
|
49
|
+
constructor(em: SqlEntityManager, options?: MikroKyselyPluginOptions);
|
|
50
|
+
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
|
|
51
|
+
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SelectQueryNode as SelectQueryNodeClass, InsertQueryNode as InsertQueryNodeClass, UpdateQueryNode as UpdateQueryNodeClass, DeleteQueryNode as DeleteQueryNodeClass, } from 'kysely';
|
|
2
|
+
import { MikroTransformer } from './transformer.js';
|
|
3
|
+
export class MikroKyselyPlugin {
|
|
4
|
+
em;
|
|
5
|
+
options;
|
|
6
|
+
static queryNodeCache = new WeakMap();
|
|
7
|
+
transformer;
|
|
8
|
+
constructor(em, options = {}) {
|
|
9
|
+
this.em = em;
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.transformer = new MikroTransformer(em, options);
|
|
12
|
+
}
|
|
13
|
+
transformQuery(args) {
|
|
14
|
+
this.transformer.reset();
|
|
15
|
+
const result = this.transformer.transformNode(args.node, args.queryId);
|
|
16
|
+
// Cache the entity map if it is one we can process (for use in transformResult)
|
|
17
|
+
if (SelectQueryNodeClass.is(args.node) ||
|
|
18
|
+
InsertQueryNodeClass.is(args.node) ||
|
|
19
|
+
UpdateQueryNodeClass.is(args.node) ||
|
|
20
|
+
DeleteQueryNodeClass.is(args.node)) {
|
|
21
|
+
MikroKyselyPlugin.queryNodeCache.set(args.queryId, { entityMap: this.transformer.getOutputEntityMap() });
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
async transformResult(args) {
|
|
26
|
+
// Only transform results if columnNamingStrategy is 'property' or convertValues is true
|
|
27
|
+
if (this.options.columnNamingStrategy !== 'property' && !this.options.convertValues) {
|
|
28
|
+
return args.result;
|
|
29
|
+
}
|
|
30
|
+
// Retrieve the cached query node and metadata
|
|
31
|
+
const cache = MikroKyselyPlugin.queryNodeCache.get(args.queryId);
|
|
32
|
+
if (!cache) {
|
|
33
|
+
return args.result;
|
|
34
|
+
}
|
|
35
|
+
// Transform the result rows using the transformer
|
|
36
|
+
const transformedRows = this.transformer.transformResult(args.result.rows ?? [], cache.entityMap);
|
|
37
|
+
return {
|
|
38
|
+
...args.result,
|
|
39
|
+
rows: transformedRows ?? [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type EntityMetadata, type EntityProperty, type MetadataStorage } from '@mikro-orm/core';
|
|
2
|
+
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';
|
|
3
|
+
import type { MikroKyselyPluginOptions } from './index.js';
|
|
4
|
+
import type { SqlEntityManager } from '../SqlEntityManager.js';
|
|
5
|
+
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
6
|
+
export declare class MikroTransformer extends OperationNodeTransformer {
|
|
7
|
+
protected readonly em: SqlEntityManager;
|
|
8
|
+
protected readonly options: MikroKyselyPluginOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Context stack to support nested queries (subqueries, CTEs)
|
|
11
|
+
* Each level of query scope has its own Map of table aliases/names to EntityMetadata
|
|
12
|
+
* Top of stack (highest index) is the current scope
|
|
13
|
+
*/
|
|
14
|
+
protected readonly contextStack: Map<string, EntityMetadata | undefined>[];
|
|
15
|
+
/**
|
|
16
|
+
* Subquery alias map: maps subquery/CTE alias to its source table metadata
|
|
17
|
+
* Used to resolve columns from subqueries/CTEs to their original table definitions
|
|
18
|
+
*/
|
|
19
|
+
protected readonly subqueryAliasMap: Map<string, EntityMetadata | undefined>;
|
|
20
|
+
protected readonly metadata: MetadataStorage;
|
|
21
|
+
protected readonly platform: AbstractSqlPlatform;
|
|
22
|
+
/**
|
|
23
|
+
* Global map of all entities involved in the query.
|
|
24
|
+
* Populated during AST transformation and used for result transformation.
|
|
25
|
+
*/
|
|
26
|
+
protected readonly entityMap: Map<string, EntityMetadata<any>>;
|
|
27
|
+
constructor(em: SqlEntityManager, options?: MikroKyselyPluginOptions);
|
|
28
|
+
reset(): void;
|
|
29
|
+
getOutputEntityMap(): Map<string, EntityMetadata>;
|
|
30
|
+
transformSelectQuery(node: SelectQueryNode, queryId: QueryId): SelectQueryNode;
|
|
31
|
+
transformInsertQuery(node: InsertQueryNode, queryId?: QueryId): InsertQueryNode;
|
|
32
|
+
transformUpdateQuery(node: UpdateQueryNode, queryId?: QueryId): UpdateQueryNode;
|
|
33
|
+
transformDeleteQuery(node: DeleteQueryNode, queryId?: QueryId): DeleteQueryNode;
|
|
34
|
+
transformMergeQuery(node: MergeQueryNode, queryId?: QueryId): MergeQueryNode;
|
|
35
|
+
transformIdentifier(node: IdentifierNode, queryId: QueryId): IdentifierNode;
|
|
36
|
+
/**
|
|
37
|
+
* Find owner entity metadata for the current identifier in the context stack.
|
|
38
|
+
* Supports both aliased and non-aliased table references.
|
|
39
|
+
* Searches up the context stack to support correlated subqueries.
|
|
40
|
+
* Also checks subquery/CTE aliases to resolve to their source tables.
|
|
41
|
+
*/
|
|
42
|
+
findOwnerEntityInContext(): EntityMetadata | undefined;
|
|
43
|
+
processOnCreateHooks(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
|
|
44
|
+
processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
45
|
+
processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
|
|
46
|
+
processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
47
|
+
mapColumnsToProperties(columns: readonly ColumnNode[], meta: EntityMetadata): (EntityProperty | undefined)[];
|
|
48
|
+
normalizeColumnName(identifier: IdentifierNode): string;
|
|
49
|
+
findProperty(meta: EntityMetadata | undefined, columnName?: string): EntityProperty | undefined;
|
|
50
|
+
shouldConvertValues(): boolean;
|
|
51
|
+
prepareInputValue(prop: EntityProperty | undefined, value: unknown, enabled: boolean): unknown;
|
|
52
|
+
/**
|
|
53
|
+
* Look up a table name/alias in the context stack.
|
|
54
|
+
* Searches from current scope (top of stack) to parent scopes (bottom).
|
|
55
|
+
* This supports correlated subqueries and references to outer query tables.
|
|
56
|
+
*/
|
|
57
|
+
lookupInContextStack(tableNameOrAlias: string): EntityMetadata | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Process WITH node (CTE definitions)
|
|
60
|
+
*/
|
|
61
|
+
processWithNode(withNode: WithNode, context: Map<string, EntityMetadata | undefined>): void;
|
|
62
|
+
/**
|
|
63
|
+
* Extract CTE name from CommonTableExpressionNameNode
|
|
64
|
+
*/
|
|
65
|
+
getCTEName(nameNode: CommonTableExpressionNameNode): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Process a FROM item (can be TableNode or AliasNode)
|
|
68
|
+
*/
|
|
69
|
+
processFromItem(from: any, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
|
|
70
|
+
context: Map<string, EntityMetadata | undefined>): void;
|
|
71
|
+
/**
|
|
72
|
+
* Process a JOIN node
|
|
73
|
+
*/
|
|
74
|
+
processJoinNode(join: JoinNode, context: Map<string, EntityMetadata | undefined>): void;
|
|
75
|
+
/**
|
|
76
|
+
* Extract the primary source table from a SELECT query
|
|
77
|
+
* This helps resolve columns from subqueries to their original entity tables
|
|
78
|
+
*/
|
|
79
|
+
extractSourceTableFromSelectQuery(selectQuery: SelectQueryNode): EntityMetadata | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Extract alias name from an alias node
|
|
82
|
+
*/
|
|
83
|
+
extractAliasName(alias: any): string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Extract table name from a TableNode
|
|
86
|
+
*/
|
|
87
|
+
getTableName(node: TableNode | undefined): string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Find entity metadata by table name or entity name
|
|
90
|
+
*/
|
|
91
|
+
findEntityMetadata(name: string): EntityMetadata | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Transform result rows by mapping database column names to property names
|
|
94
|
+
* This is called for SELECT queries when columnNamingStrategy is 'property'
|
|
95
|
+
*/
|
|
96
|
+
transformResult(rows: Record<string, any>[] | undefined, entityMap: Map<string, EntityMetadata>): Record<string, any>[] | undefined;
|
|
97
|
+
buildGlobalFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, EntityProperty>;
|
|
98
|
+
buildGlobalRelationFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, string>;
|
|
99
|
+
/**
|
|
100
|
+
* Build a mapping from database field names to property objects
|
|
101
|
+
* Format: { 'field_name': EntityProperty }
|
|
102
|
+
*/
|
|
103
|
+
buildFieldToPropertyMap(meta: EntityMetadata, alias?: string): Record<string, EntityProperty>;
|
|
104
|
+
/**
|
|
105
|
+
* Build a mapping for relation fields
|
|
106
|
+
* For ManyToOne relations, we need to map from the foreign key field to the relation property
|
|
107
|
+
* Format: { 'foreign_key_field': 'relationPropertyName' }
|
|
108
|
+
*/
|
|
109
|
+
buildRelationFieldMap(meta: EntityMetadata, alias?: string): Record<string, string>;
|
|
110
|
+
/**
|
|
111
|
+
* Transform a single row by mapping column names to property names
|
|
112
|
+
*/
|
|
113
|
+
transformRow(row: Record<string, any>, fieldToPropertyMap: Record<string, EntityProperty>, relationFieldMap: Record<string, string>): Record<string, any>;
|
|
114
|
+
prepareOutputValue(prop: EntityProperty | undefined, value: unknown): unknown;
|
|
115
|
+
}
|
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
import { ReferenceKind, isRaw, } from '@mikro-orm/core';
|
|
2
|
+
import { AliasNode, ColumnNode, ColumnUpdateNode, OperationNodeTransformer, PrimitiveValueListNode, ReferenceNode, SchemableIdentifierNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
|
|
3
|
+
export class MikroTransformer extends OperationNodeTransformer {
|
|
4
|
+
em;
|
|
5
|
+
options;
|
|
6
|
+
/**
|
|
7
|
+
* Context stack to support nested queries (subqueries, CTEs)
|
|
8
|
+
* Each level of query scope has its own Map of table aliases/names to EntityMetadata
|
|
9
|
+
* Top of stack (highest index) is the current scope
|
|
10
|
+
*/
|
|
11
|
+
contextStack = [];
|
|
12
|
+
/**
|
|
13
|
+
* Subquery alias map: maps subquery/CTE alias to its source table metadata
|
|
14
|
+
* Used to resolve columns from subqueries/CTEs to their original table definitions
|
|
15
|
+
*/
|
|
16
|
+
subqueryAliasMap = new Map();
|
|
17
|
+
metadata;
|
|
18
|
+
platform;
|
|
19
|
+
/**
|
|
20
|
+
* Global map of all entities involved in the query.
|
|
21
|
+
* Populated during AST transformation and used for result transformation.
|
|
22
|
+
*/
|
|
23
|
+
entityMap = new Map();
|
|
24
|
+
constructor(em, options = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.em = em;
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.metadata = em.getMetadata();
|
|
29
|
+
this.platform = em.getDriver().getPlatform();
|
|
30
|
+
}
|
|
31
|
+
reset() {
|
|
32
|
+
this.subqueryAliasMap.clear();
|
|
33
|
+
this.entityMap.clear();
|
|
34
|
+
}
|
|
35
|
+
getOutputEntityMap() {
|
|
36
|
+
return this.entityMap;
|
|
37
|
+
}
|
|
38
|
+
transformSelectQuery(node, queryId) {
|
|
39
|
+
// Push a new context for this query scope (starts with inherited parent context)
|
|
40
|
+
const currentContext = new Map();
|
|
41
|
+
this.contextStack.push(currentContext);
|
|
42
|
+
try {
|
|
43
|
+
// Process WITH clause (CTEs) first - they define names available in this scope
|
|
44
|
+
if (node.with) {
|
|
45
|
+
this.processWithNode(node.with, currentContext);
|
|
46
|
+
}
|
|
47
|
+
// Process FROM clause - main tables in this scope
|
|
48
|
+
if (node.from?.froms) {
|
|
49
|
+
for (const from of node.from.froms) {
|
|
50
|
+
this.processFromItem(from, currentContext);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Process JOINs - additional tables joined into this scope
|
|
54
|
+
if (node.joins) {
|
|
55
|
+
for (const join of node.joins) {
|
|
56
|
+
this.processJoinNode(join, currentContext);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return super.transformSelectQuery(node, queryId);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
// Pop the context when exiting this query scope
|
|
63
|
+
this.contextStack.pop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
transformInsertQuery(node, queryId) {
|
|
67
|
+
const currentContext = new Map();
|
|
68
|
+
this.contextStack.push(currentContext);
|
|
69
|
+
try {
|
|
70
|
+
let entityMeta;
|
|
71
|
+
if (node.into) {
|
|
72
|
+
const tableName = this.getTableName(node.into);
|
|
73
|
+
if (tableName) {
|
|
74
|
+
const meta = this.findEntityMetadata(tableName);
|
|
75
|
+
if (meta) {
|
|
76
|
+
entityMeta = meta;
|
|
77
|
+
currentContext.set(meta.tableName, meta);
|
|
78
|
+
this.entityMap.set(meta.tableName, meta);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const nodeWithHooks = this.options.processOnCreateHooks && entityMeta
|
|
83
|
+
? this.processOnCreateHooks(node, entityMeta)
|
|
84
|
+
: node;
|
|
85
|
+
const nodeWithConvertedValues = this.options.convertValues && entityMeta
|
|
86
|
+
? this.processInsertValues(nodeWithHooks, entityMeta)
|
|
87
|
+
: nodeWithHooks;
|
|
88
|
+
// Handle ON CONFLICT clause
|
|
89
|
+
let finalNode = nodeWithConvertedValues;
|
|
90
|
+
if (node.onConflict?.updates && entityMeta) {
|
|
91
|
+
// Create a temporary UpdateQueryNode to reuse processOnUpdateHooks and processUpdateValues
|
|
92
|
+
// We only care about the updates part
|
|
93
|
+
const tempUpdateNode = {
|
|
94
|
+
kind: 'UpdateQueryNode',
|
|
95
|
+
table: node.into, // Dummy table
|
|
96
|
+
updates: node.onConflict.updates,
|
|
97
|
+
};
|
|
98
|
+
const updatesWithHooks = this.options.processOnUpdateHooks
|
|
99
|
+
? this.processOnUpdateHooks(tempUpdateNode, entityMeta).updates
|
|
100
|
+
: node.onConflict.updates;
|
|
101
|
+
const tempUpdateNodeWithHooks = {
|
|
102
|
+
...tempUpdateNode,
|
|
103
|
+
updates: updatesWithHooks,
|
|
104
|
+
};
|
|
105
|
+
const updatesWithConvertedValues = this.options.convertValues
|
|
106
|
+
? this.processUpdateValues(tempUpdateNodeWithHooks, entityMeta).updates
|
|
107
|
+
: updatesWithHooks;
|
|
108
|
+
if (updatesWithConvertedValues && updatesWithConvertedValues !== node.onConflict.updates) {
|
|
109
|
+
// Construct the new OnConflictNode with updated values
|
|
110
|
+
finalNode = {
|
|
111
|
+
...finalNode,
|
|
112
|
+
onConflict: {
|
|
113
|
+
...node.onConflict,
|
|
114
|
+
updates: updatesWithConvertedValues,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return super.transformInsertQuery(finalNode, queryId);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.contextStack.pop();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
transformUpdateQuery(node, queryId) {
|
|
126
|
+
const currentContext = new Map();
|
|
127
|
+
this.contextStack.push(currentContext);
|
|
128
|
+
try {
|
|
129
|
+
let entityMeta;
|
|
130
|
+
if (node.table && TableNode.is(node.table)) {
|
|
131
|
+
const tableName = this.getTableName(node.table);
|
|
132
|
+
if (tableName) {
|
|
133
|
+
const meta = this.findEntityMetadata(tableName);
|
|
134
|
+
if (meta) {
|
|
135
|
+
entityMeta = meta;
|
|
136
|
+
currentContext.set(meta.tableName, meta);
|
|
137
|
+
this.entityMap.set(meta.tableName, meta);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Process FROM clause in UPDATE queries (for UPDATE with JOIN)
|
|
142
|
+
if (node.from) {
|
|
143
|
+
for (const fromItem of node.from.froms) {
|
|
144
|
+
this.processFromItem(fromItem, currentContext);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Also process JOINs in UPDATE queries
|
|
148
|
+
if (node.joins) {
|
|
149
|
+
for (const join of node.joins) {
|
|
150
|
+
this.processJoinNode(join, currentContext);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const nodeWithHooks = this.options.processOnUpdateHooks && entityMeta
|
|
154
|
+
? this.processOnUpdateHooks(node, entityMeta)
|
|
155
|
+
: node;
|
|
156
|
+
const nodeWithConvertedValues = this.options.convertValues && entityMeta
|
|
157
|
+
? this.processUpdateValues(nodeWithHooks, entityMeta)
|
|
158
|
+
: nodeWithHooks;
|
|
159
|
+
return super.transformUpdateQuery(nodeWithConvertedValues, queryId);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
this.contextStack.pop();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
transformDeleteQuery(node, queryId) {
|
|
166
|
+
const currentContext = new Map();
|
|
167
|
+
this.contextStack.push(currentContext);
|
|
168
|
+
try {
|
|
169
|
+
const froms = node.from?.froms;
|
|
170
|
+
if (froms && froms.length > 0) {
|
|
171
|
+
const firstFrom = froms[0];
|
|
172
|
+
if (TableNode.is(firstFrom)) {
|
|
173
|
+
const tableName = this.getTableName(firstFrom);
|
|
174
|
+
if (tableName) {
|
|
175
|
+
const meta = this.findEntityMetadata(tableName);
|
|
176
|
+
if (meta) {
|
|
177
|
+
currentContext.set(meta.tableName, meta);
|
|
178
|
+
this.entityMap.set(meta.tableName, meta);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Also process JOINs in DELETE queries
|
|
184
|
+
if (node.joins) {
|
|
185
|
+
for (const join of node.joins) {
|
|
186
|
+
this.processJoinNode(join, currentContext);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return super.transformDeleteQuery(node, queryId);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
this.contextStack.pop();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
transformMergeQuery(node, queryId) {
|
|
196
|
+
const currentContext = new Map();
|
|
197
|
+
this.contextStack.push(currentContext);
|
|
198
|
+
try {
|
|
199
|
+
return super.transformMergeQuery(node, queryId);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
this.contextStack.pop();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
transformIdentifier(node, queryId) {
|
|
206
|
+
node = super.transformIdentifier(node, queryId);
|
|
207
|
+
const parent = this.nodeStack[this.nodeStack.length - 2];
|
|
208
|
+
// Transform table names when tableNamingStrategy is 'entity'
|
|
209
|
+
if (this.options.tableNamingStrategy === 'entity' && parent && SchemableIdentifierNode.is(parent)) {
|
|
210
|
+
const meta = this.findEntityMetadata(node.name);
|
|
211
|
+
if (meta) {
|
|
212
|
+
return {
|
|
213
|
+
...node,
|
|
214
|
+
name: meta.tableName,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Transform column names when columnNamingStrategy is 'property'
|
|
219
|
+
// Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions)
|
|
220
|
+
if (this.options.columnNamingStrategy === 'property' && parent && (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))) {
|
|
221
|
+
const ownerMeta = this.findOwnerEntityInContext();
|
|
222
|
+
if (ownerMeta) {
|
|
223
|
+
const prop = ownerMeta.properties[node.name];
|
|
224
|
+
const fieldName = prop?.fieldNames?.[0];
|
|
225
|
+
if (fieldName) {
|
|
226
|
+
return {
|
|
227
|
+
...node,
|
|
228
|
+
name: fieldName,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return node;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Find owner entity metadata for the current identifier in the context stack.
|
|
237
|
+
* Supports both aliased and non-aliased table references.
|
|
238
|
+
* Searches up the context stack to support correlated subqueries.
|
|
239
|
+
* Also checks subquery/CTE aliases to resolve to their source tables.
|
|
240
|
+
*/
|
|
241
|
+
findOwnerEntityInContext() {
|
|
242
|
+
// Check if current column has a table reference (e.g., u.firstName)
|
|
243
|
+
const reference = this.nodeStack.find(it => ReferenceNode.is(it));
|
|
244
|
+
if (reference?.table && TableNode.is(reference.table)) {
|
|
245
|
+
const tableName = this.getTableName(reference.table);
|
|
246
|
+
if (tableName) {
|
|
247
|
+
// First, check in subquery alias map (for CTE/subquery columns)
|
|
248
|
+
if (this.subqueryAliasMap.has(tableName)) {
|
|
249
|
+
return this.subqueryAliasMap.get(tableName);
|
|
250
|
+
}
|
|
251
|
+
// Find entity metadata to get the actual table name
|
|
252
|
+
// Context uses table names (meta.tableName) as keys, not entity names
|
|
253
|
+
const entityMeta = this.findEntityMetadata(tableName);
|
|
254
|
+
if (entityMeta) {
|
|
255
|
+
// Search in context stack using the actual table name
|
|
256
|
+
const meta = this.lookupInContextStack(entityMeta.tableName);
|
|
257
|
+
if (meta) {
|
|
258
|
+
return meta;
|
|
259
|
+
}
|
|
260
|
+
// Also try with the entity name (for cases where context uses entity name)
|
|
261
|
+
const metaByEntityName = this.lookupInContextStack(tableName);
|
|
262
|
+
if (metaByEntityName) {
|
|
263
|
+
return metaByEntityName;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// If entity metadata not found, try direct lookup (for CTE/subquery cases)
|
|
268
|
+
const meta = this.lookupInContextStack(tableName);
|
|
269
|
+
if (meta) {
|
|
270
|
+
return meta;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// If no explicit table reference, use the first entity in current context
|
|
276
|
+
if (this.contextStack.length > 0) {
|
|
277
|
+
const currentContext = this.contextStack[this.contextStack.length - 1];
|
|
278
|
+
for (const [alias, meta] of currentContext.entries()) {
|
|
279
|
+
if (meta) {
|
|
280
|
+
return meta;
|
|
281
|
+
}
|
|
282
|
+
// If the context value is undefined but the alias is in subqueryAliasMap,
|
|
283
|
+
// use the mapped metadata (for CTE/subquery cases)
|
|
284
|
+
if (!meta && this.subqueryAliasMap.has(alias)) {
|
|
285
|
+
const mappedMeta = this.subqueryAliasMap.get(alias);
|
|
286
|
+
if (mappedMeta) {
|
|
287
|
+
return mappedMeta;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
processOnCreateHooks(node, meta) {
|
|
295
|
+
if (!node.columns || !node.values || !ValuesNode.is(node.values)) {
|
|
296
|
+
return node;
|
|
297
|
+
}
|
|
298
|
+
const existingProps = new Set();
|
|
299
|
+
for (const col of node.columns) {
|
|
300
|
+
const prop = this.findProperty(meta, this.normalizeColumnName(col.column));
|
|
301
|
+
if (prop) {
|
|
302
|
+
existingProps.add(prop.name);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const missingProps = meta.props.filter(prop => prop.onCreate && !existingProps.has(prop.name));
|
|
306
|
+
if (missingProps.length === 0) {
|
|
307
|
+
return node;
|
|
308
|
+
}
|
|
309
|
+
const newColumns = [...node.columns];
|
|
310
|
+
for (const prop of missingProps) {
|
|
311
|
+
newColumns.push(ColumnNode.create(prop.name));
|
|
312
|
+
}
|
|
313
|
+
const newRows = node.values.values.map(row => {
|
|
314
|
+
const valuesToAdd = missingProps.map(prop => {
|
|
315
|
+
const val = prop.onCreate(undefined, this.em);
|
|
316
|
+
return val;
|
|
317
|
+
});
|
|
318
|
+
if (ValueListNode.is(row)) {
|
|
319
|
+
const newValues = [...row.values, ...valuesToAdd.map(v => ValueNode.create(v))];
|
|
320
|
+
return ValueListNode.create(newValues);
|
|
321
|
+
}
|
|
322
|
+
if (PrimitiveValueListNode.is(row)) {
|
|
323
|
+
const newValues = [...row.values, ...valuesToAdd];
|
|
324
|
+
return PrimitiveValueListNode.create(newValues);
|
|
325
|
+
}
|
|
326
|
+
return row;
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
...node,
|
|
330
|
+
columns: Object.freeze(newColumns),
|
|
331
|
+
values: ValuesNode.create(newRows),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
processOnUpdateHooks(node, meta) {
|
|
335
|
+
if (!node.updates) {
|
|
336
|
+
return node;
|
|
337
|
+
}
|
|
338
|
+
const existingProps = new Set();
|
|
339
|
+
for (const update of node.updates) {
|
|
340
|
+
if (ColumnNode.is(update.column)) {
|
|
341
|
+
const prop = this.findProperty(meta, this.normalizeColumnName(update.column.column));
|
|
342
|
+
if (prop) {
|
|
343
|
+
existingProps.add(prop.name);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const missingProps = meta.props.filter(prop => prop.onUpdate && !existingProps.has(prop.name));
|
|
348
|
+
if (missingProps.length === 0) {
|
|
349
|
+
return node;
|
|
350
|
+
}
|
|
351
|
+
const newUpdates = [...node.updates];
|
|
352
|
+
for (const prop of missingProps) {
|
|
353
|
+
const val = prop.onUpdate(undefined, this.em);
|
|
354
|
+
newUpdates.push(ColumnUpdateNode.create(ColumnNode.create(prop.name), ValueNode.create(val)));
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
...node,
|
|
358
|
+
updates: Object.freeze(newUpdates),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
processInsertValues(node, meta) {
|
|
362
|
+
if (!node.columns?.length || !node.values || !ValuesNode.is(node.values)) {
|
|
363
|
+
return node;
|
|
364
|
+
}
|
|
365
|
+
const columnProps = this.mapColumnsToProperties(node.columns, meta);
|
|
366
|
+
const shouldConvert = this.shouldConvertValues();
|
|
367
|
+
let changed = false;
|
|
368
|
+
const convertedRows = node.values.values.map(row => {
|
|
369
|
+
if (ValueListNode.is(row)) {
|
|
370
|
+
if (row.values.length !== columnProps.length) {
|
|
371
|
+
return row;
|
|
372
|
+
}
|
|
373
|
+
const values = row.values.map((valueNode, idx) => {
|
|
374
|
+
if (!ValueNode.is(valueNode)) {
|
|
375
|
+
return valueNode;
|
|
376
|
+
}
|
|
377
|
+
const converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert);
|
|
378
|
+
if (converted === valueNode.value) {
|
|
379
|
+
return valueNode;
|
|
380
|
+
}
|
|
381
|
+
changed = true;
|
|
382
|
+
return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted);
|
|
383
|
+
});
|
|
384
|
+
return ValueListNode.create(values);
|
|
385
|
+
}
|
|
386
|
+
if (PrimitiveValueListNode.is(row)) {
|
|
387
|
+
if (row.values.length !== columnProps.length) {
|
|
388
|
+
return row;
|
|
389
|
+
}
|
|
390
|
+
const values = row.values.map((value, idx) => {
|
|
391
|
+
const converted = this.prepareInputValue(columnProps[idx], value, shouldConvert);
|
|
392
|
+
if (converted !== value) {
|
|
393
|
+
changed = true;
|
|
394
|
+
}
|
|
395
|
+
return converted;
|
|
396
|
+
});
|
|
397
|
+
return PrimitiveValueListNode.create(values);
|
|
398
|
+
}
|
|
399
|
+
return row;
|
|
400
|
+
});
|
|
401
|
+
if (!changed) {
|
|
402
|
+
return node;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
...node,
|
|
406
|
+
values: ValuesNode.create(convertedRows),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
processUpdateValues(node, meta) {
|
|
410
|
+
if (!node.updates?.length) {
|
|
411
|
+
return node;
|
|
412
|
+
}
|
|
413
|
+
const shouldConvert = this.shouldConvertValues();
|
|
414
|
+
let changed = false;
|
|
415
|
+
const updates = node.updates.map(updateNode => {
|
|
416
|
+
if (!ValueNode.is(updateNode.value)) {
|
|
417
|
+
return updateNode;
|
|
418
|
+
}
|
|
419
|
+
const columnName = ColumnNode.is(updateNode.column)
|
|
420
|
+
? this.normalizeColumnName(updateNode.column.column)
|
|
421
|
+
: undefined;
|
|
422
|
+
const property = this.findProperty(meta, columnName);
|
|
423
|
+
const converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert);
|
|
424
|
+
if (converted === updateNode.value.value) {
|
|
425
|
+
return updateNode;
|
|
426
|
+
}
|
|
427
|
+
changed = true;
|
|
428
|
+
const newValueNode = updateNode.value.immediate
|
|
429
|
+
? ValueNode.createImmediate(converted)
|
|
430
|
+
: ValueNode.create(converted);
|
|
431
|
+
return {
|
|
432
|
+
...updateNode,
|
|
433
|
+
value: newValueNode,
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
if (!changed) {
|
|
437
|
+
return node;
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
...node,
|
|
441
|
+
updates,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
mapColumnsToProperties(columns, meta) {
|
|
445
|
+
return columns.map(column => {
|
|
446
|
+
const columnName = this.normalizeColumnName(column.column);
|
|
447
|
+
return this.findProperty(meta, columnName);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
normalizeColumnName(identifier) {
|
|
451
|
+
const name = identifier.name;
|
|
452
|
+
if (!name.includes('.')) {
|
|
453
|
+
return name;
|
|
454
|
+
}
|
|
455
|
+
const parts = name.split('.');
|
|
456
|
+
return parts[parts.length - 1] ?? name;
|
|
457
|
+
}
|
|
458
|
+
findProperty(meta, columnName) {
|
|
459
|
+
if (!meta || !columnName) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
if (meta.properties[columnName]) {
|
|
463
|
+
return meta.properties[columnName];
|
|
464
|
+
}
|
|
465
|
+
return meta.props.find(prop => prop.fieldNames?.includes(columnName));
|
|
466
|
+
}
|
|
467
|
+
shouldConvertValues() {
|
|
468
|
+
return !!this.options.convertValues;
|
|
469
|
+
}
|
|
470
|
+
prepareInputValue(prop, value, enabled) {
|
|
471
|
+
if (!enabled || !prop || value == null) {
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
if (typeof value === 'object' && value !== null) {
|
|
475
|
+
if (isRaw(value)) {
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
if ('kind' in value) {
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (prop.customType && !isRaw(value)) {
|
|
483
|
+
return prop.customType.convertToDatabaseValue(value, this.platform, { fromQuery: true, key: prop.name, mode: 'query-data' });
|
|
484
|
+
}
|
|
485
|
+
if (value instanceof Date) {
|
|
486
|
+
return this.platform.processDateProperty(value);
|
|
487
|
+
}
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Look up a table name/alias in the context stack.
|
|
492
|
+
* Searches from current scope (top of stack) to parent scopes (bottom).
|
|
493
|
+
* This supports correlated subqueries and references to outer query tables.
|
|
494
|
+
*/
|
|
495
|
+
lookupInContextStack(tableNameOrAlias) {
|
|
496
|
+
// Search from top of stack (current scope) to bottom (parent scopes)
|
|
497
|
+
for (let i = this.contextStack.length - 1; i >= 0; i--) {
|
|
498
|
+
const context = this.contextStack[i];
|
|
499
|
+
if (context.has(tableNameOrAlias)) {
|
|
500
|
+
return context.get(tableNameOrAlias);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Process WITH node (CTE definitions)
|
|
507
|
+
*/
|
|
508
|
+
processWithNode(withNode, context) {
|
|
509
|
+
for (const cte of withNode.expressions) {
|
|
510
|
+
const cteName = this.getCTEName(cte.name);
|
|
511
|
+
if (cteName) {
|
|
512
|
+
// CTEs are not entities, so map to undefined
|
|
513
|
+
// They will be transformed recursively by transformSelectQuery
|
|
514
|
+
context.set(cteName, undefined);
|
|
515
|
+
// Also try to extract the source table from the CTE's expression
|
|
516
|
+
// This helps resolve columns in subsequent queries that use the CTE
|
|
517
|
+
if (cte.expression?.kind === 'SelectQueryNode') {
|
|
518
|
+
const sourceMeta = this.extractSourceTableFromSelectQuery(cte.expression);
|
|
519
|
+
if (sourceMeta) {
|
|
520
|
+
this.subqueryAliasMap.set(cteName, sourceMeta);
|
|
521
|
+
// Add CTE to entityMap so it can be used for result transformation if needed
|
|
522
|
+
// (though CTEs usually don't appear in result rows directly, but their columns might)
|
|
523
|
+
this.entityMap.set(cteName, sourceMeta);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Extract CTE name from CommonTableExpressionNameNode
|
|
531
|
+
*/
|
|
532
|
+
getCTEName(nameNode) {
|
|
533
|
+
if (TableNode.is(nameNode.table)) {
|
|
534
|
+
return this.getTableName(nameNode.table);
|
|
535
|
+
}
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Process a FROM item (can be TableNode or AliasNode)
|
|
540
|
+
*/
|
|
541
|
+
processFromItem(from, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
|
|
542
|
+
context) {
|
|
543
|
+
if (AliasNode.is(from)) {
|
|
544
|
+
if (TableNode.is(from.node)) {
|
|
545
|
+
// Regular table with alias
|
|
546
|
+
const tableName = this.getTableName(from.node);
|
|
547
|
+
if (tableName && from.alias) {
|
|
548
|
+
const meta = this.findEntityMetadata(tableName);
|
|
549
|
+
const aliasName = this.extractAliasName(from.alias);
|
|
550
|
+
if (aliasName) {
|
|
551
|
+
context.set(aliasName, meta);
|
|
552
|
+
if (meta) {
|
|
553
|
+
this.entityMap.set(aliasName, meta);
|
|
554
|
+
}
|
|
555
|
+
// Also map the alias in subqueryAliasMap if the table name is a CTE
|
|
556
|
+
if (this.subqueryAliasMap.has(tableName)) {
|
|
557
|
+
this.subqueryAliasMap.set(aliasName, this.subqueryAliasMap.get(tableName));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (from.node?.kind === 'SelectQueryNode') {
|
|
563
|
+
// Subquery with alias
|
|
564
|
+
const aliasName = this.extractAliasName(from.alias);
|
|
565
|
+
if (aliasName) {
|
|
566
|
+
context.set(aliasName, undefined);
|
|
567
|
+
// Try to extract the source table from the subquery
|
|
568
|
+
const sourceMeta = this.extractSourceTableFromSelectQuery(from.node);
|
|
569
|
+
if (sourceMeta) {
|
|
570
|
+
this.subqueryAliasMap.set(aliasName, sourceMeta);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// Other types with alias
|
|
576
|
+
const aliasName = this.extractAliasName(from.alias);
|
|
577
|
+
if (aliasName) {
|
|
578
|
+
context.set(aliasName, undefined);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else if (TableNode.is(from)) {
|
|
583
|
+
// Table without alias
|
|
584
|
+
const tableName = this.getTableName(from);
|
|
585
|
+
if (tableName) {
|
|
586
|
+
const meta = this.findEntityMetadata(tableName);
|
|
587
|
+
context.set(tableName, meta);
|
|
588
|
+
if (meta) {
|
|
589
|
+
this.entityMap.set(tableName, meta);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Process a JOIN node
|
|
596
|
+
*/
|
|
597
|
+
processJoinNode(join, context) {
|
|
598
|
+
const joinTable = join.table;
|
|
599
|
+
if (AliasNode.is(joinTable)) {
|
|
600
|
+
if (TableNode.is(joinTable.node)) {
|
|
601
|
+
// Regular table with alias in JOIN
|
|
602
|
+
const tableName = this.getTableName(joinTable.node);
|
|
603
|
+
if (tableName && joinTable.alias) {
|
|
604
|
+
const meta = this.findEntityMetadata(tableName);
|
|
605
|
+
const aliasName = this.extractAliasName(joinTable.alias);
|
|
606
|
+
if (aliasName) {
|
|
607
|
+
context.set(aliasName, meta);
|
|
608
|
+
if (meta) {
|
|
609
|
+
this.entityMap.set(aliasName, meta);
|
|
610
|
+
}
|
|
611
|
+
// Also map the alias in subqueryAliasMap if the table name is a CTE
|
|
612
|
+
if (this.subqueryAliasMap.has(tableName)) {
|
|
613
|
+
this.subqueryAliasMap.set(aliasName, this.subqueryAliasMap.get(tableName));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (joinTable.node?.kind === 'SelectQueryNode') {
|
|
619
|
+
// Subquery with alias in JOIN
|
|
620
|
+
const aliasName = this.extractAliasName(joinTable.alias);
|
|
621
|
+
if (aliasName) {
|
|
622
|
+
context.set(aliasName, undefined);
|
|
623
|
+
// Try to extract the source table from the subquery
|
|
624
|
+
const sourceMeta = this.extractSourceTableFromSelectQuery(joinTable.node);
|
|
625
|
+
if (sourceMeta) {
|
|
626
|
+
this.subqueryAliasMap.set(aliasName, sourceMeta);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
// Other types with alias
|
|
632
|
+
const aliasName = this.extractAliasName(joinTable.alias);
|
|
633
|
+
if (aliasName) {
|
|
634
|
+
context.set(aliasName, undefined);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else if (TableNode.is(joinTable)) {
|
|
639
|
+
// Table without alias in JOIN
|
|
640
|
+
const tableName = this.getTableName(joinTable);
|
|
641
|
+
if (tableName) {
|
|
642
|
+
const meta = this.findEntityMetadata(tableName);
|
|
643
|
+
// Use table name (meta.tableName) as key to match transformUpdateQuery behavior
|
|
644
|
+
if (meta) {
|
|
645
|
+
context.set(meta.tableName, meta);
|
|
646
|
+
this.entityMap.set(meta.tableName, meta);
|
|
647
|
+
// Also set with entity name for backward compatibility
|
|
648
|
+
context.set(tableName, meta);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
context.set(tableName, undefined);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Extract the primary source table from a SELECT query
|
|
658
|
+
* This helps resolve columns from subqueries to their original entity tables
|
|
659
|
+
*/
|
|
660
|
+
extractSourceTableFromSelectQuery(selectQuery) {
|
|
661
|
+
if (!selectQuery.from?.froms || selectQuery.from.froms.length === 0) {
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
// Get the first FROM table
|
|
665
|
+
const firstFrom = selectQuery.from.froms[0];
|
|
666
|
+
let sourceTable;
|
|
667
|
+
if (AliasNode.is(firstFrom) && TableNode.is(firstFrom.node)) {
|
|
668
|
+
sourceTable = firstFrom.node;
|
|
669
|
+
}
|
|
670
|
+
else if (TableNode.is(firstFrom)) {
|
|
671
|
+
sourceTable = firstFrom;
|
|
672
|
+
}
|
|
673
|
+
if (sourceTable) {
|
|
674
|
+
const tableName = this.getTableName(sourceTable);
|
|
675
|
+
if (tableName) {
|
|
676
|
+
return this.findEntityMetadata(tableName);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Extract alias name from an alias node
|
|
683
|
+
*/
|
|
684
|
+
extractAliasName(alias) {
|
|
685
|
+
if (typeof alias === 'object' && 'name' in alias) {
|
|
686
|
+
return alias.name;
|
|
687
|
+
}
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Extract table name from a TableNode
|
|
692
|
+
*/
|
|
693
|
+
getTableName(node) {
|
|
694
|
+
if (!node) {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
if (TableNode.is(node) && SchemableIdentifierNode.is(node.table)) {
|
|
698
|
+
const identifier = node.table.identifier;
|
|
699
|
+
if (typeof identifier === 'object' && 'name' in identifier) {
|
|
700
|
+
return identifier.name;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Find entity metadata by table name or entity name
|
|
707
|
+
*/
|
|
708
|
+
findEntityMetadata(name) {
|
|
709
|
+
const byEntity = this.metadata.find(name);
|
|
710
|
+
if (byEntity) {
|
|
711
|
+
return byEntity;
|
|
712
|
+
}
|
|
713
|
+
const allMetadata = Array.from(this.metadata);
|
|
714
|
+
const byTable = allMetadata.find(m => m.tableName === name);
|
|
715
|
+
if (byTable) {
|
|
716
|
+
return byTable;
|
|
717
|
+
}
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Transform result rows by mapping database column names to property names
|
|
722
|
+
* This is called for SELECT queries when columnNamingStrategy is 'property'
|
|
723
|
+
*/
|
|
724
|
+
transformResult(rows, entityMap) {
|
|
725
|
+
// Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data
|
|
726
|
+
if ((this.options.columnNamingStrategy !== 'property' && !this.options.convertValues) || !rows || rows.length === 0) {
|
|
727
|
+
return rows;
|
|
728
|
+
}
|
|
729
|
+
// If no entities found (e.g. raw query without known tables), return rows as is
|
|
730
|
+
if (entityMap.size === 0) {
|
|
731
|
+
return rows;
|
|
732
|
+
}
|
|
733
|
+
// Build a global mapping from database field names to property objects
|
|
734
|
+
const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap);
|
|
735
|
+
const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
|
|
736
|
+
// Transform each row
|
|
737
|
+
return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap));
|
|
738
|
+
}
|
|
739
|
+
buildGlobalFieldMap(entityMap) {
|
|
740
|
+
const map = {};
|
|
741
|
+
for (const [alias, meta] of entityMap.entries()) {
|
|
742
|
+
Object.assign(map, this.buildFieldToPropertyMap(meta, alias));
|
|
743
|
+
}
|
|
744
|
+
return map;
|
|
745
|
+
}
|
|
746
|
+
buildGlobalRelationFieldMap(entityMap) {
|
|
747
|
+
const map = {};
|
|
748
|
+
for (const [alias, meta] of entityMap.entries()) {
|
|
749
|
+
Object.assign(map, this.buildRelationFieldMap(meta, alias));
|
|
750
|
+
}
|
|
751
|
+
return map;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Build a mapping from database field names to property objects
|
|
755
|
+
* Format: { 'field_name': EntityProperty }
|
|
756
|
+
*/
|
|
757
|
+
buildFieldToPropertyMap(meta, alias) {
|
|
758
|
+
const map = {};
|
|
759
|
+
for (const prop of meta.props) {
|
|
760
|
+
if (prop.fieldNames && prop.fieldNames.length > 0) {
|
|
761
|
+
for (const fieldName of prop.fieldNames) {
|
|
762
|
+
if (!(fieldName in map)) {
|
|
763
|
+
map[fieldName] = prop;
|
|
764
|
+
}
|
|
765
|
+
if (alias) {
|
|
766
|
+
const dotted = `${alias}.${fieldName}`;
|
|
767
|
+
if (!(dotted in map)) {
|
|
768
|
+
map[dotted] = prop;
|
|
769
|
+
}
|
|
770
|
+
const underscored = `${alias}_${fieldName}`;
|
|
771
|
+
if (!(underscored in map)) {
|
|
772
|
+
map[underscored] = prop;
|
|
773
|
+
}
|
|
774
|
+
const doubleUnderscored = `${alias}__${fieldName}`;
|
|
775
|
+
if (!(doubleUnderscored in map)) {
|
|
776
|
+
map[doubleUnderscored] = prop;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (!(prop.name in map)) {
|
|
782
|
+
map[prop.name] = prop;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return map;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Build a mapping for relation fields
|
|
789
|
+
* For ManyToOne relations, we need to map from the foreign key field to the relation property
|
|
790
|
+
* Format: { 'foreign_key_field': 'relationPropertyName' }
|
|
791
|
+
*/
|
|
792
|
+
buildRelationFieldMap(meta, alias) {
|
|
793
|
+
const map = {};
|
|
794
|
+
for (const prop of meta.props) {
|
|
795
|
+
// For ManyToOne/OneToOne relations, find the foreign key field
|
|
796
|
+
if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE) {
|
|
797
|
+
if (prop.fieldNames && prop.fieldNames.length > 0) {
|
|
798
|
+
const fieldName = prop.fieldNames[0];
|
|
799
|
+
map[fieldName] = prop.name;
|
|
800
|
+
if (alias) {
|
|
801
|
+
map[`${alias}.${fieldName}`] = prop.name;
|
|
802
|
+
map[`${alias}_${fieldName}`] = prop.name;
|
|
803
|
+
map[`${alias}__${fieldName}`] = prop.name;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return map;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Transform a single row by mapping column names to property names
|
|
812
|
+
*/
|
|
813
|
+
transformRow(row, fieldToPropertyMap, relationFieldMap) {
|
|
814
|
+
const transformed = { ...row };
|
|
815
|
+
// First pass: map regular fields from fieldName to propertyName and convert values
|
|
816
|
+
for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) {
|
|
817
|
+
if (!(fieldName in transformed)) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const converted = this.prepareOutputValue(prop, transformed[fieldName]);
|
|
821
|
+
if (this.options.columnNamingStrategy === 'property' && prop.name !== fieldName) {
|
|
822
|
+
if (!(prop.name in transformed)) {
|
|
823
|
+
transformed[prop.name] = converted;
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
transformed[prop.name] = converted;
|
|
827
|
+
}
|
|
828
|
+
delete transformed[fieldName];
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (this.options.convertValues) {
|
|
832
|
+
transformed[fieldName] = converted;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// Second pass: handle relation fields
|
|
836
|
+
// Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
|
|
837
|
+
if (this.options.columnNamingStrategy === 'property') {
|
|
838
|
+
for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) {
|
|
839
|
+
if (fieldName in transformed && !(relationPropertyName in transformed)) {
|
|
840
|
+
// Move the foreign key value to the relation property name
|
|
841
|
+
transformed[relationPropertyName] = transformed[fieldName];
|
|
842
|
+
delete transformed[fieldName];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return transformed;
|
|
847
|
+
}
|
|
848
|
+
prepareOutputValue(prop, value) {
|
|
849
|
+
if (!this.options.convertValues || !prop || value == null) {
|
|
850
|
+
return value;
|
|
851
|
+
}
|
|
852
|
+
if (prop.customType) {
|
|
853
|
+
return prop.customType.convertToJSValue(value, this.platform);
|
|
854
|
+
}
|
|
855
|
+
// Aligned with EntityComparator.getResultMapper logic
|
|
856
|
+
if (prop.runtimeType === 'boolean') {
|
|
857
|
+
// Use !! conversion like EntityComparator: value == null ? value : !!value
|
|
858
|
+
return value == null ? value : !!value;
|
|
859
|
+
}
|
|
860
|
+
if (prop.runtimeType === 'Date' && !this.platform.isNumericProperty(prop)) {
|
|
861
|
+
// Aligned with EntityComparator: exclude numeric timestamp properties
|
|
862
|
+
// If already Date instance or null, return as is
|
|
863
|
+
if (value == null || value instanceof Date) {
|
|
864
|
+
return value;
|
|
865
|
+
}
|
|
866
|
+
// Handle timezone like EntityComparator.parseDate
|
|
867
|
+
const tz = this.platform.getTimezone();
|
|
868
|
+
if (!tz || tz === 'local') {
|
|
869
|
+
return this.platform.parseDate(value);
|
|
870
|
+
}
|
|
871
|
+
// For non-local timezone, check if value already has timezone info
|
|
872
|
+
// Number (timestamp) doesn't need timezone handling, string needs check
|
|
873
|
+
if (typeof value === 'number' || (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))) {
|
|
874
|
+
return this.platform.parseDate(value);
|
|
875
|
+
}
|
|
876
|
+
// Append timezone if not present (only for string values)
|
|
877
|
+
return this.platform.parseDate(value + tz);
|
|
878
|
+
}
|
|
879
|
+
// For all other runtimeTypes (number, string, bigint, Buffer, object, any, etc.)
|
|
880
|
+
// EntityComparator just assigns directly without conversion
|
|
881
|
+
return value;
|
|
882
|
+
}
|
|
883
|
+
}
|
package/typings.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { DatabaseSchema } from './schema/DatabaseSchema.js';
|
|
|
5
5
|
import type { DatabaseTable } from './schema/DatabaseTable.js';
|
|
6
6
|
import type { QueryBuilder } from './query/QueryBuilder.js';
|
|
7
7
|
import type { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
8
|
+
import type { MikroKyselyPluginOptions } from './plugin/index.js';
|
|
8
9
|
export interface Table {
|
|
9
10
|
table_name: string;
|
|
10
11
|
schema_name?: string;
|
|
@@ -195,7 +196,7 @@ export type MaybeReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
|
|
|
195
196
|
export type InferEntityProperties<Schema> = Schema extends EntitySchemaWithMeta<any, any, any, any, infer Properties> ? Properties : never;
|
|
196
197
|
export type InferKyselyDB<TEntities extends {
|
|
197
198
|
name: string;
|
|
198
|
-
}, TOptions = {}> = MapValueAsTable<MapByName<TEntities>, TOptions>;
|
|
199
|
+
}, TOptions extends MikroKyselyPluginOptions = {}> = MapValueAsTable<MapByName<TEntities>, TOptions>;
|
|
199
200
|
export type InferDBFromKysely<TKysely extends Kysely<any>> = TKysely extends Kysely<infer TDB> ? TDB : never;
|
|
200
201
|
type PreferStringLiteral<TCandidate, TFallback> = [
|
|
201
202
|
TCandidate
|
|
@@ -206,14 +207,14 @@ export type MapByName<T extends {
|
|
|
206
207
|
}> = {
|
|
207
208
|
[P in T as PreferStringLiteral<NonNullable<P['tableName']>, P['name']>]: P;
|
|
208
209
|
};
|
|
209
|
-
export type MapValueAsTable<TMap extends Record<string, any>, TOptions = {}> = {
|
|
210
|
-
[K in keyof TMap as TransformName<K, 'underscore'>]: InferKyselyTable<TMap[K],
|
|
210
|
+
export type MapValueAsTable<TMap extends Record<string, any>, TOptions extends MikroKyselyPluginOptions = {}> = {
|
|
211
|
+
[K in keyof TMap as TransformName<K, TOptions['tableNamingStrategy'] extends 'entity' ? 'entity' : 'underscore'>]: InferKyselyTable<TMap[K], TOptions>;
|
|
211
212
|
};
|
|
212
|
-
export type InferKyselyTable<TSchema extends EntitySchemaWithMeta,
|
|
213
|
-
-readonly [K in keyof InferEntityProperties<TSchema> as TransformColumnName<K,
|
|
213
|
+
export type InferKyselyTable<TSchema extends EntitySchemaWithMeta, TOptions extends MikroKyselyPluginOptions = {}> = ExcludeNever<{
|
|
214
|
+
-readonly [K in keyof InferEntityProperties<TSchema> as TransformColumnName<K, TOptions['columnNamingStrategy'] extends 'property' ? 'property' : 'underscore', MaybeReturnType<InferEntityProperties<TSchema>[K]>>]: InferColumnValue<MaybeReturnType<InferEntityProperties<TSchema>[K]>, TOptions['processOnCreateHooks'] extends true ? true : false>;
|
|
214
215
|
}>;
|
|
215
216
|
type TransformName<TName, TNamingStrategy extends 'underscore' | 'entity'> = TNamingStrategy extends 'underscore' ? TName extends string ? SnakeCase<TName> : TName : TName;
|
|
216
|
-
type TransformColumnName<TName, TNamingStrategy extends 'underscore' | '
|
|
217
|
+
type TransformColumnName<TName, TNamingStrategy extends 'underscore' | 'property', TBuilder> = TNamingStrategy extends 'property' ? TName : TBuilder extends {
|
|
217
218
|
'~options': {
|
|
218
219
|
fieldName: string;
|
|
219
220
|
};
|