@mikro-orm/mariadb 7.0.2-dev.8 → 7.0.2
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/MariaDbDriver.d.ts +23 -6
- package/MariaDbDriver.js +20 -19
- package/MariaDbMikroORM.d.ts +52 -12
- package/MariaDbMikroORM.js +15 -14
- package/MariaDbPlatform.d.ts +4 -3
- package/MariaDbPlatform.js +10 -9
- package/MariaDbQueryBuilder.d.ts +7 -2
- package/MariaDbQueryBuilder.js +73 -76
- package/MariaDbSchemaHelper.d.ts +21 -8
- package/MariaDbSchemaHelper.js +143 -139
- package/README.md +128 -294
- package/index.d.ts +5 -1
- package/index.js +1 -1
- package/package.json +5 -5
package/MariaDbDriver.d.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyEntity,
|
|
3
|
+
type Configuration,
|
|
4
|
+
type ConnectionType,
|
|
5
|
+
type LoggingOptions,
|
|
6
|
+
type Transaction,
|
|
7
|
+
type Constructor,
|
|
8
|
+
type EntityName,
|
|
9
|
+
} from '@mikro-orm/core';
|
|
2
10
|
import { MySqlDriver, type SqlEntityManager } from '@mikro-orm/mysql';
|
|
3
11
|
import { MariaDbPlatform } from './MariaDbPlatform.js';
|
|
4
12
|
import { MariaDbQueryBuilder } from './MariaDbQueryBuilder.js';
|
|
5
13
|
import { MariaDbMikroORM } from './MariaDbMikroORM.js';
|
|
14
|
+
/** Database driver for MariaDB, extending the MySQL driver with MariaDB-specific behavior. */
|
|
6
15
|
export declare class MariaDbDriver extends MySqlDriver {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
readonly platform: MariaDbPlatform;
|
|
17
|
+
constructor(config: Configuration);
|
|
18
|
+
createQueryBuilder<T extends AnyEntity<T>>(
|
|
19
|
+
entityName: EntityName<T>,
|
|
20
|
+
ctx?: Transaction,
|
|
21
|
+
preferredConnectionType?: ConnectionType,
|
|
22
|
+
convertCustomTypes?: boolean,
|
|
23
|
+
loggerContext?: LoggingOptions,
|
|
24
|
+
alias?: string,
|
|
25
|
+
em?: SqlEntityManager,
|
|
26
|
+
): MariaDbQueryBuilder<T, any, any, any>;
|
|
27
|
+
/** @inheritDoc */
|
|
28
|
+
getORMClass(): Constructor<MariaDbMikroORM>;
|
|
12
29
|
}
|
package/MariaDbDriver.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import { QueryFlag
|
|
1
|
+
import { QueryFlag } from '@mikro-orm/core';
|
|
2
2
|
import { MySqlDriver } from '@mikro-orm/mysql';
|
|
3
3
|
import { MariaDbPlatform } from './MariaDbPlatform.js';
|
|
4
4
|
import { MariaDbQueryBuilder } from './MariaDbQueryBuilder.js';
|
|
5
5
|
import { MariaDbMikroORM } from './MariaDbMikroORM.js';
|
|
6
|
+
/** Database driver for MariaDB, extending the MySQL driver with MariaDB-specific behavior. */
|
|
6
7
|
export class MariaDbDriver extends MySqlDriver {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return qb;
|
|
21
|
-
}
|
|
22
|
-
/** @inheritDoc */
|
|
23
|
-
getORMClass() {
|
|
24
|
-
return MariaDbMikroORM;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super(config);
|
|
10
|
+
this.platform = new MariaDbPlatform();
|
|
11
|
+
}
|
|
12
|
+
createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, loggerContext, alias, em) {
|
|
13
|
+
// do not compute the connectionType if EM is provided as it will be computed from it in the QB later on
|
|
14
|
+
const connectionType = em
|
|
15
|
+
? preferredConnectionType
|
|
16
|
+
: this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
|
|
17
|
+
const qb = new MariaDbQueryBuilder(entityName, this.metadata, this, ctx, alias, connectionType, em, loggerContext);
|
|
18
|
+
if (!convertCustomTypes) {
|
|
19
|
+
qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
|
|
25
20
|
}
|
|
21
|
+
return qb;
|
|
22
|
+
}
|
|
23
|
+
/** @inheritDoc */
|
|
24
|
+
getORMClass() {
|
|
25
|
+
return MariaDbMikroORM;
|
|
26
|
+
}
|
|
26
27
|
}
|
package/MariaDbMikroORM.d.ts
CHANGED
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyEntity,
|
|
3
|
+
type EntityClass,
|
|
4
|
+
type EntitySchema,
|
|
5
|
+
MikroORM,
|
|
6
|
+
type Options,
|
|
7
|
+
type IDatabaseDriver,
|
|
8
|
+
type EntityManager,
|
|
9
|
+
type EntityManagerType,
|
|
10
|
+
} from '@mikro-orm/core';
|
|
2
11
|
import type { SqlEntityManager } from '@mikro-orm/mysql';
|
|
3
12
|
import { MariaDbDriver } from './MariaDbDriver.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
13
|
+
/** Configuration options for the MariaDB driver. */
|
|
14
|
+
export type MariaDbOptions<
|
|
15
|
+
EM extends SqlEntityManager<MariaDbDriver> = SqlEntityManager<MariaDbDriver>,
|
|
16
|
+
Entities extends (string | EntityClass<AnyEntity> | EntitySchema)[] = (
|
|
17
|
+
| string
|
|
18
|
+
| EntityClass<AnyEntity>
|
|
19
|
+
| EntitySchema
|
|
20
|
+
)[],
|
|
21
|
+
> = Partial<Options<MariaDbDriver, EM, Entities>>;
|
|
22
|
+
/** Creates a type-safe configuration object for the MariaDB driver. */
|
|
23
|
+
export declare function defineMariaDbConfig<
|
|
24
|
+
EM extends SqlEntityManager<MariaDbDriver> = SqlEntityManager<MariaDbDriver>,
|
|
25
|
+
Entities extends (string | EntityClass<AnyEntity> | EntitySchema)[] = (
|
|
26
|
+
| string
|
|
27
|
+
| EntityClass<AnyEntity>
|
|
28
|
+
| EntitySchema
|
|
29
|
+
)[],
|
|
30
|
+
>(options: Partial<Options<MariaDbDriver, EM, Entities>>): Partial<Options<MariaDbDriver, EM, Entities>>;
|
|
6
31
|
/**
|
|
7
32
|
* @inheritDoc
|
|
8
33
|
*/
|
|
9
|
-
export declare class MariaDbMikroORM<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
export declare class MariaDbMikroORM<
|
|
35
|
+
EM extends SqlEntityManager<MariaDbDriver> = SqlEntityManager<MariaDbDriver>,
|
|
36
|
+
Entities extends (string | EntityClass<AnyEntity> | EntitySchema)[] = (
|
|
37
|
+
| string
|
|
38
|
+
| EntityClass<AnyEntity>
|
|
39
|
+
| EntitySchema
|
|
40
|
+
)[],
|
|
41
|
+
> extends MikroORM<MariaDbDriver, EM, Entities> {
|
|
42
|
+
/**
|
|
43
|
+
* @inheritDoc
|
|
44
|
+
*/
|
|
45
|
+
static init<
|
|
46
|
+
D extends IDatabaseDriver = MariaDbDriver,
|
|
47
|
+
EM extends EntityManager<D> = D[typeof EntityManagerType] & EntityManager<D>,
|
|
48
|
+
Entities extends (string | EntityClass<AnyEntity> | EntitySchema)[] = (
|
|
49
|
+
| string
|
|
50
|
+
| EntityClass<AnyEntity>
|
|
51
|
+
| EntitySchema
|
|
52
|
+
)[],
|
|
53
|
+
>(options: Partial<Options<D, EM, Entities>>): Promise<MikroORM<D, EM, Entities>>;
|
|
54
|
+
/**
|
|
55
|
+
* @inheritDoc
|
|
56
|
+
*/
|
|
57
|
+
constructor(options: Partial<Options<MariaDbDriver, EM, Entities>>);
|
|
18
58
|
}
|
package/MariaDbMikroORM.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { defineConfig, MikroORM
|
|
1
|
+
import { defineConfig, MikroORM } from '@mikro-orm/core';
|
|
2
2
|
import { MariaDbDriver } from './MariaDbDriver.js';
|
|
3
|
+
/** Creates a type-safe configuration object for the MariaDB driver. */
|
|
3
4
|
export function defineMariaDbConfig(options) {
|
|
4
|
-
|
|
5
|
+
return defineConfig({ driver: MariaDbDriver, ...options });
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* @inheritDoc
|
|
8
9
|
*/
|
|
9
10
|
export class MariaDbMikroORM extends MikroORM {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @inheritDoc
|
|
13
|
+
*/
|
|
14
|
+
static async init(options) {
|
|
15
|
+
return super.init(defineMariaDbConfig(options));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @inheritDoc
|
|
19
|
+
*/
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super(defineMariaDbConfig(options));
|
|
22
|
+
}
|
|
22
23
|
}
|
package/MariaDbPlatform.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { MySqlPlatform, type TransformContext } from '@mikro-orm/mysql';
|
|
2
2
|
import { MariaDbSchemaHelper } from './MariaDbSchemaHelper.js';
|
|
3
|
+
/** Platform implementation for MariaDB. */
|
|
3
4
|
export declare class MariaDbPlatform extends MySqlPlatform {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
protected readonly schemaHelper: MariaDbSchemaHelper;
|
|
6
|
+
convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
|
|
7
|
+
convertsJsonAutomatically(): boolean;
|
|
7
8
|
}
|
package/MariaDbPlatform.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { MySqlPlatform } from '@mikro-orm/mysql';
|
|
2
2
|
import { MariaDbSchemaHelper } from './MariaDbSchemaHelper.js';
|
|
3
|
+
/** Platform implementation for MariaDB. */
|
|
3
4
|
export class MariaDbPlatform extends MySqlPlatform {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
return JSON.stringify(value);
|
|
10
|
-
}
|
|
11
|
-
convertsJsonAutomatically() {
|
|
12
|
-
return false;
|
|
5
|
+
schemaHelper = new MariaDbSchemaHelper(this);
|
|
6
|
+
convertJsonToDatabaseValue(value, context) {
|
|
7
|
+
if (context?.mode === 'hydration') {
|
|
8
|
+
return value;
|
|
13
9
|
}
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
}
|
|
12
|
+
convertsJsonAutomatically() {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
14
15
|
}
|
package/MariaDbQueryBuilder.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { QueryBuilder } from '@mikro-orm/mysql';
|
|
|
3
3
|
/**
|
|
4
4
|
* @inheritDoc
|
|
5
5
|
*/
|
|
6
|
-
export declare class MariaDbQueryBuilder<
|
|
7
|
-
|
|
6
|
+
export declare class MariaDbQueryBuilder<
|
|
7
|
+
Entity extends object = AnyEntity,
|
|
8
|
+
RootAlias extends string = never,
|
|
9
|
+
Hint extends string = never,
|
|
10
|
+
Context extends object = never,
|
|
11
|
+
> extends QueryBuilder<Entity, RootAlias, Hint, Context> {
|
|
12
|
+
protected wrapPaginateSubQuery(meta: EntityMetadata): void;
|
|
8
13
|
}
|
package/MariaDbQueryBuilder.js
CHANGED
|
@@ -4,83 +4,80 @@ import { QueryBuilder } from '@mikro-orm/mysql';
|
|
|
4
4
|
* @inheritDoc
|
|
5
5
|
*/
|
|
6
6
|
export class MariaDbQueryBuilder extends QueryBuilder {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
const key = raw(`min(${this.platform.quoteIdentifier(fieldName)}${type})`);
|
|
37
|
-
orderBy.push({ [key]: direction });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
subQuery.orderBy(orderBy);
|
|
7
|
+
wrapPaginateSubQuery(meta) {
|
|
8
|
+
const pks = this.prepareFields(meta.primaryKeys, 'sub-query');
|
|
9
|
+
const quotedPKs = pks.map(pk => this.platform.quoteIdentifier(pk));
|
|
10
|
+
const subQuery = this.clone(['orderBy', 'fields']).select(pks).groupBy(pks).limit(this.state.limit);
|
|
11
|
+
// revert the on conditions added via populateWhere, we want to apply those only once
|
|
12
|
+
Object.values(subQuery.state.joins).forEach(join => (join.cond = join.cond_ ?? {}));
|
|
13
|
+
if (this.state.offset) {
|
|
14
|
+
subQuery.offset(this.state.offset);
|
|
15
|
+
}
|
|
16
|
+
const addToSelect = [];
|
|
17
|
+
if (this.state.orderBy.length > 0) {
|
|
18
|
+
const orderBy = [];
|
|
19
|
+
for (const orderMap of this.state.orderBy) {
|
|
20
|
+
for (const field of Utils.getObjectQueryKeys(orderMap)) {
|
|
21
|
+
const direction = orderMap[field];
|
|
22
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
23
|
+
orderBy.push({ [field]: direction });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const [a, f] = this.helper.splitField(field);
|
|
27
|
+
const prop = this.helper.getProperty(f, a);
|
|
28
|
+
const type = this.platform.castColumn(prop);
|
|
29
|
+
const fieldName = this.helper.mapper(field, this.type, undefined, null);
|
|
30
|
+
if (!prop?.persist && !prop?.formula && !pks.includes(fieldName)) {
|
|
31
|
+
addToSelect.push(fieldName);
|
|
32
|
+
}
|
|
33
|
+
const key = raw(`min(${this.platform.quoteIdentifier(fieldName)}${type})`);
|
|
34
|
+
orderBy.push({ [key]: direction });
|
|
41
35
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
36
|
+
}
|
|
37
|
+
subQuery.orderBy(orderBy);
|
|
38
|
+
}
|
|
39
|
+
subQuery.state.finalized = true;
|
|
40
|
+
const innerQuery = subQuery.as(this.mainAlias.aliasName).clear('select').select(pks);
|
|
41
|
+
/* v8 ignore next */
|
|
42
|
+
if (addToSelect.length > 0) {
|
|
43
|
+
addToSelect.forEach(prop => {
|
|
44
|
+
const field = this.state.fields.find(field => {
|
|
45
|
+
if (typeof field === 'object' && field && '__as' in field) {
|
|
46
|
+
return field.__as === prop;
|
|
47
|
+
}
|
|
48
|
+
if (field instanceof RawQueryFragment) {
|
|
49
|
+
// not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
|
|
50
|
+
return field.sql.includes(prop);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
});
|
|
54
|
+
if (field instanceof RawQueryFragment) {
|
|
55
|
+
innerQuery.select(this.platform.formatQuery(field.sql, field.params));
|
|
56
|
+
} else if (field) {
|
|
57
|
+
innerQuery.select(field);
|
|
64
58
|
}
|
|
65
|
-
|
|
66
|
-
// https://stackoverflow.com/questions/17892762/mysql-this-version-of-mysql-doesnt-yet-support-limit-in-all-any-some-subqu
|
|
67
|
-
const subSubQuery = this.platform.createNativeQueryBuilder();
|
|
68
|
-
subSubQuery.select(raw(`json_arrayagg(${quotedPKs.join(', ')})`)).from(innerQuery);
|
|
69
|
-
this.state.limit = undefined;
|
|
70
|
-
this.state.offset = undefined;
|
|
71
|
-
// Save the original WHERE conditions before pruning joins
|
|
72
|
-
const originalCond = this.state.cond;
|
|
73
|
-
const populatePaths = this.getPopulatePaths();
|
|
74
|
-
// Remove joins that are not used for population or ordering
|
|
75
|
-
this.pruneJoinsForPagination(meta, populatePaths);
|
|
76
|
-
// Transfer WHERE conditions to ORDER BY joins (GH #6160)
|
|
77
|
-
this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
|
|
78
|
-
const subquerySql = subSubQuery.toString();
|
|
79
|
-
const key = meta.getPrimaryProps()[0].runtimeType === 'string'
|
|
80
|
-
? `concat('"', ${quotedPKs.join(', ')}, '"')`
|
|
81
|
-
: quotedPKs.join(', ');
|
|
82
|
-
const sql = `json_contains((${subquerySql}), ${key})`;
|
|
83
|
-
this.state.cond = {};
|
|
84
|
-
this.select(this.state.fields).where(sql);
|
|
59
|
+
});
|
|
85
60
|
}
|
|
61
|
+
// multiple sub-queries are needed to get around mysql limitations with order by + limit + where in + group by (o.O)
|
|
62
|
+
// https://stackoverflow.com/questions/17892762/mysql-this-version-of-mysql-doesnt-yet-support-limit-in-all-any-some-subqu
|
|
63
|
+
const subSubQuery = this.platform.createNativeQueryBuilder();
|
|
64
|
+
subSubQuery.select(raw(`json_arrayagg(${quotedPKs.join(', ')})`)).from(innerQuery);
|
|
65
|
+
this.state.limit = undefined;
|
|
66
|
+
this.state.offset = undefined;
|
|
67
|
+
// Save the original WHERE conditions before pruning joins
|
|
68
|
+
const originalCond = this.state.cond;
|
|
69
|
+
const populatePaths = this.getPopulatePaths();
|
|
70
|
+
// Remove joins that are not used for population or ordering
|
|
71
|
+
this.pruneJoinsForPagination(meta, populatePaths);
|
|
72
|
+
// Transfer WHERE conditions to ORDER BY joins (GH #6160)
|
|
73
|
+
this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
|
|
74
|
+
const subquerySql = subSubQuery.toString();
|
|
75
|
+
const key =
|
|
76
|
+
meta.getPrimaryProps()[0].runtimeType === 'string'
|
|
77
|
+
? `concat('"', ${quotedPKs.join(', ')}, '"')`
|
|
78
|
+
: quotedPKs.join(', ');
|
|
79
|
+
const sql = `json_contains((${subquerySql}), ${key})`;
|
|
80
|
+
this.state.cond = {};
|
|
81
|
+
this.select(this.state.fields).where(sql);
|
|
82
|
+
}
|
|
86
83
|
}
|
package/MariaDbSchemaHelper.d.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AbstractSqlConnection,
|
|
3
|
+
type CheckDef,
|
|
4
|
+
type Column,
|
|
5
|
+
type IndexDef,
|
|
6
|
+
type DatabaseSchema,
|
|
7
|
+
type Table,
|
|
8
|
+
MySqlSchemaHelper,
|
|
9
|
+
} from '@mikro-orm/mysql';
|
|
2
10
|
import { type Dictionary, type Type } from '@mikro-orm/core';
|
|
11
|
+
/** Schema introspection helper for MariaDB. */
|
|
3
12
|
export declare class MariaDbSchemaHelper extends MySqlSchemaHelper {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
protected appendMySqlIndexSuffix(sql: string, index: IndexDef): string;
|
|
14
|
+
loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[]): Promise<void>;
|
|
15
|
+
getAllIndexes(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<IndexDef[]>>;
|
|
16
|
+
getAllColumns(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<Column[]>>;
|
|
17
|
+
getAllChecks(
|
|
18
|
+
connection: AbstractSqlConnection,
|
|
19
|
+
tables: Table[],
|
|
20
|
+
columns?: Dictionary<Column[]>,
|
|
21
|
+
): Promise<Dictionary<CheckDef[]>>;
|
|
22
|
+
protected getChecksSQL(tables: Table[]): string;
|
|
23
|
+
protected wrap(val: string | undefined | null, type: Type<unknown>): string | undefined | null;
|
|
11
24
|
}
|