@mikro-orm/core 7.2.0-dev.13 → 7.2.0-dev.15
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/EntityManager.d.ts +30 -2
- package/EntityManager.js +214 -43
- package/MikroORM.js +3 -0
- package/connections/Connection.d.ts +3 -1
- package/drivers/IDatabaseDriver.d.ts +1 -0
- package/entity/Collection.js +4 -2
- package/entity/EntityFactory.js +6 -0
- package/entity/EntityLoader.js +12 -7
- package/entity/EntityRepository.js +5 -1
- package/entity/defineEntity.d.ts +34 -13
- package/entity/defineEntity.js +1 -1
- package/enums.d.ts +3 -1
- package/errors.d.ts +32 -0
- package/errors.js +75 -0
- package/events/EventManager.js +6 -3
- package/exceptions.d.ts +5 -0
- package/exceptions.js +5 -0
- package/hydration/ObjectHydrator.d.ts +2 -0
- package/hydration/ObjectHydrator.js +12 -8
- package/index.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +1 -0
- package/metadata/MetadataDiscovery.js +70 -11
- package/metadata/MetadataStorage.js +17 -1
- package/metadata/types.d.ts +5 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +12 -2
- package/platforms/Platform.js +43 -1
- package/types/Type.js +4 -4
- package/typings.d.ts +44 -2
- package/typings.js +24 -1
- package/unit-of-work/ChangeSetPersister.js +17 -13
- package/unit-of-work/UnitOfWork.js +11 -4
- package/utils/Configuration.d.ts +15 -1
- package/utils/Configuration.js +11 -1
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +7 -3
- package/utils/QueryHelper.d.ts +12 -0
- package/utils/QueryHelper.js +75 -4
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +14 -2
- package/utils/Utils.js +24 -2
- package/utils/env-vars.js +1 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/rls-utils.d.ts +35 -0
- package/utils/rls-utils.js +97 -0
- package/utils/upsert-utils.d.ts +9 -1
- package/utils/upsert-utils.js +26 -3
|
@@ -165,6 +165,9 @@ export class MetadataDiscovery {
|
|
|
165
165
|
filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
|
|
166
166
|
filtered.forEach(meta => this.initCheckConstraints(meta));
|
|
167
167
|
filtered.forEach(meta => this.initTriggers(meta));
|
|
168
|
+
// filter names are load-bearing for RLS (policy and session variable names), backfill from the dictionary key
|
|
169
|
+
filtered.forEach(meta => Object.entries(meta.filters).forEach(([key, filter]) => (filter.name ??= key)));
|
|
170
|
+
filtered.forEach(meta => this.initPolicies(meta));
|
|
168
171
|
forEachProp((_m, p) => {
|
|
169
172
|
this.initDefaultValue(p);
|
|
170
173
|
this.inferTypeFromDefault(p);
|
|
@@ -441,7 +444,18 @@ export class MetadataDiscovery {
|
|
|
441
444
|
if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
|
|
442
445
|
prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
|
|
443
446
|
}
|
|
444
|
-
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
|
|
447
|
+
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.polymorphic) {
|
|
448
|
+
if (prop.targetMeta) {
|
|
449
|
+
// same layout as `initManyToOneFields` builds later: `[discriminatorColumn, ...fkIdColumns]`
|
|
450
|
+
const pkFields = prop.targetMeta.getPrimaryProps().flatMap(pk => {
|
|
451
|
+
this.initFieldName(pk);
|
|
452
|
+
return pk.fieldNames;
|
|
453
|
+
});
|
|
454
|
+
const idColumns = pkFields.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, pkFields.length > 1));
|
|
455
|
+
prop.fieldNames = [prop.discriminatorColumn, ...idColumns];
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
445
459
|
prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
|
|
446
460
|
}
|
|
447
461
|
else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
|
|
@@ -451,10 +465,13 @@ export class MetadataDiscovery {
|
|
|
451
465
|
initManyToOneFieldName(prop, name) {
|
|
452
466
|
const meta2 = prop.targetMeta;
|
|
453
467
|
const ret = [];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
468
|
+
// with `targetKey` on a composite PK target, derive the FK field name from that property
|
|
469
|
+
// instead of the PKs (simple PK targets keep the PK based naming for backwards compatibility)
|
|
470
|
+
const referencedKeys = prop.targetKey && meta2.compositePK ? [prop.targetKey] : meta2.primaryKeys;
|
|
471
|
+
for (const referencedKey of referencedKeys) {
|
|
472
|
+
this.initFieldName(meta2.properties[referencedKey]);
|
|
473
|
+
for (const fieldName of meta2.properties[referencedKey].fieldNames) {
|
|
474
|
+
ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, !prop.targetKey && meta2.compositePK));
|
|
458
475
|
}
|
|
459
476
|
}
|
|
460
477
|
return ret;
|
|
@@ -1055,11 +1072,18 @@ export class MetadataDiscovery {
|
|
|
1055
1072
|
}
|
|
1056
1073
|
// TPT children have their own tables that don't contain the parent's columns,
|
|
1057
1074
|
// so propagating parent indexes/uniques/checks/triggers would target missing columns.
|
|
1075
|
+
// deep equality, as the subclass items might be copies of the base class ones (e.g. with TC39 decorators)
|
|
1058
1076
|
if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
|
|
1059
|
-
meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
|
|
1060
|
-
meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
|
|
1061
|
-
meta.checks = Utils.unique([...base.checks, ...meta.checks]);
|
|
1062
|
-
meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
|
|
1077
|
+
meta.indexes = Utils.unique([...base.indexes, ...meta.indexes], Utils.equals);
|
|
1078
|
+
meta.uniques = Utils.unique([...base.uniques, ...meta.uniques], Utils.equals);
|
|
1079
|
+
meta.checks = Utils.unique([...base.checks, ...meta.checks], Utils.equals);
|
|
1080
|
+
meta.triggers = Utils.unique([...base.triggers, ...meta.triggers], Utils.equals);
|
|
1081
|
+
}
|
|
1082
|
+
// Policies pass down only from inlined abstract bases; STI children share the root table
|
|
1083
|
+
// and TPT children own their tables, so both declare policies directly on the root/child.
|
|
1084
|
+
if (base.abstract && base.inheritanceType !== 'sti' && (meta.inheritanceType !== 'tpt' || !meta.tptParent)) {
|
|
1085
|
+
meta.policies = Utils.unique([...base.policies, ...meta.policies]);
|
|
1086
|
+
meta.rowLevelSecurity ??= base.rowLevelSecurity;
|
|
1063
1087
|
}
|
|
1064
1088
|
const pks = Object.values(meta.properties)
|
|
1065
1089
|
.filter(p => p.primary)
|
|
@@ -1247,8 +1271,17 @@ export class MetadataDiscovery {
|
|
|
1247
1271
|
if (embeddedProp.nullable || refInArray) {
|
|
1248
1272
|
meta.properties[name].nullable = true;
|
|
1249
1273
|
}
|
|
1274
|
+
// polymorphic relations derive their column names from the discriminator, so prefix it too
|
|
1275
|
+
if (meta.properties[name].polymorphic && !object) {
|
|
1276
|
+
meta.properties[name].discriminator = prefix + meta.properties[name].discriminator;
|
|
1277
|
+
meta.properties[name].discriminatorColumn = prefix + meta.properties[name].discriminatorColumn;
|
|
1278
|
+
}
|
|
1250
1279
|
if (meta.properties[name].fieldNames) {
|
|
1251
|
-
|
|
1280
|
+
const { fieldNames, polymorphic } = meta.properties[name];
|
|
1281
|
+
// polymorphic `fieldNames` hold `[discriminatorColumn, ...fkIdColumns]`, so prefix all of them
|
|
1282
|
+
for (let i = 0; i < (polymorphic ? fieldNames.length : 1); i++) {
|
|
1283
|
+
fieldNames[i] = prefix + fieldNames[i];
|
|
1284
|
+
}
|
|
1252
1285
|
}
|
|
1253
1286
|
else {
|
|
1254
1287
|
const name2 = meta.properties[name].name;
|
|
@@ -1677,6 +1710,9 @@ export class MetadataDiscovery {
|
|
|
1677
1710
|
if (prop.persist === false || prop.nativeEnumName || !prop.items?.every(item => typeof item === 'string')) {
|
|
1678
1711
|
continue;
|
|
1679
1712
|
}
|
|
1713
|
+
if (prop.customType instanceof t.json || ['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1680
1716
|
this.initFieldName(prop);
|
|
1681
1717
|
let expression = null;
|
|
1682
1718
|
if (prop.enum) {
|
|
@@ -1716,6 +1752,28 @@ export class MetadataDiscovery {
|
|
|
1716
1752
|
}
|
|
1717
1753
|
meta.hasTriggers = true;
|
|
1718
1754
|
}
|
|
1755
|
+
initPolicies(meta) {
|
|
1756
|
+
if (meta.policies.length === 0) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1760
|
+
const table = this.createSchemaTable(meta);
|
|
1761
|
+
// resolve callbacks into a copy — the defs can be shared with siblings via an inlined abstract base,
|
|
1762
|
+
// and resolving in place would bake the first child's table into every other child's policy
|
|
1763
|
+
meta.policies = meta.policies.map(policy => {
|
|
1764
|
+
if (!(policy.using instanceof Function) && !(policy.check instanceof Function)) {
|
|
1765
|
+
return policy;
|
|
1766
|
+
}
|
|
1767
|
+
const resolved = { ...policy };
|
|
1768
|
+
if (resolved.using instanceof Function) {
|
|
1769
|
+
resolved.using = resolved.using(columns, table);
|
|
1770
|
+
}
|
|
1771
|
+
if (resolved.check instanceof Function) {
|
|
1772
|
+
resolved.check = resolved.check(columns, table);
|
|
1773
|
+
}
|
|
1774
|
+
return resolved;
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1719
1777
|
initGeneratedColumn(meta, prop) {
|
|
1720
1778
|
if (!prop.generated && prop.columnTypes) {
|
|
1721
1779
|
const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
|
|
@@ -2080,6 +2138,7 @@ export class MetadataDiscovery {
|
|
|
2080
2138
|
prop.columnTypes.push(...columnTypes);
|
|
2081
2139
|
if (!targetMeta.compositePK || prop.targetKey) {
|
|
2082
2140
|
prop.customType = referencedProp.customType;
|
|
2141
|
+
prop.collation ??= referencedProp.collation;
|
|
2083
2142
|
}
|
|
2084
2143
|
}
|
|
2085
2144
|
}
|
|
@@ -2135,7 +2194,7 @@ export class MetadataDiscovery {
|
|
|
2135
2194
|
shouldForceConstructorUsage(meta) {
|
|
2136
2195
|
const forceConstructor = this.#config.get('forceEntityConstructor');
|
|
2137
2196
|
if (Array.isArray(forceConstructor)) {
|
|
2138
|
-
return forceConstructor.some(cls => Utils.
|
|
2197
|
+
return forceConstructor.some(cls => Utils.matchesEntity(cls, meta));
|
|
2139
2198
|
}
|
|
2140
2199
|
return forceConstructor;
|
|
2141
2200
|
}
|
|
@@ -17,6 +17,7 @@ export class MetadataStorage {
|
|
|
17
17
|
#idMap;
|
|
18
18
|
#classNameMap;
|
|
19
19
|
#uniqueNameMap;
|
|
20
|
+
#ambiguousNames = new Set();
|
|
20
21
|
constructor(metadata = {}) {
|
|
21
22
|
this.#idMap = {};
|
|
22
23
|
this.#uniqueNameMap = {};
|
|
@@ -64,6 +65,10 @@ export class MetadataStorage {
|
|
|
64
65
|
}
|
|
65
66
|
/** Returns metadata for the given entity, optionally initializing it if not found. */
|
|
66
67
|
get(entityName, init = false) {
|
|
68
|
+
// string lookups cannot be resolved when several classes were minified to the same name
|
|
69
|
+
if (typeof entityName === 'string' && this.#ambiguousNames.has(entityName)) {
|
|
70
|
+
throw MetadataError.ambiguousEntityName(entityName);
|
|
71
|
+
}
|
|
67
72
|
const exists = this.find(entityName);
|
|
68
73
|
if (exists) {
|
|
69
74
|
return exists;
|
|
@@ -99,7 +104,13 @@ export class MetadataStorage {
|
|
|
99
104
|
this.#metadataMap.set(entityName, meta);
|
|
100
105
|
this.#idMap[meta._id] = meta;
|
|
101
106
|
this.#uniqueNameMap[meta.uniqueName] = meta;
|
|
102
|
-
|
|
107
|
+
const className = Utils.className(entityName);
|
|
108
|
+
const existing = this.#classNameMap[className];
|
|
109
|
+
// track name collisions caused by minifiers mangling two classes to the same name
|
|
110
|
+
if (existing && existing !== meta && existing.class !== meta.class) {
|
|
111
|
+
this.#ambiguousNames.add(className);
|
|
112
|
+
}
|
|
113
|
+
this.#classNameMap[className] = meta;
|
|
103
114
|
return meta;
|
|
104
115
|
}
|
|
105
116
|
/** Removes metadata for the given entity from all internal maps. */
|
|
@@ -110,6 +121,11 @@ export class MetadataStorage {
|
|
|
110
121
|
delete this.#idMap[meta._id];
|
|
111
122
|
delete this.#uniqueNameMap[meta.uniqueName];
|
|
112
123
|
delete this.#classNameMap[meta.className];
|
|
124
|
+
// the name may still be ambiguous among the remaining metas
|
|
125
|
+
const remaining = new Set([...this.#metadataMap.values()].filter(m => m.className === meta.className).map(m => m.class));
|
|
126
|
+
if (remaining.size <= 1) {
|
|
127
|
+
this.#ambiguousNames.delete(meta.className);
|
|
128
|
+
}
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
/** Decorates all entity prototypes with helper methods (e.g. init, toJSON). */
|
package/metadata/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef, PolicyDef } from '../typings.js';
|
|
2
2
|
import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
|
|
3
3
|
import type { Type, types } from '../types/index.js';
|
|
4
4
|
import type { EntityManager } from '../EntityManager.js';
|
|
@@ -104,6 +104,10 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
104
104
|
hasTriggers?: boolean;
|
|
105
105
|
/** Database triggers to create for this entity's table. (SQL drivers only) */
|
|
106
106
|
triggers?: TriggerDef<E>[];
|
|
107
|
+
/** PostgreSQL row level security policies for this entity's table. Declaring policies implicitly enables RLS. */
|
|
108
|
+
policies?: PolicyDef<E>[];
|
|
109
|
+
/** Enables PostgreSQL row level security on this entity's table. `'force'` also enforces it for the table owner. Set to `false` to keep declared policies staged while leaving RLS disabled. */
|
|
110
|
+
rowLevelSecurity?: boolean | 'force';
|
|
107
111
|
/**
|
|
108
112
|
* PostgreSQL partitioning definition for this table.
|
|
109
113
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.15",
|
|
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",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -211,8 +211,9 @@ export declare abstract class Platform {
|
|
|
211
211
|
getBlobDeclarationSQL(): string;
|
|
212
212
|
getJsonDeclarationSQL(): string;
|
|
213
213
|
getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | Raw;
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
/** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
|
|
215
|
+
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | Raw;
|
|
216
|
+
processJsonCondition<T extends object>(o: FilterQuery<T>, value: EntityValue<T>, path: EntityKey<T>[], alias: boolean | string): FilterQuery<T>;
|
|
216
217
|
protected getJsonValueType(value: unknown): string;
|
|
217
218
|
getJsonIndexDefinition(index: {
|
|
218
219
|
columnNames: string[];
|
|
@@ -315,6 +316,15 @@ export declare abstract class Platform {
|
|
|
315
316
|
supportsDownMigrations(): boolean;
|
|
316
317
|
/** Whether the platform supports deferred unique constraints. */
|
|
317
318
|
supportsDeferredUniqueConstraints(): boolean;
|
|
319
|
+
/** Whether the platform supports row level security (PostgreSQL). */
|
|
320
|
+
supportsRowLevelSecurity(): boolean;
|
|
321
|
+
/** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
|
|
322
|
+
supportsConnectionSessionContext(): boolean;
|
|
323
|
+
/**
|
|
324
|
+
* SQL cast suffix (e.g. `'::uuid'`, or `''` when none is needed) applied when an RLS filter reads a session
|
|
325
|
+
* variable via `current_setting()` as the given column type, or `null` if the type has no automatic cast.
|
|
326
|
+
*/
|
|
327
|
+
getCurrentSettingCast(mappedType: Type<unknown>): string | null;
|
|
318
328
|
/** Platform-specific validation of entity metadata. */
|
|
319
329
|
validateMetadata(meta: EntityMetadata): void;
|
|
320
330
|
/**
|
package/platforms/Platform.js
CHANGED
|
@@ -413,6 +413,7 @@ export class Platform {
|
|
|
413
413
|
getSearchJsonPropertySQL(path, type, aliased) {
|
|
414
414
|
return path;
|
|
415
415
|
}
|
|
416
|
+
/** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
|
|
416
417
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
417
418
|
return path.join('.');
|
|
418
419
|
}
|
|
@@ -424,7 +425,8 @@ export class Platform {
|
|
|
424
425
|
return o;
|
|
425
426
|
}
|
|
426
427
|
if (path.length === 1) {
|
|
427
|
-
|
|
428
|
+
const key = typeof alias === 'string' ? `${alias}.${path[0]}` : path[0];
|
|
429
|
+
o[key] = value;
|
|
428
430
|
return o;
|
|
429
431
|
}
|
|
430
432
|
const type = this.getJsonValueType(value);
|
|
@@ -720,11 +722,51 @@ export class Platform {
|
|
|
720
722
|
supportsDeferredUniqueConstraints() {
|
|
721
723
|
return true;
|
|
722
724
|
}
|
|
725
|
+
/** Whether the platform supports row level security (PostgreSQL). */
|
|
726
|
+
supportsRowLevelSecurity() {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
/** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
|
|
730
|
+
supportsConnectionSessionContext() {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* SQL cast suffix (e.g. `'::uuid'`, or `''` when none is needed) applied when an RLS filter reads a session
|
|
735
|
+
* variable via `current_setting()` as the given column type, or `null` if the type has no automatic cast.
|
|
736
|
+
*/
|
|
737
|
+
getCurrentSettingCast(mappedType) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
723
740
|
/** Platform-specific validation of entity metadata. */
|
|
724
741
|
validateMetadata(meta) {
|
|
725
742
|
if (meta.partitionBy && !this.supportsPartitionedTables()) {
|
|
726
743
|
throw new MetadataError(`Entity ${meta.className} uses partitionBy, but ${this.constructor.name} does not support partitioned tables`);
|
|
727
744
|
}
|
|
745
|
+
const declaresRls = meta.policies.length > 0 || !!meta.rowLevelSecurity;
|
|
746
|
+
if (declaresRls && !this.supportsRowLevelSecurity()) {
|
|
747
|
+
throw MetadataError.rowLevelSecurityNotSupportedByDriver(meta);
|
|
748
|
+
}
|
|
749
|
+
// STI hierarchies share a single table, so only the root may declare policies; `root` is optional-chained
|
|
750
|
+
// as `validateMetadata` is public API and tolerates partially populated metadata
|
|
751
|
+
if (declaresRls && meta.root?.inheritanceType === 'sti' && meta.root !== meta) {
|
|
752
|
+
throw MetadataError.rowLevelSecurityOnNonRootStiEntity(meta);
|
|
753
|
+
}
|
|
754
|
+
if (meta.root?.inheritanceType === 'sti' && meta.root !== meta) {
|
|
755
|
+
for (const filter of Object.values(meta.filters)) {
|
|
756
|
+
// inherited root filters share the def object; only defs declared on the child itself are a problem,
|
|
757
|
+
// as non-root STI metas never reach the schema generator and the policy would silently not exist
|
|
758
|
+
if (filter.rls && meta.root.filters[filter.name] !== filter) {
|
|
759
|
+
throw MetadataError.rlsFilterOnNonRootStiEntity(meta, filter.name);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (!this.supportsRowLevelSecurity()) {
|
|
764
|
+
for (const filter of Object.values(meta.filters)) {
|
|
765
|
+
if (filter.rls) {
|
|
766
|
+
throw MetadataError.rlsFilterNotSupportedByDriver(meta, filter.name);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
728
770
|
}
|
|
729
771
|
/**
|
|
730
772
|
* Generates a custom order by statement given a set of in order values, eg.
|
package/types/Type.js
CHANGED
|
@@ -55,11 +55,11 @@ export class Type {
|
|
|
55
55
|
return prop.columnTypes?.[0] ?? platform.getTextTypeDeclarationSQL(prop);
|
|
56
56
|
}
|
|
57
57
|
static getType(cls) {
|
|
58
|
-
|
|
59
|
-
if (!Type.types.has(
|
|
60
|
-
Type.types.set(
|
|
58
|
+
// keyed by class reference, as minifiers can mangle two classes to the same name
|
|
59
|
+
if (!Type.types.has(cls)) {
|
|
60
|
+
Type.types.set(cls, new cls());
|
|
61
61
|
}
|
|
62
|
-
return Type.types.get(
|
|
62
|
+
return Type.types.get(cls);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Checks whether the argument is instance of `Type`.
|
package/typings.d.ts
CHANGED
|
@@ -478,12 +478,12 @@ type NonArrayObject = object & {
|
|
|
478
478
|
export type EntityDataProp<T, C extends boolean> = T extends Date ? string | Date : T extends Scalar ? T : T extends ScalarReference<infer U> ? EntityDataProp<U, C> : T extends {
|
|
479
479
|
__runtime?: infer Runtime;
|
|
480
480
|
__raw?: infer Raw;
|
|
481
|
-
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
|
|
481
|
+
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
|
|
482
482
|
/** Like `EntityDataProp` but used in `RequiredEntityData` context with required/optional key distinction. */
|
|
483
483
|
export type RequiredEntityDataProp<T, O, C extends boolean> = T extends Date ? string | Date : Exclude<T, null> extends RequiredNullable.Brand ? T | null : T extends Scalar ? T : T extends ScalarReference<infer U> ? RequiredEntityDataProp<U, O, C> : T extends {
|
|
484
484
|
__runtime?: infer Runtime;
|
|
485
485
|
__raw?: infer Raw;
|
|
486
|
-
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
|
|
486
|
+
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
|
|
487
487
|
/** Nested entity data shape for embedded or related entities within `EntityData`. */
|
|
488
488
|
export type EntityDataNested<T, C extends boolean = false> = T extends undefined ? never : T extends any[] ? Readonly<T> : EntityData<T, C> | ExpandEntityProp<T, C>;
|
|
489
489
|
type UnwrapScalarRef<T> = T extends ScalarReference<infer U> ? U : T;
|
|
@@ -710,6 +710,8 @@ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table:
|
|
|
710
710
|
export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
|
|
711
711
|
/** Callback for CHECK constraint expressions. Receives column mappings and table info. */
|
|
712
712
|
export type CheckCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
|
|
713
|
+
/** Callback for row level security policy expressions. Receives column mappings and table info. */
|
|
714
|
+
export type PolicyCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
|
|
713
715
|
/** Callback for trigger body expressions. Receives column mappings and table info. */
|
|
714
716
|
export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
715
717
|
/**
|
|
@@ -724,6 +726,28 @@ export interface CheckConstraint<T = any> {
|
|
|
724
726
|
property?: string;
|
|
725
727
|
expression: string | Raw | CheckCallback<T>;
|
|
726
728
|
}
|
|
729
|
+
/** Definition of a PostgreSQL row level security policy on a table. */
|
|
730
|
+
export interface PolicyDef<T = any> {
|
|
731
|
+
/** Policy name. Auto-generated if omitted. */
|
|
732
|
+
name?: string;
|
|
733
|
+
/** DML command the policy applies to. Defaults to `'all'`. */
|
|
734
|
+
command?: 'select' | 'insert' | 'update' | 'delete' | 'all';
|
|
735
|
+
/** Whether the policy is permissive (OR-combined) or restrictive (AND-combined). Defaults to `'permissive'`. */
|
|
736
|
+
type?: 'permissive' | 'restrictive';
|
|
737
|
+
/** Database roles the policy applies to. Defaults to `PUBLIC`. */
|
|
738
|
+
roles?: string[];
|
|
739
|
+
/** `USING` expression filtering visible rows. Can be a string, Raw query, or callback receiving column name mappings. */
|
|
740
|
+
using?: string | Raw | PolicyCallback<T>;
|
|
741
|
+
/** `WITH CHECK` expression validating written rows. Can be a string, Raw query, or callback receiving column name mappings. */
|
|
742
|
+
check?: string | Raw | PolicyCallback<T>;
|
|
743
|
+
}
|
|
744
|
+
/** Per-context database session state applied for row level security (session variables and role). */
|
|
745
|
+
export interface SessionContext {
|
|
746
|
+
/** Session variables set via `set_config`, typically referenced by RLS policies through `current_setting()`. `Date` values are serialized to ISO 8601. */
|
|
747
|
+
variables?: Dictionary<string | number | boolean | Date>;
|
|
748
|
+
/** Database role to switch to for the duration of the context (`set local role` / `set role`). */
|
|
749
|
+
role?: string;
|
|
750
|
+
}
|
|
727
751
|
/** Definition of a database trigger on a table. */
|
|
728
752
|
export interface TriggerDef<T = any> {
|
|
729
753
|
/** Trigger name. Auto-generated if omitted. */
|
|
@@ -1050,6 +1074,12 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
|
|
|
1050
1074
|
constructor(meta?: Partial<EntityMetadata>);
|
|
1051
1075
|
addProperty(prop: Partial<EntityProperty<Entity>>): void;
|
|
1052
1076
|
removeProperty(name: string, sync?: boolean): void;
|
|
1077
|
+
/** For TPT entities, the version column exists only on the table of the entity that declares it. */
|
|
1078
|
+
ownsVersionProperty(): boolean;
|
|
1079
|
+
/** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
|
|
1080
|
+
getOwnConcurrencyCheckKeys(): EntityKey<Entity>[];
|
|
1081
|
+
/** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
|
|
1082
|
+
hasOptimisticLock(): boolean;
|
|
1053
1083
|
getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
|
|
1054
1084
|
getPrimaryProp(): EntityProperty<Entity>;
|
|
1055
1085
|
/**
|
|
@@ -1165,6 +1195,9 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1165
1195
|
}[];
|
|
1166
1196
|
checks: CheckConstraint<Entity>[];
|
|
1167
1197
|
triggers: TriggerDef<Entity>[];
|
|
1198
|
+
policies: PolicyDef<Entity>[];
|
|
1199
|
+
/** Enables row level security on the table. `'force'` also enables it for the table owner. Implied by non-empty `policies`, unless set to `false`, which keeps the policies staged but RLS disabled. */
|
|
1200
|
+
rowLevelSecurity?: boolean | 'force';
|
|
1168
1201
|
repositoryClass?: string;
|
|
1169
1202
|
repository: () => EntityClass<EntityRepository<any>>;
|
|
1170
1203
|
hooks: {
|
|
@@ -1492,6 +1525,15 @@ type FilterDefResolved<T extends object = any> = {
|
|
|
1492
1525
|
entity?: EntityName<T> | EntityName<T>[];
|
|
1493
1526
|
args?: boolean;
|
|
1494
1527
|
strict?: boolean;
|
|
1528
|
+
/**
|
|
1529
|
+
* Also materializes this filter as a PostgreSQL row level security policy on the entity's table, and stages the
|
|
1530
|
+
* matching session variables when its params are enabled via `em.setFilterParams()`. The `cond` must be compilable
|
|
1531
|
+
* to a static expression (no access to `em`/`type`/`options`, not async). Each referenced argument maps to a session
|
|
1532
|
+
* variable named `mikro.<filterName>.<argName>`; pass `{ setting }` to override that name for a single-argument filter.
|
|
1533
|
+
*/
|
|
1534
|
+
rls?: boolean | {
|
|
1535
|
+
setting?: string;
|
|
1536
|
+
};
|
|
1495
1537
|
};
|
|
1496
1538
|
/** Definition of a query filter that can be registered globally or per-entity via `@Filter()`. */
|
|
1497
1539
|
export type FilterDef<T extends EntityName | readonly EntityName[] = any> = FilterDefResolved<EntityFromInput<T>> & {
|
package/typings.js
CHANGED
|
@@ -49,6 +49,7 @@ export class EntityMetadata {
|
|
|
49
49
|
this.uniques = [];
|
|
50
50
|
this.checks = [];
|
|
51
51
|
this.triggers = [];
|
|
52
|
+
this.policies = [];
|
|
52
53
|
this.referencingProperties = [];
|
|
53
54
|
this.concurrencyCheckKeys = new Set();
|
|
54
55
|
Object.assign(this, meta);
|
|
@@ -79,6 +80,25 @@ export class EntityMetadata {
|
|
|
79
80
|
this.sync();
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
/** For TPT entities, the version column exists only on the table of the entity that declares it. */
|
|
84
|
+
ownsVersionProperty() {
|
|
85
|
+
if (!this.versionProperty) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return this.inheritanceType !== 'tpt' || !this.ownProps || this.ownProps.some(p => p.name === this.versionProperty);
|
|
89
|
+
}
|
|
90
|
+
/** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
|
|
91
|
+
getOwnConcurrencyCheckKeys() {
|
|
92
|
+
const keys = [...this.concurrencyCheckKeys];
|
|
93
|
+
if (this.inheritanceType !== 'tpt' || !this.ownProps) {
|
|
94
|
+
return keys;
|
|
95
|
+
}
|
|
96
|
+
return keys.filter(key => this.ownProps.some(p => p.name === key));
|
|
97
|
+
}
|
|
98
|
+
/** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
|
|
99
|
+
hasOptimisticLock() {
|
|
100
|
+
return this.ownsVersionProperty() || this.getOwnConcurrencyCheckKeys().length > 0;
|
|
101
|
+
}
|
|
82
102
|
getPrimaryProps(flatten = false) {
|
|
83
103
|
const pks = this.primaryKeys.map(pk => this.properties[pk]);
|
|
84
104
|
if (flatten) {
|
|
@@ -216,7 +236,10 @@ export class EntityMetadata {
|
|
|
216
236
|
if (config) {
|
|
217
237
|
const platform = config.getPlatform();
|
|
218
238
|
for (const prop of this.props) {
|
|
219
|
-
if (prop.enum &&
|
|
239
|
+
if (prop.enum &&
|
|
240
|
+
!prop.nativeEnumName &&
|
|
241
|
+
prop.items?.every(item => typeof item === 'string') &&
|
|
242
|
+
!['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
|
|
220
243
|
const name = platform.getIndexName(this.tableName, prop.fieldNames, 'check');
|
|
221
244
|
const exists = this.checks.findIndex(check => check.name === name);
|
|
222
245
|
if (exists !== -1) {
|
|
@@ -204,13 +204,14 @@ export class ChangeSetPersister {
|
|
|
204
204
|
}
|
|
205
205
|
checkConcurrencyKeys(meta, changeSet, cond) {
|
|
206
206
|
const tmp = [];
|
|
207
|
-
|
|
207
|
+
const keys = meta.getOwnConcurrencyCheckKeys();
|
|
208
|
+
for (const key of keys) {
|
|
208
209
|
cond[key] = changeSet.originalEntity[key];
|
|
209
210
|
if (changeSet.payload[key]) {
|
|
210
211
|
tmp.push(key);
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
|
-
if (tmp.length === 0 &&
|
|
214
|
+
if (tmp.length === 0 && keys.length > 0) {
|
|
214
215
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
215
216
|
}
|
|
216
217
|
}
|
|
@@ -303,32 +304,33 @@ export class ChangeSetPersister {
|
|
|
303
304
|
options = this.prepareOptions(meta, options, {
|
|
304
305
|
convertCustomTypes: false,
|
|
305
306
|
});
|
|
306
|
-
if (meta.
|
|
307
|
-
(!meta.
|
|
307
|
+
if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
|
|
308
|
+
(!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
|
|
308
309
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
309
310
|
}
|
|
310
|
-
if (meta.
|
|
311
|
+
if (meta.ownsVersionProperty()) {
|
|
311
312
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
312
313
|
}
|
|
313
314
|
this.checkConcurrencyKeys(meta, changeSet, cond);
|
|
314
315
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
315
316
|
}
|
|
316
317
|
async checkOptimisticLocks(meta, changeSets, options) {
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
const concurrencyCheckKeys = meta.getOwnConcurrencyCheckKeys();
|
|
319
|
+
if (concurrencyCheckKeys.length === 0 &&
|
|
320
|
+
(!meta.ownsVersionProperty() || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
|
|
319
321
|
return;
|
|
320
322
|
}
|
|
321
323
|
// skip entity references as they don't have version values loaded
|
|
322
324
|
changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
|
|
325
|
+
const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
|
|
323
326
|
const $or = changeSets.map(cs => {
|
|
324
|
-
const cond = Utils.getPrimaryKeyCond(cs.originalEntity,
|
|
325
|
-
if (meta.
|
|
327
|
+
const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
|
|
328
|
+
if (meta.ownsVersionProperty()) {
|
|
326
329
|
// @ts-ignore
|
|
327
330
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
328
331
|
}
|
|
329
332
|
return cond;
|
|
330
333
|
});
|
|
331
|
-
const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
|
|
332
334
|
options = this.prepareOptions(meta, options, {
|
|
333
335
|
fields: primaryKeys,
|
|
334
336
|
orderBy: meta.primaryKeys.reduce((o, pk) => {
|
|
@@ -336,7 +338,9 @@ export class ChangeSetPersister {
|
|
|
336
338
|
return o;
|
|
337
339
|
}, {}),
|
|
338
340
|
});
|
|
339
|
-
|
|
341
|
+
// TPT tables query their own metadata, as the version column might not live on the root table
|
|
342
|
+
const target = meta.inheritanceType === 'tpt' ? meta : meta.root;
|
|
343
|
+
const res = await this.#driver.find(target.class, { $or }, options);
|
|
340
344
|
if (res.length !== changeSets.length) {
|
|
341
345
|
// a FK pointing to a composite PK is an array, so the values need to be compared deeply
|
|
342
346
|
const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
|
|
@@ -347,7 +351,7 @@ export class ChangeSetPersister {
|
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
checkOptimisticLock(meta, changeSet, res) {
|
|
350
|
-
if ((meta.
|
|
354
|
+
if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
|
|
351
355
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
352
356
|
}
|
|
353
357
|
}
|
|
@@ -356,7 +360,7 @@ export class ChangeSetPersister {
|
|
|
356
360
|
* so we use a single query in case of both versioning and default values is used.
|
|
357
361
|
*/
|
|
358
362
|
async reloadVersionValues(meta, changeSets, options) {
|
|
359
|
-
const reloadProps = meta.
|
|
363
|
+
const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
|
|
360
364
|
if (changeSets[0].type === ChangeSetType.CREATE) {
|
|
361
365
|
for (const prop of meta.props) {
|
|
362
366
|
if (prop.persist === false) {
|
|
@@ -124,7 +124,6 @@ export class UnitOfWork {
|
|
|
124
124
|
if (options?.newEntity) {
|
|
125
125
|
return entity;
|
|
126
126
|
}
|
|
127
|
-
const forceUndefined = this.#em.config.get('forceUndefined');
|
|
128
127
|
const wrapped = helper(entity);
|
|
129
128
|
if (options?.loaded && wrapped.__initialized && !wrapped.__onLoadFired) {
|
|
130
129
|
this.#loadedEntities.add(entity);
|
|
@@ -332,6 +331,7 @@ export class UnitOfWork {
|
|
|
332
331
|
let parentCs = changeSet.tptChangeSets.find(pc => pc.meta === current);
|
|
333
332
|
if (!parentCs) {
|
|
334
333
|
parentCs = new ChangeSet(entity, changeSet.type, {}, current);
|
|
334
|
+
parentCs.originalEntity = changeSet.originalEntity;
|
|
335
335
|
changeSet.tptChangeSets.splice(idx, 0, parentCs);
|
|
336
336
|
}
|
|
337
337
|
idx++;
|
|
@@ -472,6 +472,7 @@ export class UnitOfWork {
|
|
|
472
472
|
const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
|
|
473
473
|
await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
|
|
474
474
|
ctx: oldTx,
|
|
475
|
+
sessionContext: this.#em.getTransactionSessionContext(),
|
|
475
476
|
eventBroadcaster: new TransactionEventBroadcaster(this.#em),
|
|
476
477
|
loggerContext,
|
|
477
478
|
});
|
|
@@ -527,6 +528,10 @@ export class UnitOfWork {
|
|
|
527
528
|
if (prop.formula) {
|
|
528
529
|
delete referrer[prop.name];
|
|
529
530
|
}
|
|
531
|
+
else if (prop.primary) {
|
|
532
|
+
// the referrer's primary key contains the removed entity, so its identity cannot survive the removal
|
|
533
|
+
this.unsetIdentity(referrer);
|
|
534
|
+
}
|
|
530
535
|
else {
|
|
531
536
|
delete helper(referrer).__data[prop.name];
|
|
532
537
|
}
|
|
@@ -702,13 +707,14 @@ export class UnitOfWork {
|
|
|
702
707
|
payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
|
|
703
708
|
}
|
|
704
709
|
}
|
|
705
|
-
|
|
710
|
+
// the table declaring the version property or a concurrency check still needs its bump and lock check
|
|
711
|
+
if (!isCreate && Object.keys(payload).length === 0 && !current.hasOptimisticLock()) {
|
|
706
712
|
current = current.tptParent;
|
|
707
713
|
continue;
|
|
708
714
|
}
|
|
709
715
|
const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
|
|
716
|
+
cs.originalEntity = originalChangeSet.originalEntity;
|
|
710
717
|
if (current === meta) {
|
|
711
|
-
cs.originalEntity = originalChangeSet.originalEntity;
|
|
712
718
|
leafCs = cs;
|
|
713
719
|
}
|
|
714
720
|
else {
|
|
@@ -1205,7 +1211,8 @@ export class UnitOfWork {
|
|
|
1205
1211
|
const addToGroup = (cs) => {
|
|
1206
1212
|
// Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
|
|
1207
1213
|
if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) &&
|
|
1208
|
-
!Utils.hasObjectKeys(cs.payload)
|
|
1214
|
+
!Utils.hasObjectKeys(cs.payload) &&
|
|
1215
|
+
!cs.meta.hasOptimisticLock()) {
|
|
1209
1216
|
return;
|
|
1210
1217
|
}
|
|
1211
1218
|
const group = groups[cs.type];
|