@mikro-orm/sql 7.0.0-dev.336 → 7.0.0-dev.338
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/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type SchemaHelper } from './schema/SchemaHelper.js';
|
|
|
4
4
|
import type { IndexDef } from './typings.js';
|
|
5
5
|
import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
6
6
|
export declare abstract class AbstractSqlPlatform extends Platform {
|
|
7
|
+
#private;
|
|
7
8
|
protected readonly schemaHelper?: SchemaHelper;
|
|
8
9
|
usesPivotTable(): boolean;
|
|
9
10
|
indexForeignKeys(): boolean;
|
|
@@ -48,6 +49,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
48
49
|
quoteCollation(collation: string): string;
|
|
49
50
|
/** @internal */
|
|
50
51
|
protected validateCollationName(collation: string): void;
|
|
52
|
+
/** @internal */
|
|
53
|
+
validateJsonPropertyName(name: string): void;
|
|
51
54
|
/**
|
|
52
55
|
* Returns FROM clause for JSON array iteration.
|
|
53
56
|
* @internal
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SqlEntityRepository } from './SqlEntityRepository.js';
|
|
|
3
3
|
import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js';
|
|
4
4
|
import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
5
5
|
export class AbstractSqlPlatform extends Platform {
|
|
6
|
+
static #JSON_PROPERTY_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
6
7
|
schemaHelper;
|
|
7
8
|
usesPivotTable() {
|
|
8
9
|
return true;
|
|
@@ -123,6 +124,12 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
123
124
|
throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
/** @internal */
|
|
128
|
+
validateJsonPropertyName(name) {
|
|
129
|
+
if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) {
|
|
130
|
+
throw new Error(`Invalid JSON property name: '${name}'. JSON property names must contain only alphanumeric characters and underscores.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
126
133
|
/**
|
|
127
134
|
* Returns FROM clause for JSON array iteration.
|
|
128
135
|
* @internal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.0.0-dev.
|
|
3
|
+
"version": "7.0.0-dev.338",
|
|
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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@mikro-orm/core": "^6.6.9"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.0.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.0.0-dev.338"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
|
@@ -59,8 +59,14 @@ export declare class QueryBuilderHelper {
|
|
|
59
59
|
private processEmbeddedArrayCondition;
|
|
60
60
|
private buildJsonArrayExists;
|
|
61
61
|
private resolveEmbeddedProp;
|
|
62
|
-
private buildEmbeddedArrayWhere;
|
|
63
62
|
private buildEmbeddedArrayOperatorCondition;
|
|
63
|
+
private processJsonElemMatch;
|
|
64
|
+
/**
|
|
65
|
+
* Shared logic for building WHERE conditions inside JSON array EXISTS subqueries.
|
|
66
|
+
* Used by both embedded array queries (metadata-driven) and $elemMatch (type-inferred).
|
|
67
|
+
*/
|
|
68
|
+
private buildArrayElementWhere;
|
|
69
|
+
private inferJsonValueType;
|
|
64
70
|
processOnConflictCondition(cond: FilterQuery<any>, schema?: string): FilterQuery<any>;
|
|
65
71
|
createFormulaTable(alias: string, meta: EntityMetadata, schema?: string): FormulaTable;
|
|
66
72
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, JsonType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
2
2
|
import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
@@ -448,6 +448,15 @@ export class QueryBuilderHelper {
|
|
|
448
448
|
return this.processEmbeddedArrayCondition(cond[key], prop, a);
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
|
+
// $elemMatch on JSON properties — iterate array elements via EXISTS subquery.
|
|
452
|
+
// When combined with other operators (e.g. $contains), processObjectSubCondition
|
|
453
|
+
// splits them first (size > 1), so $elemMatch arrives here alone.
|
|
454
|
+
if (prop && cond[key].$elemMatch != null && Utils.getObjectKeysSize(cond[key]) === 1) {
|
|
455
|
+
if (!(prop.customType instanceof JsonType)) {
|
|
456
|
+
throw new ValidationError(`$elemMatch can only be used on JSON array properties, but '${this.#entityName}.${prop.name}' has type '${prop.type}'`);
|
|
457
|
+
}
|
|
458
|
+
return this.processJsonElemMatch(cond[key].$elemMatch, prop, a);
|
|
459
|
+
}
|
|
451
460
|
}
|
|
452
461
|
if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) {
|
|
453
462
|
return this.processObjectSubCondition(cond, key, type);
|
|
@@ -841,14 +850,18 @@ export class QueryBuilderHelper {
|
|
|
841
850
|
return [QueryType.SELECT, QueryType.COUNT].includes(type);
|
|
842
851
|
}
|
|
843
852
|
processEmbeddedArrayCondition(cond, prop, alias) {
|
|
844
|
-
const
|
|
845
|
-
const
|
|
853
|
+
const column = this.#platform.quoteIdentifier(`${alias}.${prop.fieldNames[0]}`);
|
|
854
|
+
const resolveProperty = (key) => {
|
|
855
|
+
const { embProp, jsonPropName } = this.resolveEmbeddedProp(prop, key);
|
|
856
|
+
return { name: jsonPropName, type: embProp.runtimeType ?? 'string' };
|
|
857
|
+
};
|
|
858
|
+
const invalidObjectError = (key) => ValidationError.invalidEmbeddableQuery(this.#entityName, key, prop.type);
|
|
846
859
|
const parts = [];
|
|
847
860
|
const allParams = [];
|
|
848
861
|
// Top-level $not generates NOT EXISTS (no element matches the inner condition).
|
|
849
862
|
const { $not, ...rest } = cond;
|
|
850
863
|
if (Utils.hasObjectKeys(rest)) {
|
|
851
|
-
const result = this.buildJsonArrayExists(rest,
|
|
864
|
+
const result = this.buildJsonArrayExists(rest, column, false, resolveProperty, invalidObjectError);
|
|
852
865
|
if (result) {
|
|
853
866
|
parts.push(result.sql);
|
|
854
867
|
allParams.push(...result.params);
|
|
@@ -858,7 +871,7 @@ export class QueryBuilderHelper {
|
|
|
858
871
|
if (!Utils.isPlainObject($not)) {
|
|
859
872
|
throw new ValidationError(`Invalid query: $not in embedded array queries expects an object value`);
|
|
860
873
|
}
|
|
861
|
-
const result = this.buildJsonArrayExists($not,
|
|
874
|
+
const result = this.buildJsonArrayExists($not, column, true, resolveProperty, invalidObjectError);
|
|
862
875
|
if (result) {
|
|
863
876
|
parts.push(result.sql);
|
|
864
877
|
allParams.push(...result.params);
|
|
@@ -869,10 +882,10 @@ export class QueryBuilderHelper {
|
|
|
869
882
|
}
|
|
870
883
|
return { sql: parts.join(' and '), params: allParams };
|
|
871
884
|
}
|
|
872
|
-
buildJsonArrayExists(cond,
|
|
885
|
+
buildJsonArrayExists(cond, column, negate, resolveProperty, invalidObjectError) {
|
|
873
886
|
const jeAlias = `__je${this.#jsonAliasCounter++}`;
|
|
874
887
|
const referencedProps = new Map();
|
|
875
|
-
const { sql: whereSql, params } = this.
|
|
888
|
+
const { sql: whereSql, params } = this.buildArrayElementWhere(cond, jeAlias, referencedProps, resolveProperty, invalidObjectError);
|
|
876
889
|
if (!whereSql) {
|
|
877
890
|
return null;
|
|
878
891
|
}
|
|
@@ -890,7 +903,55 @@ export class QueryBuilderHelper {
|
|
|
890
903
|
const jsonPropName = raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
|
|
891
904
|
return { embProp, jsonPropName };
|
|
892
905
|
}
|
|
893
|
-
|
|
906
|
+
buildEmbeddedArrayOperatorCondition(lhs, value, params) {
|
|
907
|
+
const supported = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$not', '$like', '$exists']);
|
|
908
|
+
const parts = [];
|
|
909
|
+
// Clone to avoid getOperatorReplacement mutating the original (it sets value[op] = null for $exists).
|
|
910
|
+
value = { ...value };
|
|
911
|
+
for (const op of Object.keys(value)) {
|
|
912
|
+
if (!supported.has(op)) {
|
|
913
|
+
throw new ValidationError(`Operator ${op} is not supported in embedded array queries`);
|
|
914
|
+
}
|
|
915
|
+
const replacement = this.getOperatorReplacement(op, value);
|
|
916
|
+
const val = value[op];
|
|
917
|
+
if (['$in', '$nin'].includes(op)) {
|
|
918
|
+
if (!Array.isArray(val)) {
|
|
919
|
+
throw new ValidationError(`Invalid query: ${op} operator expects an array value`);
|
|
920
|
+
}
|
|
921
|
+
else if (val.length === 0) {
|
|
922
|
+
parts.push(`1 = ${op === '$in' ? 0 : 1}`);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
val.forEach((v) => params.push(v));
|
|
926
|
+
parts.push(`${lhs} ${replacement} (${val.map(() => '?').join(', ')})`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
else if (op === '$exists') {
|
|
930
|
+
parts.push(`${lhs} ${replacement} null`);
|
|
931
|
+
}
|
|
932
|
+
else if (val === null) {
|
|
933
|
+
parts.push(`${lhs} ${replacement} null`);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
parts.push(`${lhs} ${replacement} ?`);
|
|
937
|
+
params.push(val);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return parts.join(' and ');
|
|
941
|
+
}
|
|
942
|
+
processJsonElemMatch(cond, prop, alias) {
|
|
943
|
+
const column = this.#platform.quoteIdentifier(`${alias}.${prop.fieldNames[0]}`);
|
|
944
|
+
const result = this.buildJsonArrayExists(cond, column, false, (key, value) => {
|
|
945
|
+
this.#platform.validateJsonPropertyName(key);
|
|
946
|
+
return { name: key, type: this.inferJsonValueType(value) };
|
|
947
|
+
}, () => ValidationError.invalidQueryCondition(cond));
|
|
948
|
+
return result ?? { sql: '1 = 1', params: [] };
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Shared logic for building WHERE conditions inside JSON array EXISTS subqueries.
|
|
952
|
+
* Used by both embedded array queries (metadata-driven) and $elemMatch (type-inferred).
|
|
953
|
+
*/
|
|
954
|
+
buildArrayElementWhere(cond, jeAlias, referencedProps, resolveProperty, invalidObjectError) {
|
|
894
955
|
const parts = [];
|
|
895
956
|
const params = [];
|
|
896
957
|
for (const k of Object.keys(cond)) {
|
|
@@ -901,7 +962,7 @@ export class QueryBuilderHelper {
|
|
|
901
962
|
}
|
|
902
963
|
const subParts = [];
|
|
903
964
|
for (const item of items) {
|
|
904
|
-
const sub = this.
|
|
965
|
+
const sub = this.buildArrayElementWhere(item, jeAlias, referencedProps, resolveProperty, invalidObjectError);
|
|
905
966
|
if (sub.sql) {
|
|
906
967
|
subParts.push(sub.sql);
|
|
907
968
|
params.push(...sub.params);
|
|
@@ -916,22 +977,21 @@ export class QueryBuilderHelper {
|
|
|
916
977
|
// Within $or/$and scope, $not provides element-level negation:
|
|
917
978
|
// "this element does not match the condition".
|
|
918
979
|
if (k === '$not') {
|
|
919
|
-
const sub = this.
|
|
980
|
+
const sub = this.buildArrayElementWhere(cond[k], jeAlias, referencedProps, resolveProperty, invalidObjectError);
|
|
920
981
|
if (sub.sql) {
|
|
921
982
|
parts.push(`not (${sub.sql})`);
|
|
922
983
|
params.push(...sub.params);
|
|
923
984
|
}
|
|
924
985
|
continue;
|
|
925
986
|
}
|
|
926
|
-
const { embProp, jsonPropName } = this.resolveEmbeddedProp(prop, k);
|
|
927
|
-
referencedProps.set(k, { name: jsonPropName, type: embProp.runtimeType ?? 'string' });
|
|
928
|
-
const lhs = this.#platform.getJsonArrayElementPropertySQL(jeAlias, jsonPropName, embProp.runtimeType ?? 'string');
|
|
929
987
|
const value = cond[k];
|
|
988
|
+
const { name, type } = resolveProperty(k, value);
|
|
989
|
+
referencedProps.set(k, { name, type });
|
|
990
|
+
const lhs = this.#platform.getJsonArrayElementPropertySQL(jeAlias, name, type);
|
|
930
991
|
if (Utils.isPlainObject(value)) {
|
|
931
|
-
// Validate that all keys are operators — nested embeddables within array elements are not supported.
|
|
932
992
|
const valueKeys = Object.keys(value);
|
|
933
993
|
if (valueKeys.some(vk => !Utils.isOperator(vk))) {
|
|
934
|
-
throw
|
|
994
|
+
throw invalidObjectError(k);
|
|
935
995
|
}
|
|
936
996
|
const sub = this.buildEmbeddedArrayOperatorCondition(lhs, value, params);
|
|
937
997
|
parts.push(sub);
|
|
@@ -946,41 +1006,38 @@ export class QueryBuilderHelper {
|
|
|
946
1006
|
}
|
|
947
1007
|
return { sql: parts.join(' and '), params };
|
|
948
1008
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
value
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
if (
|
|
962
|
-
|
|
1009
|
+
inferJsonValueType(value) {
|
|
1010
|
+
if (typeof value === 'number') {
|
|
1011
|
+
return 'number';
|
|
1012
|
+
}
|
|
1013
|
+
if (typeof value === 'boolean') {
|
|
1014
|
+
return 'boolean';
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof value === 'bigint') {
|
|
1017
|
+
return 'bigint';
|
|
1018
|
+
}
|
|
1019
|
+
if (Utils.isPlainObject(value)) {
|
|
1020
|
+
for (const v of Object.values(value)) {
|
|
1021
|
+
if (typeof v === 'number') {
|
|
1022
|
+
return 'number';
|
|
963
1023
|
}
|
|
964
|
-
|
|
965
|
-
|
|
1024
|
+
if (typeof v === 'boolean') {
|
|
1025
|
+
return 'boolean';
|
|
966
1026
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1027
|
+
if (typeof v === 'bigint') {
|
|
1028
|
+
return 'bigint';
|
|
1029
|
+
}
|
|
1030
|
+
if (Array.isArray(v) && v.length > 0) {
|
|
1031
|
+
if (typeof v[0] === 'number') {
|
|
1032
|
+
return 'number';
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof v[0] === 'boolean') {
|
|
1035
|
+
return 'boolean';
|
|
1036
|
+
}
|
|
970
1037
|
}
|
|
971
|
-
}
|
|
972
|
-
else if (op === '$exists') {
|
|
973
|
-
parts.push(`${lhs} ${replacement} null`);
|
|
974
|
-
}
|
|
975
|
-
else if (val === null) {
|
|
976
|
-
parts.push(`${lhs} ${replacement} null`);
|
|
977
|
-
}
|
|
978
|
-
else {
|
|
979
|
-
parts.push(`${lhs} ${replacement} ?`);
|
|
980
|
-
params.push(val);
|
|
981
1038
|
}
|
|
982
1039
|
}
|
|
983
|
-
return
|
|
1040
|
+
return 'string';
|
|
984
1041
|
}
|
|
985
1042
|
processOnConflictCondition(cond, schema) {
|
|
986
1043
|
const meta = this.#metadata.get(this.#entityName);
|