@mikro-orm/sql 7.1.16-dev.1 → 7.1.16-dev.11
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 +21 -3
- package/AbstractSqlConnection.js +54 -4
- package/AbstractSqlDriver.d.ts +1 -1
- package/AbstractSqlDriver.js +17 -4
- package/AbstractSqlPlatform.d.ts +3 -1
- package/AbstractSqlPlatform.js +32 -1
- package/README.md +1 -0
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
- package/dialects/mysql/BaseMySqlPlatform.js +4 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +2 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +33 -1
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +18 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +152 -0
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/package.json +2 -2
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +28 -4
- package/query/QueryBuilder.js +21 -7
- package/query/QueryBuilderHelper.js +4 -0
- 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 +90 -0
- package/schema/SchemaHelper.d.ts +6 -0
- package/schema/SchemaHelper.js +15 -0
- package/schema/SqlSchemaGenerator.js +8 -0
- package/typings.d.ts +16 -0
|
@@ -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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.1.16-dev.
|
|
3
|
+
"version": "7.1.16-dev.11",
|
|
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": "^7.1.15"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.16-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.16-dev.11"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
|
@@ -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
|
}
|
|
@@ -154,7 +170,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
154
170
|
alias = nestedAlias;
|
|
155
171
|
}
|
|
156
172
|
if (this.shouldAutoJoin(qb, nestedAlias)) {
|
|
157
|
-
return !keys.some(k =>
|
|
173
|
+
return !keys.some(k => this.isCollectionOperator(k));
|
|
158
174
|
}
|
|
159
175
|
return keys.some(field => {
|
|
160
176
|
const childNode = this.payload[field];
|
|
@@ -238,6 +254,14 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
238
254
|
delete o[key];
|
|
239
255
|
o.$and = $and;
|
|
240
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
|
+
}
|
|
241
265
|
shouldAutoJoin(qb, nestedAlias) {
|
|
242
266
|
if (!this.prop || !this.parent) {
|
|
243
267
|
return false;
|
|
@@ -246,7 +270,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
246
270
|
if (keys.every(k => typeof k === 'string' && k.includes('.') && k.startsWith(`${qb.alias}.`))) {
|
|
247
271
|
return false;
|
|
248
272
|
}
|
|
249
|
-
if (keys.some(k =>
|
|
273
|
+
if (keys.some(k => this.isCollectionOperator(k))) {
|
|
250
274
|
return true;
|
|
251
275
|
}
|
|
252
276
|
const meta = this.metadata.find(this.entityName);
|
package/query/QueryBuilder.js
CHANGED
|
@@ -1065,17 +1065,26 @@ export class QueryBuilder {
|
|
|
1065
1065
|
this.limit(1);
|
|
1066
1066
|
}
|
|
1067
1067
|
const query = this.toQuery();
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1068
|
+
const cacheKey = ['qb.execute', query.sql, query.params, method];
|
|
1069
|
+
// session context (row level security) scopes cached rows per tenant/role, avoiding cross-context serves
|
|
1070
|
+
const qbSessionContext = this.em?.getSessionContext();
|
|
1071
|
+
if (qbSessionContext) {
|
|
1072
|
+
cacheKey.push(qbSessionContext);
|
|
1073
|
+
}
|
|
1074
|
+
const cached = await this.em?.tryCache(this.mainAlias.entityName, this.#state.cache, cacheKey);
|
|
1074
1075
|
if (cached?.data !== undefined) {
|
|
1075
1076
|
return cached.data;
|
|
1076
1077
|
}
|
|
1077
1078
|
const loggerContext = { id: this.em?.id, ...this.loggerContext, ...this.#abortOptions };
|
|
1078
|
-
const
|
|
1079
|
+
const conn = this.getConnection();
|
|
1080
|
+
// outside a transaction, wrap in a short implicit one so RLS `set local` session context applies (no-op when unset)
|
|
1081
|
+
const sessionContext = this.context ? undefined : this.em?.getTransactionSessionContext();
|
|
1082
|
+
const res = await (sessionContext
|
|
1083
|
+
? conn.transactional(trx => conn.execute(query.sql, query.params, method, trx, loggerContext), {
|
|
1084
|
+
sessionContext,
|
|
1085
|
+
loggerContext,
|
|
1086
|
+
})
|
|
1087
|
+
: conn.execute(query.sql, query.params, method, this.context, loggerContext));
|
|
1079
1088
|
const meta = this.mainAlias.meta;
|
|
1080
1089
|
if (!options.mapResults || !meta) {
|
|
1081
1090
|
await this.em?.storeCache(this.#state.cache, cached, res);
|
|
@@ -1125,6 +1134,11 @@ export class QueryBuilder {
|
|
|
1125
1134
|
* ```
|
|
1126
1135
|
*/
|
|
1127
1136
|
async *stream(options) {
|
|
1137
|
+
// mirror EntityManager.stream — a stream can't open the implicit session-context transaction, so under the
|
|
1138
|
+
// 'transaction' strategy fail closed instead of silently running the cursor without the staged context
|
|
1139
|
+
if (!this.context && this.em?.getTransactionSessionContext()) {
|
|
1140
|
+
throw ValidationError.sessionContextStreamRequiresTransaction();
|
|
1141
|
+
}
|
|
1128
1142
|
options ??= {};
|
|
1129
1143
|
options.mergeResults ??= true;
|
|
1130
1144
|
options.mapResults ??= true;
|
|
@@ -643,6 +643,10 @@ export class QueryBuilderHelper {
|
|
|
643
643
|
return '?';
|
|
644
644
|
}
|
|
645
645
|
getOperatorReplacement(op, value) {
|
|
646
|
+
// collection properties expand `$all` into `$some` sub-queries in `ObjectCriteriaNode`, nothing else has a SQL equivalent
|
|
647
|
+
if (op === '$all') {
|
|
648
|
+
throw new Error('The `$all` operator is supported only on collection properties in SQL drivers, use `$contains` for array columns instead.');
|
|
649
|
+
}
|
|
646
650
|
let replacement = QueryOperator[op];
|
|
647
651
|
if (op === '$exists') {
|
|
648
652
|
replacement = value[op] ? 'is not' : 'is';
|
|
@@ -48,6 +48,10 @@ export declare class DatabaseSchema {
|
|
|
48
48
|
/** Separate from `create()` so the comparator only pays for routine introspection when the user actually defined routines. SQLite/libSQL helpers return []. */
|
|
49
49
|
loadRoutines(connection: AbstractSqlConnection, platform: AbstractSqlPlatform, schemas?: string[]): Promise<void>;
|
|
50
50
|
static fromMetadata(metadata: EntityMetadata[], platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, em?: any): DatabaseSchema;
|
|
51
|
+
/** Compiles an `rls`-flagged filter's condition into a resolved policy backed by `current_setting()` lookups. */
|
|
52
|
+
private static compileRlsFilterPolicy;
|
|
53
|
+
/** Truncates the base first so the collision suffix survives the identifier limit. */
|
|
54
|
+
private static uniquePolicyName;
|
|
51
55
|
/** Separate from {@link fromMetadata} so the comparator only walks routines when the user defined any. */
|
|
52
56
|
addRoutinesFromMetadata(routines: readonly Routine[], platform: AbstractSqlPlatform, em?: any): void;
|
|
53
57
|
/**
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReferenceKind, isRaw, } from '@mikro-orm/core';
|
|
1
|
+
import { ReferenceKind, MetadataError, QueryHelper, Utils, isRaw, } from '@mikro-orm/core';
|
|
2
2
|
import { DatabaseTable } from './DatabaseTable.js';
|
|
3
3
|
import { normalizeViewDefinition } from './SchemaHelper.js';
|
|
4
4
|
import { getTablePartitioning } from './partitioning.js';
|
|
@@ -262,9 +262,115 @@ export class DatabaseSchema {
|
|
|
262
262
|
expression: trigger.expression,
|
|
263
263
|
});
|
|
264
264
|
}
|
|
265
|
+
// non-empty policies imply RLS; `rowLevelSecurity: 'force'` enforces it for the table owner too, but an
|
|
266
|
+
// explicit `rowLevelSecurity: false` keeps RLS disabled even when policies are staged (they stay dormant)
|
|
267
|
+
table.rlsEnabled = meta.rowLevelSecurity !== false && (meta.policies.length > 0 || !!meta.rowLevelSecurity);
|
|
268
|
+
table.rlsForced = meta.rowLevelSecurity === 'force';
|
|
269
|
+
const usedPolicyNames = new Set();
|
|
270
|
+
const resolve = (raw) => {
|
|
271
|
+
if (raw == null) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
return isRaw(raw) ? platform.formatQuery(raw.sql, raw.params) : raw;
|
|
275
|
+
};
|
|
276
|
+
for (const policy of meta.policies) {
|
|
277
|
+
const command = policy.command ?? 'all';
|
|
278
|
+
// deterministic default name derived from table + command + collision index, truncated the
|
|
279
|
+
// same way check names are, so the introspected (engine-truncated) name matches metadata
|
|
280
|
+
const max = platform.getMaxIdentifierLength();
|
|
281
|
+
let name = policy.name;
|
|
282
|
+
if (!name) {
|
|
283
|
+
name = this.uniquePolicyName(`${meta.collection}_${command}_policy`, platform, usedPolicyNames);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
name = name.substring(0, max);
|
|
287
|
+
// explicit names skip the collision suffixing, so a duplicate would otherwise die with a raw pg error
|
|
288
|
+
// on create or be silently swallowed by the name-keyed diff dictionaries — reject it up front
|
|
289
|
+
if (usedPolicyNames.has(name)) {
|
|
290
|
+
throw MetadataError.duplicatePolicyName(meta, name);
|
|
291
|
+
}
|
|
292
|
+
usedPolicyNames.add(name);
|
|
293
|
+
}
|
|
294
|
+
table.addPolicy({
|
|
295
|
+
name,
|
|
296
|
+
command,
|
|
297
|
+
type: policy.type ?? 'permissive',
|
|
298
|
+
roles: policy.roles ?? [],
|
|
299
|
+
using: resolve(policy.using),
|
|
300
|
+
check: resolve(policy.check),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// rls-flagged filters materialize as additional permissive policies (app-level WHERE + DB-level policy);
|
|
304
|
+
// filters inherited from a TPT parent are skipped — the policy already lives on the parent table, and the
|
|
305
|
+
// child table may not even have the referenced columns
|
|
306
|
+
const rlsFilters = Object.values(meta.filters).filter(filter => filter.rls && !(meta.tptParent && Object.values(meta.tptParent.filters).includes(filter)));
|
|
307
|
+
if (rlsFilters.length > 0) {
|
|
308
|
+
// an explicit `rowLevelSecurity: false` still stages the filter's policy but keeps RLS off (dormant)
|
|
309
|
+
table.rlsEnabled = meta.rowLevelSecurity !== false;
|
|
310
|
+
for (const filter of rlsFilters) {
|
|
311
|
+
table.addPolicy(this.compileRlsFilterPolicy(meta, filter, table, platform, usedPolicyNames));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
265
314
|
}
|
|
266
315
|
return schema;
|
|
267
316
|
}
|
|
317
|
+
/** Compiles an `rls`-flagged filter's condition into a resolved policy backed by `current_setting()` lookups. */
|
|
318
|
+
static compileRlsFilterPolicy(meta, filter, table, platform, usedPolicyNames) {
|
|
319
|
+
const accessed = new Set();
|
|
320
|
+
const cond = QueryHelper.resolveRlsFilterCond(filter, accessed, meta.className);
|
|
321
|
+
const setting = typeof filter.rls === 'object' ? filter.rls.setting : undefined;
|
|
322
|
+
if (setting && accessed.size > 1) {
|
|
323
|
+
throw MetadataError.rlsFilterMultiArgSetting(filter.name, [...accessed]);
|
|
324
|
+
}
|
|
325
|
+
// the config-bound driver is always an `AbstractSqlDriver` here, like in `DatabaseTable.processIndexWhere`
|
|
326
|
+
const driver = platform.getConfig().getDriver();
|
|
327
|
+
let sql = driver.renderPartialIndexWhere(meta.class, cond);
|
|
328
|
+
const prefix = QueryHelper.RLS_SENTINEL_PREFIX;
|
|
329
|
+
const suffix = QueryHelper.RLS_SENTINEL_SUFFIX;
|
|
330
|
+
// match `"<column>" <op> '<sentinel>'` — the LHS is always a quoted column emitted from this entity's own
|
|
331
|
+
// where; group 1 keeps the column + operator so only the sentinel literal is swapped for the session lookup
|
|
332
|
+
const re = new RegExp(`("([^"]+)"\\s*(?:!=|>=|<=|=|>|<)\\s*)'${prefix}(\\w+)${suffix}'`, 'g');
|
|
333
|
+
sql = sql.replace(re, (_whole, lhs, column, arg) => {
|
|
334
|
+
const col = table.getColumn(column);
|
|
335
|
+
// the condition can reference a property that renders a field name without a managed column
|
|
336
|
+
// (`persist: false`, `skipColumns`) — fail with a descriptive error instead of a crash
|
|
337
|
+
if (!col) {
|
|
338
|
+
throw MetadataError.rlsFilterUnmanagedColumn(filter.name, column);
|
|
339
|
+
}
|
|
340
|
+
// native enum columns compare against the enum type itself, `current_setting()` text won't coerce implicitly
|
|
341
|
+
const cast = col.nativeEnumName
|
|
342
|
+
? `::${platform.quoteIdentifier(col.nativeEnumName)}`
|
|
343
|
+
: platform.getCurrentSettingCast(col.mappedType);
|
|
344
|
+
if (cast === null) {
|
|
345
|
+
throw MetadataError.rlsFilterUncastableType(filter.name, col.type);
|
|
346
|
+
}
|
|
347
|
+
// a sentinel implies the arg was accessed, and multi-arg custom settings were already rejected above
|
|
348
|
+
const name = setting || Utils.getRlsSettingName(filter.name, arg);
|
|
349
|
+
return `${lhs}current_setting(${platform.quoteValue(name)})${cast}`;
|
|
350
|
+
});
|
|
351
|
+
// a leftover sentinel means an argument appeared somewhere other than a direct comparison, which we can't compile
|
|
352
|
+
if (sql.includes(prefix)) {
|
|
353
|
+
throw MetadataError.rlsFilterUnsupportedCond(filter.name);
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
name: this.uniquePolicyName(`${meta.collection}_${filter.name}_policy`, platform, usedPolicyNames),
|
|
357
|
+
command: 'all',
|
|
358
|
+
type: 'permissive',
|
|
359
|
+
roles: [],
|
|
360
|
+
using: sql,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/** Truncates the base first so the collision suffix survives the identifier limit. */
|
|
364
|
+
static uniquePolicyName(base, platform, used) {
|
|
365
|
+
const max = platform.getMaxIdentifierLength();
|
|
366
|
+
let name = base.substring(0, max);
|
|
367
|
+
for (let i = 2; used.has(name); i++) {
|
|
368
|
+
const suffix = `_${i}`;
|
|
369
|
+
name = base.substring(0, max - suffix.length) + suffix;
|
|
370
|
+
}
|
|
371
|
+
used.add(name);
|
|
372
|
+
return name;
|
|
373
|
+
}
|
|
268
374
|
/** Separate from {@link fromMetadata} so the comparator only walks routines when the user defined any. */
|
|
269
375
|
addRoutinesFromMetadata(routines, platform, em) {
|
|
270
376
|
const resolveBody = (raw) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type IndexCallback, type NamingStrategy } from '@mikro-orm/core';
|
|
2
2
|
import type { SchemaHelper } from './SchemaHelper.js';
|
|
3
|
-
import type { CheckDef, Column, ForeignKey, IndexDef, TablePartitioning, SqlTriggerDef } from '../typings.js';
|
|
3
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, TablePartitioning, SqlPolicyDef, SqlTriggerDef } from '../typings.js';
|
|
4
4
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
@@ -15,6 +15,10 @@ export declare class DatabaseTable {
|
|
|
15
15
|
items: string[];
|
|
16
16
|
}>;
|
|
17
17
|
comment?: string;
|
|
18
|
+
/** Whether row level security is enabled on the table (postgres only). */
|
|
19
|
+
rlsEnabled: boolean;
|
|
20
|
+
/** Whether row level security is also enforced for the table owner (postgres `force`). */
|
|
21
|
+
rlsForced: boolean;
|
|
18
22
|
partitioning?: TablePartitioning;
|
|
19
23
|
/**
|
|
20
24
|
* Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
|
|
@@ -34,6 +38,11 @@ export declare class DatabaseTable {
|
|
|
34
38
|
/** @internal */
|
|
35
39
|
setPartitioning(partitioning?: TablePartitioning): void;
|
|
36
40
|
getTriggers(): SqlTriggerDef[];
|
|
41
|
+
getPolicies(): SqlPolicyDef[];
|
|
42
|
+
/** `[]` and `['public']` both mean PUBLIC — a single predicate so metadata, introspection and codegen agree. */
|
|
43
|
+
static isDefaultPolicyRoles(roles: string[]): boolean;
|
|
44
|
+
/** @internal */
|
|
45
|
+
setPolicies(policies: SqlPolicyDef[]): void;
|
|
37
46
|
/** @internal */
|
|
38
47
|
setIndexes(indexes: IndexDef[]): void;
|
|
39
48
|
/** @internal */
|
|
@@ -65,6 +74,8 @@ export declare class DatabaseTable {
|
|
|
65
74
|
hasCheck(checkName: string): boolean;
|
|
66
75
|
getTrigger(triggerName: string): SqlTriggerDef | undefined;
|
|
67
76
|
hasTrigger(triggerName: string): boolean;
|
|
77
|
+
getPolicy(policyName: string): SqlPolicyDef | undefined;
|
|
78
|
+
hasPolicy(policyName: string): boolean;
|
|
68
79
|
getPrimaryKey(): IndexDef | undefined;
|
|
69
80
|
hasPrimaryKey(): boolean;
|
|
70
81
|
private getForeignKeyDeclaration;
|
|
@@ -99,5 +110,6 @@ export declare class DatabaseTable {
|
|
|
99
110
|
private processIndexWhere;
|
|
100
111
|
addCheck(check: CheckDef): void;
|
|
101
112
|
addTrigger(trigger: SqlTriggerDef): void;
|
|
113
|
+
addPolicy(policy: SqlPolicyDef): void;
|
|
102
114
|
toJSON(): Dictionary;
|
|
103
115
|
}
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -10,10 +10,15 @@ export class DatabaseTable {
|
|
|
10
10
|
#indexes = [];
|
|
11
11
|
#checks = [];
|
|
12
12
|
#triggers = [];
|
|
13
|
+
#policies = [];
|
|
13
14
|
#foreignKeys = {};
|
|
14
15
|
#platform;
|
|
15
16
|
nativeEnums = {}; // for postgres
|
|
16
17
|
comment;
|
|
18
|
+
/** Whether row level security is enabled on the table (postgres only). */
|
|
19
|
+
rlsEnabled = false;
|
|
20
|
+
/** Whether row level security is also enforced for the table owner (postgres `force`). */
|
|
21
|
+
rlsForced = false;
|
|
17
22
|
partitioning;
|
|
18
23
|
/**
|
|
19
24
|
* Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
|
|
@@ -55,6 +60,17 @@ export class DatabaseTable {
|
|
|
55
60
|
getTriggers() {
|
|
56
61
|
return this.#triggers;
|
|
57
62
|
}
|
|
63
|
+
getPolicies() {
|
|
64
|
+
return this.#policies;
|
|
65
|
+
}
|
|
66
|
+
/** `[]` and `['public']` both mean PUBLIC — a single predicate so metadata, introspection and codegen agree. */
|
|
67
|
+
static isDefaultPolicyRoles(roles) {
|
|
68
|
+
return roles.length === 0 || (roles.length === 1 && roles[0] === 'public');
|
|
69
|
+
}
|
|
70
|
+
/** @internal */
|
|
71
|
+
setPolicies(policies) {
|
|
72
|
+
this.#policies = policies;
|
|
73
|
+
}
|
|
58
74
|
/** @internal */
|
|
59
75
|
setIndexes(indexes) {
|
|
60
76
|
this.#indexes = indexes;
|
|
@@ -670,6 +686,12 @@ export class DatabaseTable {
|
|
|
670
686
|
hasTrigger(triggerName) {
|
|
671
687
|
return !!this.getTrigger(triggerName);
|
|
672
688
|
}
|
|
689
|
+
getPolicy(policyName) {
|
|
690
|
+
return this.#policies.find(p => p.name === policyName);
|
|
691
|
+
}
|
|
692
|
+
hasPolicy(policyName) {
|
|
693
|
+
return !!this.getPolicy(policyName);
|
|
694
|
+
}
|
|
673
695
|
getPrimaryKey() {
|
|
674
696
|
return this.#indexes.find(i => i.primary);
|
|
675
697
|
}
|
|
@@ -990,6 +1012,9 @@ export class DatabaseTable {
|
|
|
990
1012
|
addTrigger(trigger) {
|
|
991
1013
|
this.#triggers.push(trigger);
|
|
992
1014
|
}
|
|
1015
|
+
addPolicy(policy) {
|
|
1016
|
+
this.#policies.push(policy);
|
|
1017
|
+
}
|
|
993
1018
|
toJSON() {
|
|
994
1019
|
const columns = this.#columns;
|
|
995
1020
|
// locale-independent comparison so the snapshot is stable across machines
|
|
@@ -1123,13 +1148,26 @@ export class DatabaseTable {
|
|
|
1123
1148
|
}
|
|
1124
1149
|
return out;
|
|
1125
1150
|
};
|
|
1151
|
+
const normalizePolicy = (policy) => {
|
|
1152
|
+
const out = { name: policy.name, command: policy.command, type: policy.type };
|
|
1153
|
+
if (!DatabaseTable.isDefaultPolicyRoles(policy.roles)) {
|
|
1154
|
+
out.roles = [...policy.roles].sort(byString);
|
|
1155
|
+
}
|
|
1156
|
+
for (const field of ['using', 'check']) {
|
|
1157
|
+
if (policy[field]) {
|
|
1158
|
+
out[field] = policy[field];
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return out;
|
|
1162
|
+
};
|
|
1126
1163
|
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1127
1164
|
const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
|
|
1128
1165
|
const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
|
|
1166
|
+
const sortedPolicies = [...this.#policies].sort((a, b) => byString(a.name, b.name)).map(normalizePolicy);
|
|
1129
1167
|
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1130
1168
|
.sort(([a], [b]) => byString(a, b))
|
|
1131
1169
|
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
1132
|
-
|
|
1170
|
+
const ret = {
|
|
1133
1171
|
name: this.name,
|
|
1134
1172
|
schema: this.schema,
|
|
1135
1173
|
columns: columnsMapped,
|
|
@@ -1142,5 +1180,16 @@ export class DatabaseTable {
|
|
|
1142
1180
|
// platforms that can't read comments back (sqlite), where keeping it would flip the snapshot
|
|
1143
1181
|
comment: supportsComments ? this.comment || null : null,
|
|
1144
1182
|
};
|
|
1183
|
+
// emit RLS state only when set, so snapshots of non-RLS tables stay byte-for-byte unchanged
|
|
1184
|
+
if (sortedPolicies.length > 0) {
|
|
1185
|
+
ret.policies = sortedPolicies;
|
|
1186
|
+
}
|
|
1187
|
+
if (this.rlsEnabled) {
|
|
1188
|
+
ret.rlsEnabled = true;
|
|
1189
|
+
}
|
|
1190
|
+
if (this.rlsForced) {
|
|
1191
|
+
ret.rlsForced = true;
|
|
1192
|
+
}
|
|
1193
|
+
return ret;
|
|
1145
1194
|
}
|
|
1146
1195
|
}
|
|
@@ -88,6 +88,8 @@ export declare class SchemaComparator {
|
|
|
88
88
|
*/
|
|
89
89
|
private diffViewExpression;
|
|
90
90
|
private diffTrigger;
|
|
91
|
+
private diffPolicies;
|
|
92
|
+
private diffPolicy;
|
|
91
93
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
92
94
|
private parseDecimalDefault;
|
|
93
95
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
@@ -328,6 +328,7 @@ export class SchemaComparator {
|
|
|
328
328
|
addedIndexes: {},
|
|
329
329
|
addedChecks: {},
|
|
330
330
|
addedTriggers: {},
|
|
331
|
+
addedPolicies: {},
|
|
331
332
|
changedColumns: {},
|
|
332
333
|
changedForeignKeys: {},
|
|
333
334
|
changedIndexes: {},
|
|
@@ -338,6 +339,7 @@ export class SchemaComparator {
|
|
|
338
339
|
removedIndexes: {},
|
|
339
340
|
removedChecks: {},
|
|
340
341
|
removedTriggers: {},
|
|
342
|
+
removedPolicies: {},
|
|
341
343
|
renamedColumns: {},
|
|
342
344
|
renamedIndexes: {},
|
|
343
345
|
fromTable,
|
|
@@ -517,6 +519,9 @@ export class SchemaComparator {
|
|
|
517
519
|
}
|
|
518
520
|
}
|
|
519
521
|
}
|
|
522
|
+
if (this.#platform.supportsRowLevelSecurity()) {
|
|
523
|
+
changes += this.diffPolicies(fromTable, toTable, tableDifferences);
|
|
524
|
+
}
|
|
520
525
|
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
|
521
526
|
const toForeignKeys = { ...toTable.getForeignKeys() };
|
|
522
527
|
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
|
@@ -908,6 +913,10 @@ export class SchemaComparator {
|
|
|
908
913
|
// multi word type names in casts, the generic `::\w+` below only covers single word ones
|
|
909
914
|
// the precision is kept, so `timestamptz(3)` and `timestamp(3) with time zone` leave the same residue
|
|
910
915
|
.replace(/::\s*(?:character\s+varying|bit\s+varying|double\s+precision|(?:timestamp|time)\b(\s*\(\d+\))?(?:\s+with(?:out)?\s+time\s+zone)?)/gi, '$1')
|
|
916
|
+
// Protect dots inside string literals before the quote strip below, or the alias-prefix normalization
|
|
917
|
+
// would mangle literal contents — `current_setting('app.tenant')` and `current_setting('req.tenant')`
|
|
918
|
+
// must not both collapse to `current_settingtenant`
|
|
919
|
+
.replace(/'([^']*)'/g, (_, inner) => `'${inner.replaceAll('.', '\u0000')}'`)
|
|
911
920
|
// Remove quotes first so we can process identifiers
|
|
912
921
|
.replace(/['"`]/g, '')
|
|
913
922
|
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
|
@@ -985,6 +994,87 @@ export class SchemaComparator {
|
|
|
985
994
|
}
|
|
986
995
|
return this.diffExpression(from.body, to.body);
|
|
987
996
|
}
|
|
997
|
+
diffPolicies(fromTable, toTable, diff) {
|
|
998
|
+
let changes = 0;
|
|
999
|
+
// `ignorePolicies` makes RLS create-only: declared policies are still added and RLS is still enabled/forced,
|
|
1000
|
+
// but existing policies are never diffed for drop/alter and RLS is never disabled or unforced — this protects
|
|
1001
|
+
// hand-written policies on databases that adopted RLS before the ORM managed it
|
|
1002
|
+
const ignorePolicies = this.#platform.getConfig().get('schemaGenerator').ignorePolicies;
|
|
1003
|
+
// postgres rejects `alter column ... type` on a column referenced by any policy, so a type change forces us to
|
|
1004
|
+
// drop every still-present policy around the alter (dropped before via `getRlsDropSQL`, recreated after via
|
|
1005
|
+
// `getRlsAlterSQL`) even when the policy itself is otherwise unchanged; a `generated`-only change is emitted
|
|
1006
|
+
// as a drop + re-add of the same column, which a policy's column dependency blocks the same way
|
|
1007
|
+
const hasColumnTypeChange = Object.values(diff.changedColumns).some(c => c.changedProperties.has('type')) ||
|
|
1008
|
+
Object.keys(diff.removedColumns).some(name => name in diff.addedColumns);
|
|
1009
|
+
for (const policy of toTable.getPolicies()) {
|
|
1010
|
+
if (!fromTable.hasPolicy(policy.name)) {
|
|
1011
|
+
diff.addedPolicies[policy.name] = policy;
|
|
1012
|
+
this.log(`policy ${policy.name} added to table ${diff.name}`, { policy });
|
|
1013
|
+
changes++;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (fromTable.rlsEnabled !== toTable.rlsEnabled && (!ignorePolicies || toTable.rlsEnabled)) {
|
|
1017
|
+
diff.changedRlsEnabled = toTable.rlsEnabled;
|
|
1018
|
+
changes++;
|
|
1019
|
+
}
|
|
1020
|
+
if (fromTable.rlsForced !== toTable.rlsForced && (!ignorePolicies || toTable.rlsForced)) {
|
|
1021
|
+
diff.changedRlsForced = toTable.rlsForced;
|
|
1022
|
+
changes++;
|
|
1023
|
+
}
|
|
1024
|
+
if (ignorePolicies) {
|
|
1025
|
+
// existing policies are unmanaged here, but a type change still needs them dropped and recreated for the
|
|
1026
|
+
// alter to succeed — recreate each verbatim from introspection so the hand-written definition is preserved
|
|
1027
|
+
if (hasColumnTypeChange) {
|
|
1028
|
+
for (const policy of fromTable.getPolicies()) {
|
|
1029
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1030
|
+
diff.addedPolicies[policy.name] = policy;
|
|
1031
|
+
changes += 2;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return changes;
|
|
1035
|
+
}
|
|
1036
|
+
for (const policy of fromTable.getPolicies()) {
|
|
1037
|
+
const toPolicy = toTable.getPolicy(policy.name);
|
|
1038
|
+
if (!toPolicy) {
|
|
1039
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1040
|
+
this.log(`policy ${policy.name} removed from table ${diff.name}`);
|
|
1041
|
+
changes++;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
// changed policies are always dropped (before column drops, which the old expression can block via its
|
|
1045
|
+
// column dependencies) and recreated (after column adds) — postgres could alter some of the changes in
|
|
1046
|
+
// place, but not a policy's command or type, nor unset an expression
|
|
1047
|
+
if (this.diffPolicy(policy, toPolicy)) {
|
|
1048
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1049
|
+
diff.addedPolicies[policy.name] = toPolicy;
|
|
1050
|
+
this.log(`policy ${policy.name} recreated in table ${diff.name}`, { from: policy, to: toPolicy });
|
|
1051
|
+
changes += 2;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
// an unchanged policy still blocks a type change on any column, so drop + recreate it around the alter
|
|
1055
|
+
if (hasColumnTypeChange) {
|
|
1056
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1057
|
+
diff.addedPolicies[policy.name] = toPolicy;
|
|
1058
|
+
this.log(`policy ${policy.name} recreated around a column type change in table ${diff.name}`);
|
|
1059
|
+
changes += 2;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return changes;
|
|
1063
|
+
}
|
|
1064
|
+
diffPolicy(from, to) {
|
|
1065
|
+
// normalize so an omitted `roles` matches introspected `{public}`
|
|
1066
|
+
const normalizeRoles = (roles) => DatabaseTable.isDefaultPolicyRoles(roles) ? '' : [...roles].sort().join(',');
|
|
1067
|
+
if (from.command !== to.command || from.type !== to.type) {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
if (normalizeRoles(from.roles) !== normalizeRoles(to.roles)) {
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
if (this.diffExpression(from.using ?? '', to.using ?? '')) {
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
return this.diffExpression(from.check ?? '', to.check ?? '');
|
|
1077
|
+
}
|
|
988
1078
|
parseJsonDefault(defaultValue) {
|
|
989
1079
|
/* v8 ignore next */
|
|
990
1080
|
if (!defaultValue) {
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -154,6 +154,12 @@ export declare abstract class SchemaHelper {
|
|
|
154
154
|
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
|
|
155
155
|
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
156
156
|
protected hasInlineColumnComment(): boolean;
|
|
157
|
+
/** Row level security DDL for a freshly created table (enable/force + create policies). Postgres only. */
|
|
158
|
+
getRlsCreateSQL(table: DatabaseTable): string[];
|
|
159
|
+
/** Drops removed/changed row level security policies; emitted in the pre-alter phase, before any column drop or type alter a policy expression can block. Postgres only. */
|
|
160
|
+
getRlsDropSQL(diff: TableDifference, safe?: boolean): string[];
|
|
161
|
+
/** Row level security DDL for a table difference (enable/disable/force transitions + policy creation). Postgres only. */
|
|
162
|
+
getRlsAlterSQL(diff: TableDifference, safe?: boolean): string[];
|
|
157
163
|
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
|
|
158
164
|
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
|
|
159
165
|
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -504,6 +504,7 @@ export class SchemaHelper {
|
|
|
504
504
|
if ('changedComment' in diff) {
|
|
505
505
|
ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
|
|
506
506
|
}
|
|
507
|
+
this.append(ret, this.getRlsAlterSQL(diff, safe));
|
|
507
508
|
return ret;
|
|
508
509
|
}
|
|
509
510
|
/** Returns SQL to add columns to an existing table. */
|
|
@@ -646,6 +647,18 @@ export class SchemaHelper {
|
|
|
646
647
|
hasInlineColumnComment() {
|
|
647
648
|
return false;
|
|
648
649
|
}
|
|
650
|
+
/** Row level security DDL for a freshly created table (enable/force + create policies). Postgres only. */
|
|
651
|
+
getRlsCreateSQL(table) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
/** Drops removed/changed row level security policies; emitted in the pre-alter phase, before any column drop or type alter a policy expression can block. Postgres only. */
|
|
655
|
+
getRlsDropSQL(diff, safe) {
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
/** Row level security DDL for a table difference (enable/disable/force transitions + policy creation). Postgres only. */
|
|
659
|
+
getRlsAlterSQL(diff, safe) {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
649
662
|
async getNamespaces(connection, ctx) {
|
|
650
663
|
return [];
|
|
651
664
|
}
|
|
@@ -801,6 +814,8 @@ export class SchemaHelper {
|
|
|
801
814
|
for (const trigger of table.getTriggers()) {
|
|
802
815
|
this.append(ret, this.createTrigger(table, trigger));
|
|
803
816
|
}
|
|
817
|
+
// RLS policies can reference other tables, so they are deferred until every table exists (see the
|
|
818
|
+
// callers of getRlsCreateSQL in SqlSchemaGenerator) rather than emitted inline here
|
|
804
819
|
}
|
|
805
820
|
return ret;
|
|
806
821
|
}
|