@mikro-orm/sql 7.2.0-dev.9 → 7.2.0
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/AbstractSqlConnection.d.ts +14 -3
- package/AbstractSqlConnection.js +42 -4
- package/AbstractSqlDriver.d.ts +1 -8
- package/AbstractSqlDriver.js +63 -35
- package/AbstractSqlPlatform.d.ts +4 -2
- package/AbstractSqlPlatform.js +35 -1
- package/README.md +1 -0
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +4 -2
- package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
- package/dialects/mysql/BaseMySqlPlatform.js +4 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +18 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +153 -1
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
- package/package.json +3 -3
- package/plugin/transformer.d.ts +7 -1
- package/plugin/transformer.js +60 -1
- package/query/CriteriaNodeFactory.js +4 -0
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +30 -5
- package/query/QueryBuilder.d.ts +26 -3
- package/query/QueryBuilder.js +139 -23
- package/query/QueryBuilderHelper.d.ts +5 -0
- package/query/QueryBuilderHelper.js +33 -8
- package/schema/DatabaseSchema.d.ts +4 -0
- package/schema/DatabaseSchema.js +107 -1
- package/schema/DatabaseTable.d.ts +13 -1
- package/schema/DatabaseTable.js +50 -1
- package/schema/SchemaComparator.d.ts +2 -0
- package/schema/SchemaComparator.js +94 -4
- package/schema/SchemaHelper.d.ts +6 -0
- package/schema/SchemaHelper.js +15 -0
- package/schema/SqlSchemaGenerator.js +8 -0
- package/typings.d.ts +18 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Dictionary, type Transaction } from '@mikro-orm/core';
|
|
2
2
|
import { SchemaHelper } from '../../schema/SchemaHelper.js';
|
|
3
3
|
import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
|
|
4
|
-
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
|
|
4
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlPolicyDef, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from '../../schema/DatabaseTable.js';
|
|
7
7
|
export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
@@ -71,6 +71,17 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
71
71
|
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
72
72
|
/** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
|
|
73
73
|
private flattenDollarQuotedBodies;
|
|
74
|
+
getRlsCreateSQL(table: DatabaseTable): string[];
|
|
75
|
+
getRlsDropSQL(diff: TableDifference, safe?: boolean): string[];
|
|
76
|
+
getRlsAlterSQL(diff: TableDifference, safe?: boolean): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Quotes a policy or role name as a single identifier. Unlike `quote()`/`platform.quoteIdentifier`, which treat
|
|
79
|
+
* a dot as a schema qualifier, these names are never schema-qualified, so `my.role` must render as `"my.role"`.
|
|
80
|
+
*/
|
|
81
|
+
private quoteUnqualified;
|
|
82
|
+
private createPolicy;
|
|
83
|
+
private dropPolicy;
|
|
84
|
+
private formatPolicyRoles;
|
|
74
85
|
createRoutine(routine: SqlRoutineDef): string;
|
|
75
86
|
dropRoutine(routine: SqlRoutineDef): string;
|
|
76
87
|
getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
@@ -87,6 +98,12 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
87
98
|
getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
|
|
88
99
|
getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
|
|
89
100
|
private getTriggersSQL;
|
|
101
|
+
getAllPolicies(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<{
|
|
102
|
+
policies: SqlPolicyDef[];
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
forced: boolean;
|
|
105
|
+
}>>;
|
|
106
|
+
private parsePgRoles;
|
|
90
107
|
getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
|
|
91
108
|
getNativeEnumDefinitions(connection: AbstractSqlConnection, schemas: string[], ctx?: Transaction): Promise<Dictionary<{
|
|
92
109
|
name: string;
|
|
@@ -162,6 +162,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
162
162
|
const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
|
|
163
163
|
const partitionings = await this.getPartitions(connection, tablesBySchema, ctx);
|
|
164
164
|
const triggers = await this.getAllTriggers(connection, tablesBySchema);
|
|
165
|
+
const policies = await this.getAllPolicies(connection, tablesBySchema, ctx);
|
|
165
166
|
const dbCollation = await this.getDatabaseCollation(connection, ctx);
|
|
166
167
|
for (const t of tables) {
|
|
167
168
|
const key = this.getTableKey(t);
|
|
@@ -175,6 +176,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
175
176
|
if (triggers[key]) {
|
|
176
177
|
table.setTriggers(triggers[key]);
|
|
177
178
|
}
|
|
179
|
+
const rls = policies[key];
|
|
180
|
+
if (rls) {
|
|
181
|
+
table.setPolicies(rls.policies);
|
|
182
|
+
table.rlsEnabled = rls.enabled;
|
|
183
|
+
table.rlsForced = rls.forced;
|
|
184
|
+
}
|
|
178
185
|
table.setPartitioning(partitionings[key]);
|
|
179
186
|
}
|
|
180
187
|
}
|
|
@@ -494,7 +501,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
494
501
|
// SchemaHelper.createCheck).
|
|
495
502
|
const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
|
|
496
503
|
const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
|
|
497
|
-
const def = m ? m[1].replace(/\(([^()]*)\)::\w
|
|
504
|
+
const def = m ? m[1].replace(/\(([^()]*)\)::\w+(?:\[\])?/g, '$1') : single ? single[1] : check.expression;
|
|
498
505
|
ret[key].push({
|
|
499
506
|
name: check.name,
|
|
500
507
|
columnName: check.column_name,
|
|
@@ -540,6 +547,83 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
540
547
|
})
|
|
541
548
|
.join('');
|
|
542
549
|
}
|
|
550
|
+
getRlsCreateSQL(table) {
|
|
551
|
+
const ret = [];
|
|
552
|
+
if (table.rlsEnabled) {
|
|
553
|
+
ret.push(`alter table ${table.getQuotedName()} enable row level security`);
|
|
554
|
+
}
|
|
555
|
+
if (table.rlsForced) {
|
|
556
|
+
ret.push(`alter table ${table.getQuotedName()} force row level security`);
|
|
557
|
+
}
|
|
558
|
+
for (const policy of table.getPolicies()) {
|
|
559
|
+
ret.push(this.createPolicy(table, policy));
|
|
560
|
+
}
|
|
561
|
+
return ret;
|
|
562
|
+
}
|
|
563
|
+
getRlsDropSQL(diff, safe) {
|
|
564
|
+
// a policy expression holds a dependency on the columns it references, so removed policies (including the
|
|
565
|
+
// old version of changed ones) must be dropped before the column drops emitted later in the diff;
|
|
566
|
+
// in safe mode only recreated policies (present in both removed + added) are dropped, so a policy that is
|
|
567
|
+
// merely removed is left untouched like removed triggers/columns
|
|
568
|
+
return Object.values(diff.removedPolicies)
|
|
569
|
+
.filter(policy => !safe || policy.name in diff.addedPolicies)
|
|
570
|
+
.map(policy => this.dropPolicy(diff.toTable, policy));
|
|
571
|
+
}
|
|
572
|
+
getRlsAlterSQL(diff, safe) {
|
|
573
|
+
const ret = [];
|
|
574
|
+
const table = diff.toTable;
|
|
575
|
+
if (diff.changedRlsEnabled === true) {
|
|
576
|
+
ret.push(`alter table ${table.getQuotedName()} enable row level security`);
|
|
577
|
+
}
|
|
578
|
+
if (diff.changedRlsForced === true) {
|
|
579
|
+
ret.push(`alter table ${table.getQuotedName()} force row level security`);
|
|
580
|
+
}
|
|
581
|
+
else if (!safe && diff.changedRlsForced === false) {
|
|
582
|
+
ret.push(`alter table ${table.getQuotedName()} no force row level security`);
|
|
583
|
+
}
|
|
584
|
+
for (const policy of Object.values(diff.addedPolicies)) {
|
|
585
|
+
ret.push(this.createPolicy(table, policy));
|
|
586
|
+
}
|
|
587
|
+
// disable only after its policies are gone (removed ones were dropped via `getRlsDropSQL`); skipped in
|
|
588
|
+
// safe mode so a safe run never lifts row level security off an existing table
|
|
589
|
+
if (!safe && diff.changedRlsEnabled === false) {
|
|
590
|
+
ret.push(`alter table ${table.getQuotedName()} disable row level security`);
|
|
591
|
+
}
|
|
592
|
+
return ret;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Quotes a policy or role name as a single identifier. Unlike `quote()`/`platform.quoteIdentifier`, which treat
|
|
596
|
+
* a dot as a schema qualifier, these names are never schema-qualified, so `my.role` must render as `"my.role"`.
|
|
597
|
+
*/
|
|
598
|
+
quoteUnqualified(name) {
|
|
599
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
600
|
+
}
|
|
601
|
+
createPolicy(table, policy) {
|
|
602
|
+
const parts = [`create policy ${this.quoteUnqualified(policy.name)} on ${table.getQuotedName()}`];
|
|
603
|
+
if (policy.type === 'restrictive') {
|
|
604
|
+
parts.push('as restrictive');
|
|
605
|
+
}
|
|
606
|
+
if (policy.command !== 'all') {
|
|
607
|
+
parts.push(`for ${policy.command}`);
|
|
608
|
+
}
|
|
609
|
+
if (policy.roles.length > 0) {
|
|
610
|
+
parts.push(`to ${this.formatPolicyRoles(policy.roles)}`);
|
|
611
|
+
}
|
|
612
|
+
if (policy.using) {
|
|
613
|
+
parts.push(`using (${policy.using})`);
|
|
614
|
+
}
|
|
615
|
+
if (policy.check) {
|
|
616
|
+
parts.push(`with check (${policy.check})`);
|
|
617
|
+
}
|
|
618
|
+
return parts.join(' ');
|
|
619
|
+
}
|
|
620
|
+
dropPolicy(table, policy) {
|
|
621
|
+
return `drop policy ${this.quoteUnqualified(policy.name)} on ${table.getQuotedName()}`;
|
|
622
|
+
}
|
|
623
|
+
// `public` is a keyword and must stay unquoted; other roles are quoted like any identifier
|
|
624
|
+
formatPolicyRoles(roles) {
|
|
625
|
+
return roles.map(role => (role === 'public' ? 'public' : this.quoteUnqualified(role))).join(', ');
|
|
626
|
+
}
|
|
543
627
|
createRoutine(routine) {
|
|
544
628
|
if (routine.expression) {
|
|
545
629
|
return this.flattenDollarQuotedBodies(routine.expression);
|
|
@@ -732,6 +816,56 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
732
816
|
where (${conditions.join(' or ')})
|
|
733
817
|
order by t.trigger_name, t.event_manipulation`;
|
|
734
818
|
}
|
|
819
|
+
async getAllPolicies(connection, tablesBySchemas, ctx) {
|
|
820
|
+
const conditionsFor = (schemaColumn, tableColumn) => [...tablesBySchemas.entries()].map(([schema, tables]) => {
|
|
821
|
+
const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
|
|
822
|
+
const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
|
|
823
|
+
return `(${schemaColumn} = ${schemaName} and ${tableColumn} in (${names}))`;
|
|
824
|
+
});
|
|
825
|
+
const flagConditions = conditionsFor('ns.nspname', 'cls.relname');
|
|
826
|
+
const policyConditions = conditionsFor('schemaname', 'tablename');
|
|
827
|
+
// pg_class carries the enable/force flags (a table can enable RLS with zero policies)
|
|
828
|
+
const flagRows = await connection.execute(`select cls.relname as table_name, ns.nspname as schema_name, cls.relrowsecurity as enabled, cls.relforcerowsecurity as forced
|
|
829
|
+
from pg_class cls
|
|
830
|
+
join pg_namespace ns on ns.oid = cls.relnamespace
|
|
831
|
+
where (${flagConditions.join(' or ')})`, [], 'all', ctx);
|
|
832
|
+
const policyRows = await connection.execute(`select tablename as table_name, schemaname as schema_name, policyname as name, permissive, roles, cmd, qual, with_check
|
|
833
|
+
from pg_policies
|
|
834
|
+
where (${policyConditions.join(' or ')})
|
|
835
|
+
order by policyname`, [], 'all', ctx);
|
|
836
|
+
const policiesByTable = {};
|
|
837
|
+
for (const row of policyRows) {
|
|
838
|
+
const key = this.getTableKey(row);
|
|
839
|
+
(policiesByTable[key] ??= []).push({
|
|
840
|
+
name: row.name,
|
|
841
|
+
command: row.cmd.toLowerCase(),
|
|
842
|
+
type: row.permissive === 'PERMISSIVE' ? 'permissive' : 'restrictive',
|
|
843
|
+
roles: this.parsePgRoles(row.roles),
|
|
844
|
+
using: row.qual ?? undefined,
|
|
845
|
+
check: row.with_check ?? undefined,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
const ret = {};
|
|
849
|
+
for (const row of flagRows) {
|
|
850
|
+
const key = this.getTableKey(row);
|
|
851
|
+
ret[key] = { policies: policiesByTable[key] ?? [], enabled: row.enabled, forced: row.forced };
|
|
852
|
+
}
|
|
853
|
+
return ret;
|
|
854
|
+
}
|
|
855
|
+
// node-postgres returns `pg_policies.roles` as an unparsed array literal (`{public}`), pglite as an array
|
|
856
|
+
parsePgRoles(value) {
|
|
857
|
+
if (Array.isArray(value)) {
|
|
858
|
+
return value;
|
|
859
|
+
}
|
|
860
|
+
// tokenize the array literal instead of splitting on commas — quoted role names can contain
|
|
861
|
+
// commas, and quoted elements escape `"` and `\` with a backslash
|
|
862
|
+
const roles = [];
|
|
863
|
+
const re = /"((?:[^"\\]|\\.)*)"|[^,]+/g;
|
|
864
|
+
for (const match of value.replace(/^\{|\}$/g, '').matchAll(re)) {
|
|
865
|
+
roles.push(match[1] != null ? match[1].replace(/\\(.)/g, '$1') : match[0]);
|
|
866
|
+
}
|
|
867
|
+
return roles;
|
|
868
|
+
}
|
|
735
869
|
async getAllForeignKeys(connection, tablesBySchemas, ctx) {
|
|
736
870
|
const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
|
|
737
871
|
cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
|
|
@@ -966,6 +1100,24 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
966
1100
|
for (const { table: localTable, foreignKey } of inboundForeignKeys) {
|
|
967
1101
|
this.append(ret, this.createForeignKey(localTable, foreignKey));
|
|
968
1102
|
}
|
|
1103
|
+
// re-enable row level security and recreate the policies (createTable skips them during a rebuild);
|
|
1104
|
+
// emitted after the data copy so `force row level security` can't block the owner's insert
|
|
1105
|
+
this.append(ret, this.getRlsCreateSQL(table));
|
|
1106
|
+
// with `ignorePolicies`, hand-written policies (and the RLS flags they imply) may exist only on the
|
|
1107
|
+
// introspected side — recreate them verbatim, or the rebuild would silently strip them
|
|
1108
|
+
if (this.options.ignorePolicies) {
|
|
1109
|
+
if (diff.fromTable.rlsEnabled && !table.rlsEnabled) {
|
|
1110
|
+
ret.push(`alter table ${table.getQuotedName()} enable row level security`);
|
|
1111
|
+
}
|
|
1112
|
+
if (diff.fromTable.rlsForced && !table.rlsForced) {
|
|
1113
|
+
ret.push(`alter table ${table.getQuotedName()} force row level security`);
|
|
1114
|
+
}
|
|
1115
|
+
for (const policy of diff.fromTable.getPolicies()) {
|
|
1116
|
+
if (!table.hasPolicy(policy.name)) {
|
|
1117
|
+
ret.push(this.createPolicy(table, policy));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
969
1121
|
}
|
|
970
1122
|
if (safe) {
|
|
971
1123
|
ret.push(`-- safe mode: original tables kept in schema "${tmpSchema}"; drop that schema manually once the data is verified`);
|
|
@@ -6,6 +6,8 @@ import { SqliteExceptionConverter } from './SqliteExceptionConverter.js';
|
|
|
6
6
|
export declare class SqlitePlatform extends AbstractSqlPlatform {
|
|
7
7
|
protected readonly schemaHelper: SqliteSchemaHelper;
|
|
8
8
|
protected readonly exceptionConverter: SqliteExceptionConverter;
|
|
9
|
+
/** sqlite treats null as the lowest value when no placement is requested. */
|
|
10
|
+
sortsNullsLowest(): boolean;
|
|
9
11
|
/** @internal */
|
|
10
12
|
createNativeQueryBuilder(): SqliteNativeQueryBuilder;
|
|
11
13
|
usesDefaultKeyword(): boolean;
|
|
@@ -5,6 +5,10 @@ import { SqliteExceptionConverter } from './SqliteExceptionConverter.js';
|
|
|
5
5
|
export class SqlitePlatform extends AbstractSqlPlatform {
|
|
6
6
|
schemaHelper = new SqliteSchemaHelper(this);
|
|
7
7
|
exceptionConverter = new SqliteExceptionConverter();
|
|
8
|
+
/** sqlite treats null as the lowest value when no placement is requested. */
|
|
9
|
+
sortsNullsLowest() {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
8
12
|
/** @internal */
|
|
9
13
|
createNativeQueryBuilder() {
|
|
10
14
|
return new SqliteNativeQueryBuilder(this);
|
|
@@ -489,7 +489,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
489
489
|
* Foreign key references can only point to tables in the same database.
|
|
490
490
|
*/
|
|
491
491
|
getReferencedTableName(referencedTableName, schema) {
|
|
492
|
-
const [
|
|
492
|
+
const [, tableName] = this.splitTableName(referencedTableName);
|
|
493
493
|
// Strip any schema prefix - SQLite REFERENCES clause doesn't support it
|
|
494
494
|
return tableName;
|
|
495
495
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.2.0
|
|
3
|
+
"version": "7.2.0",
|
|
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
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"kysely": "0.29.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.
|
|
53
|
+
"@mikro-orm/core": "^7.2.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.0
|
|
56
|
+
"@mikro-orm/core": "7.2.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/plugin/transformer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EntityMetadata, type EntityProperty, type Type } from '@mikro-orm/core';
|
|
2
|
-
import { type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
|
|
2
|
+
import { type BinaryOperationNode, type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
|
|
3
3
|
import type { MikroKyselyPluginOptions } from './index.js';
|
|
4
4
|
import type { SqlEntityManager } from '../SqlEntityManager.js';
|
|
5
5
|
export declare class MikroTransformer extends OperationNodeTransformer {
|
|
@@ -28,6 +28,12 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
28
28
|
processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
29
29
|
processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
|
|
30
30
|
processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
31
|
+
transformBinaryOperation(node: BinaryOperationNode, queryId: QueryId): BinaryOperationNode;
|
|
32
|
+
/** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
|
|
33
|
+
resolveOperandProperty(operand: OperationNode): {
|
|
34
|
+
prop: EntityProperty;
|
|
35
|
+
fieldName: string;
|
|
36
|
+
} | undefined;
|
|
31
37
|
processInputValueNode(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
|
|
32
38
|
expandSelections(selections: readonly SelectionNode[]): readonly SelectionNode[];
|
|
33
39
|
expandSelection(sel: SelectionNode): SelectionNode[] | null;
|
package/plugin/transformer.js
CHANGED
|
@@ -455,6 +455,60 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
455
455
|
updates,
|
|
456
456
|
};
|
|
457
457
|
}
|
|
458
|
+
transformBinaryOperation(node, queryId) {
|
|
459
|
+
const transformed = super.transformBinaryOperation(node, queryId);
|
|
460
|
+
if (!this.#options.convertValues) {
|
|
461
|
+
return transformed;
|
|
462
|
+
}
|
|
463
|
+
const resolved = this.resolveOperandProperty(transformed.leftOperand);
|
|
464
|
+
if (!resolved) {
|
|
465
|
+
return transformed;
|
|
466
|
+
}
|
|
467
|
+
const { prop, fieldName } = resolved;
|
|
468
|
+
const right = transformed.rightOperand;
|
|
469
|
+
if (ValueNode.is(right)) {
|
|
470
|
+
const converted = this.processInputValueNode(prop, fieldName, right);
|
|
471
|
+
return converted === right ? transformed : { ...transformed, rightOperand: converted };
|
|
472
|
+
}
|
|
473
|
+
if (PrimitiveValueListNode.is(right)) {
|
|
474
|
+
// upgrade to ValueListNode when the type needs SQL-side wrapping, since
|
|
475
|
+
// PrimitiveValueListNode can only hold primitives
|
|
476
|
+
if (prop.hasConvertToDatabaseValueSQL) {
|
|
477
|
+
const values = right.values.map(value => this.processInputValueNode(prop, fieldName, ValueNode.create(value)));
|
|
478
|
+
return { ...transformed, rightOperand: ValueListNode.create(values) };
|
|
479
|
+
}
|
|
480
|
+
const values = right.values.map(value => this.prepareInputValue(prop, value, true));
|
|
481
|
+
return values.every((value, idx) => value === right.values[idx])
|
|
482
|
+
? transformed
|
|
483
|
+
: { ...transformed, rightOperand: PrimitiveValueListNode.create(values) };
|
|
484
|
+
}
|
|
485
|
+
if (ValueListNode.is(right)) {
|
|
486
|
+
let changed = false;
|
|
487
|
+
const values = right.values.map(valueNode => {
|
|
488
|
+
if (!ValueNode.is(valueNode)) {
|
|
489
|
+
return valueNode;
|
|
490
|
+
}
|
|
491
|
+
const converted = this.processInputValueNode(prop, fieldName, valueNode);
|
|
492
|
+
if (converted !== valueNode) {
|
|
493
|
+
changed = true;
|
|
494
|
+
}
|
|
495
|
+
return converted;
|
|
496
|
+
});
|
|
497
|
+
return changed ? { ...transformed, rightOperand: ValueListNode.create(values) } : transformed;
|
|
498
|
+
}
|
|
499
|
+
return transformed;
|
|
500
|
+
}
|
|
501
|
+
/** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
|
|
502
|
+
resolveOperandProperty(operand) {
|
|
503
|
+
if (!ReferenceNode.is(operand) || !ColumnNode.is(operand.column)) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
const tableName = operand.table ? this.getTableName(operand.table) : undefined;
|
|
507
|
+
const meta = this.findOwnerMeta(tableName);
|
|
508
|
+
const fieldName = this.normalizeColumnName(operand.column.column);
|
|
509
|
+
const prop = this.findProperty(meta, fieldName);
|
|
510
|
+
return prop ? { prop, fieldName } : undefined;
|
|
511
|
+
}
|
|
458
512
|
processInputValueNode(prop, fieldName, valueNode) {
|
|
459
513
|
const converted = this.prepareInputValue(prop, valueNode.value, true);
|
|
460
514
|
const newValueNode = converted === valueNode.value
|
|
@@ -543,8 +597,13 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
543
597
|
if (name) {
|
|
544
598
|
return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
|
|
545
599
|
}
|
|
600
|
+
// the stack can be empty when transforming a raw root node with embedded expressions
|
|
601
|
+
const context = this.#contextStack[this.#contextStack.length - 1];
|
|
602
|
+
if (!context) {
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
546
605
|
let single;
|
|
547
|
-
for (const meta of
|
|
606
|
+
for (const meta of context.values()) {
|
|
548
607
|
if (!meta) {
|
|
549
608
|
continue;
|
|
550
609
|
}
|
|
@@ -57,6 +57,10 @@ export class CriteriaNodeFactory {
|
|
|
57
57
|
if (isNotEmbedded && prop?.customType instanceof JsonType) {
|
|
58
58
|
return this.createScalarNode(metadata, childEntity, val, node, key, validate);
|
|
59
59
|
}
|
|
60
|
+
// operator payloads under an unresolvable alias-prefixed key (e.g. `a.meta`) are opaque values, not entity criteria
|
|
61
|
+
if (!prop && !rawField && Utils.isOperator(key, false) && String(node.key).includes('.')) {
|
|
62
|
+
return this.createScalarNode(metadata, entityName, val, node, key, validate);
|
|
63
|
+
}
|
|
60
64
|
if (prop?.kind === ReferenceKind.SCALAR && val != null && Object.keys(val).some(f => f in GroupOperator)) {
|
|
61
65
|
throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload);
|
|
62
66
|
}
|
|
@@ -13,6 +13,7 @@ export declare class ObjectCriteriaNode<T extends object> extends CriteriaNode<T
|
|
|
13
13
|
private inlineArrayChildPayload;
|
|
14
14
|
private inlineChildPayload;
|
|
15
15
|
private inlineCondition;
|
|
16
|
+
private isCollectionOperator;
|
|
16
17
|
private shouldAutoJoin;
|
|
17
18
|
private autoJoin;
|
|
18
19
|
private isPrefixed;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { CriteriaNode } from './CriteriaNode.js';
|
|
3
3
|
import { JoinType, QueryType } from './enums.js';
|
|
4
|
-
const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size'];
|
|
4
|
+
const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size', '$all'];
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
@@ -17,7 +17,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
17
17
|
alias = nestedAlias;
|
|
18
18
|
}
|
|
19
19
|
if (this.shouldAutoJoin(qb, nestedAlias)) {
|
|
20
|
-
if (keys.some(k =>
|
|
20
|
+
if (keys.some(k => this.isCollectionOperator(k))) {
|
|
21
21
|
if (![ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(this.prop.kind)) {
|
|
22
22
|
// ignore collection operators when used on a non-relational property - this can happen when they get into
|
|
23
23
|
// populateWhere via `infer` on m:n properties with select-in strategy
|
|
@@ -34,11 +34,24 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
34
34
|
const primaryKeys = parentMeta.primaryKeys.map(pk => {
|
|
35
35
|
return [QueryType.SELECT, QueryType.COUNT].includes(qb.type) ? `${knownKey ? alias : ownerAlias}.${pk}` : pk;
|
|
36
36
|
});
|
|
37
|
+
const conditions = [];
|
|
38
|
+
let matchNothing = false;
|
|
37
39
|
for (const key of keys) {
|
|
38
40
|
if (typeof key !== 'string' || !COLLECTION_OPERATORS.includes(key)) {
|
|
39
41
|
throw new Error('Mixing collection operators with other filters is not allowed.');
|
|
40
42
|
}
|
|
41
43
|
const payload = this.payload[key].unwrap();
|
|
44
|
+
// `$all` requires every listed item to be present, which is an intersection of `$some` conditions
|
|
45
|
+
if (key === '$all') {
|
|
46
|
+
// an empty `$all` matches nothing, same as in mongo
|
|
47
|
+
matchNothing ||= payload.length === 0;
|
|
48
|
+
conditions.push(...payload.map(item => ['$some', item]));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
conditions.push([key, payload]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [key, payload] of conditions) {
|
|
42
55
|
// entities with a fixed schema must resolve the `from` table's own schema in the subquery,
|
|
43
56
|
// otherwise a nested operator inherits the root entity's schema (GH #7894); for wildcard or
|
|
44
57
|
// schema-less entities the schema is resolved dynamically and needs to be carried over
|
|
@@ -70,6 +83,9 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
70
83
|
[Utils.getPrimaryKeyHash(primaryKeys)]: { [op]: sub.getNativeQuery().toRaw() },
|
|
71
84
|
});
|
|
72
85
|
}
|
|
86
|
+
if (matchNothing) {
|
|
87
|
+
$and.push({ [Utils.getPrimaryKeyHash(primaryKeys)]: { $in: [] } });
|
|
88
|
+
}
|
|
73
89
|
if ($and.length === 1) {
|
|
74
90
|
return $and[0];
|
|
75
91
|
}
|
|
@@ -113,7 +129,8 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
113
129
|
}
|
|
114
130
|
else if (isRawField) {
|
|
115
131
|
const rawField = RawQueryFragment.getKnownFragment(field);
|
|
116
|
-
|
|
132
|
+
qb.ensureTPTJoins();
|
|
133
|
+
o[raw(qb.helper.replaceAliases(rawField.sql, alias), rawField.params)] = payload;
|
|
117
134
|
}
|
|
118
135
|
else if (!childNode.validate && !childNode.prop && !field.includes('.') && !operator) {
|
|
119
136
|
// wrap unknown fields in raw() to prevent alias prefixing (e.g. raw SQL aliases in HAVING)
|
|
@@ -153,7 +170,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
153
170
|
alias = nestedAlias;
|
|
154
171
|
}
|
|
155
172
|
if (this.shouldAutoJoin(qb, nestedAlias)) {
|
|
156
|
-
return !keys.some(k =>
|
|
173
|
+
return !keys.some(k => this.isCollectionOperator(k));
|
|
157
174
|
}
|
|
158
175
|
return keys.some(field => {
|
|
159
176
|
const childNode = this.payload[field];
|
|
@@ -237,6 +254,14 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
237
254
|
delete o[key];
|
|
238
255
|
o.$and = $and;
|
|
239
256
|
}
|
|
257
|
+
isCollectionOperator(key) {
|
|
258
|
+
if (typeof key !== 'string' || !COLLECTION_OPERATORS.includes(key)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
// `$all` is primarily a mongo array operator, in SQL it is supported only on collections
|
|
262
|
+
return (key !== '$all' ||
|
|
263
|
+
(!!this.prop && [ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(this.prop.kind)));
|
|
264
|
+
}
|
|
240
265
|
shouldAutoJoin(qb, nestedAlias) {
|
|
241
266
|
if (!this.prop || !this.parent) {
|
|
242
267
|
return false;
|
|
@@ -245,7 +270,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
245
270
|
if (keys.every(k => typeof k === 'string' && k.includes('.') && k.startsWith(`${qb.alias}.`))) {
|
|
246
271
|
return false;
|
|
247
272
|
}
|
|
248
|
-
if (keys.some(k =>
|
|
273
|
+
if (keys.some(k => this.isCollectionOperator(k))) {
|
|
249
274
|
return true;
|
|
250
275
|
}
|
|
251
276
|
const meta = this.metadata.find(this.entityName);
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -439,7 +439,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
439
439
|
*/
|
|
440
440
|
join<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
441
441
|
/**
|
|
442
|
-
* Adds a JOIN clause to the query for a subquery.
|
|
442
|
+
* Adds a JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
|
|
443
443
|
*/
|
|
444
444
|
join<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
445
445
|
/**
|
|
@@ -447,7 +447,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
447
447
|
*/
|
|
448
448
|
innerJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
449
449
|
/**
|
|
450
|
-
* Adds an INNER JOIN clause to the query for a subquery.
|
|
450
|
+
* Adds an INNER JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
|
|
451
451
|
*/
|
|
452
452
|
innerJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
453
453
|
innerJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
@@ -456,7 +456,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
456
456
|
*/
|
|
457
457
|
leftJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
458
458
|
/**
|
|
459
|
-
* Adds a LEFT JOIN clause to the query for a subquery.
|
|
459
|
+
* Adds a LEFT JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
|
|
460
460
|
*/
|
|
461
461
|
leftJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
462
462
|
leftJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
@@ -782,6 +782,8 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
782
782
|
* @internal
|
|
783
783
|
*/
|
|
784
784
|
getJoinForPath(path: string, options?: ICriteriaNodeProcessOptions): JoinOptions | undefined;
|
|
785
|
+
/** Branch-insensitive path matching still must not cross branches — segments with different explicit branch markers (e.g. `Mobile[0]` vs `Mobile[1]`) belong to sibling joins. */
|
|
786
|
+
private branchesConflict;
|
|
785
787
|
/**
|
|
786
788
|
* @internal
|
|
787
789
|
*/
|
|
@@ -948,6 +950,13 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
948
950
|
*/
|
|
949
951
|
addPropertyJoin(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): string;
|
|
950
952
|
private joinReference;
|
|
953
|
+
/**
|
|
954
|
+
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
955
|
+
* Registers the parent aliases in `state.tptAlias` so column resolution finds them
|
|
956
|
+
* when conditions reference parent-table columns.
|
|
957
|
+
* @internal
|
|
958
|
+
*/
|
|
959
|
+
addTPTParentJoins(leafMeta: EntityMetadata, leafAlias: string, basePath: string): void;
|
|
951
960
|
protected prepareFields<T>(fields: InternalField<T>[], type?: 'where' | 'groupBy' | 'sub-query', schema?: string): (string | RawQueryFragment)[];
|
|
952
961
|
/**
|
|
953
962
|
* Resolves nested paths like `a.books.title` to their actual field references.
|
|
@@ -994,6 +1003,20 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
994
1003
|
processPopulateHint(): void;
|
|
995
1004
|
private processPopulateWhere;
|
|
996
1005
|
private mergeOnConditions;
|
|
1006
|
+
/**
|
|
1007
|
+
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
1008
|
+
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
1009
|
+
* and nested operators cannot be preserved inside a distributed disjunction, as `mergeOnConditions`
|
|
1010
|
+
* would flatten them into `and` conjuncts.
|
|
1011
|
+
*/
|
|
1012
|
+
private canDistributeOrBranches;
|
|
1013
|
+
private getOrBranchAliases;
|
|
1014
|
+
/**
|
|
1015
|
+
* An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
|
|
1016
|
+
* outermost join common to all targeted joins, nesting the targeted joins under it so the clause
|
|
1017
|
+
* can reference their aliases.
|
|
1018
|
+
*/
|
|
1019
|
+
private mergeFilterOrCondition;
|
|
997
1020
|
/**
|
|
998
1021
|
* When adding an inner join on a left joined relation, we need to nest them,
|
|
999
1022
|
* otherwise the inner join could discard rows of the root table.
|