@opengis/fastify-table 1.0.9 → 1.0.10
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/.eslintrc.cjs +42 -42
- package/Changelog.md +47 -47
- package/README.md +26 -26
- package/config.js +11 -11
- package/crud/controllers/deleteCrud.js +10 -10
- package/crud/controllers/insert.js +28 -28
- package/crud/controllers/update.js +29 -29
- package/crud/controllers/utils/checkXSS.js +45 -45
- package/crud/controllers/utils/xssInjection.js +72 -72
- package/crud/funcs/dataDelete.js +15 -15
- package/crud/funcs/dataInsert.js +24 -24
- package/crud/funcs/getIdByToken.js +29 -29
- package/crud/funcs/isFileExists.js +13 -13
- package/crud/funcs/setTokenById.js +55 -55
- package/helper.js +28 -28
- package/index.js +32 -32
- package/package.json +22 -22
- package/pg/funcs/autoIndex.js +89 -89
- package/pg/funcs/getMeta.js +27 -27
- package/pg/funcs/init.js +42 -42
- package/pg/funcs/pgClients.js +2 -2
- package/pg/index.js +35 -35
- package/pg/pgClients.js +17 -17
- package/policy/funcs/checkPolicy.js +74 -74
- package/policy/funcs/sqlInjection.js +33 -33
- package/policy/index.js +14 -14
- package/redis/client.js +8 -8
- package/redis/funcs/redisClients.js +2 -2
- package/redis/index.js +19 -19
- package/server/templates/form/test.dataset.form.json +411 -411
- package/server.js +14 -14
- package/table/controllers/data.js +57 -55
- package/table/controllers/filter.js +32 -24
- package/table/controllers/form.js +10 -10
- package/table/controllers/suggest.js +60 -60
- package/table/controllers/utils/getSelect.js +20 -20
- package/table/controllers/utils/getSelectMeta.js +66 -66
- package/table/funcs/getFilterSQL/index.js +75 -75
- package/table/funcs/getFilterSQL/util/formatValue.js +142 -142
- package/table/funcs/getFilterSQL/util/getCustomQuery.js +13 -13
- package/table/funcs/getFilterSQL/util/getFilterQuery.js +73 -73
- package/table/funcs/getFilterSQL/util/getOptimizedQuery.js +12 -12
- package/table/funcs/getFilterSQL/util/getTableSql.js +34 -34
- package/table/funcs/metaFormat/index.js +28 -0
- package/table/index.js +17 -14
- package/test/api/crud.test.js +50 -50
- package/test/api/crud.xss.test.js +70 -70
- package/test/api/table.test.js +49 -49
- package/test/config.example +18 -18
- package/test/funcs/crud.test.js +77 -77
- package/test/funcs/pg.test.js +32 -32
- package/test/funcs/redis.test.js +19 -19
- package/test/funcs/table.test.js +48 -48
- package/test/templates/cls/itree.recommend.json +26 -0
- package/test/templates/cls/itree.type_plant.json +65 -0
- package/test/templates/cls/test.json +9 -9
- package/test/templates/form/cp_building.form.json +32 -32
- package/test/templates/select/account_id.json +3 -3
- package/test/templates/select/contact_id.sql +1 -0
- package/test/templates/select/storage.data.json +2 -2
- package/test/templates/table/gis.dataset.table.json +20 -20
- package/test/templates/table/green_space.table.json +3 -3
package/test/funcs/pg.test.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
|
|
4
|
-
import '../config.js';
|
|
5
|
-
|
|
6
|
-
import getMeta from '../../pg/funcs/getMeta.js';
|
|
7
|
-
import autoIndex from '../../pg/funcs/autoIndex.js';
|
|
8
|
-
import pgClients from '../../pg/pgClients.js';
|
|
9
|
-
import rclient from '../../redis/client.js';
|
|
10
|
-
import getPG from '../../pg/funcs/getPG.js'
|
|
11
|
-
|
|
12
|
-
test('funcs pg', async (t) => {
|
|
13
|
-
await t.test('getMeta', async () => {
|
|
14
|
-
const { columns } = await getMeta({ table: 'gis.dataset' });
|
|
15
|
-
// console.log(columns)
|
|
16
|
-
assert.ok(columns);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
await t.test('getPG', async (t) => {
|
|
20
|
-
const data = await getPG({});
|
|
21
|
-
assert.ok(data);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
await t.test('autoIndex', async () => {
|
|
25
|
-
await autoIndex({ table: 'gis.dataset', columns: ['service_type'] });
|
|
26
|
-
assert.ok(1);
|
|
27
|
-
});
|
|
28
|
-
t.after(() => {
|
|
29
|
-
pgClients.client.end();
|
|
30
|
-
rclient.quit();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import '../config.js';
|
|
5
|
+
|
|
6
|
+
import getMeta from '../../pg/funcs/getMeta.js';
|
|
7
|
+
import autoIndex from '../../pg/funcs/autoIndex.js';
|
|
8
|
+
import pgClients from '../../pg/pgClients.js';
|
|
9
|
+
import rclient from '../../redis/client.js';
|
|
10
|
+
import getPG from '../../pg/funcs/getPG.js'
|
|
11
|
+
|
|
12
|
+
test('funcs pg', async (t) => {
|
|
13
|
+
await t.test('getMeta', async () => {
|
|
14
|
+
const { columns } = await getMeta({ table: 'gis.dataset' });
|
|
15
|
+
// console.log(columns)
|
|
16
|
+
assert.ok(columns);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await t.test('getPG', async (t) => {
|
|
20
|
+
const data = await getPG({});
|
|
21
|
+
assert.ok(data);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await t.test('autoIndex', async () => {
|
|
25
|
+
await autoIndex({ table: 'gis.dataset', columns: ['service_type'] });
|
|
26
|
+
assert.ok(1);
|
|
27
|
+
});
|
|
28
|
+
t.after(() => {
|
|
29
|
+
pgClients.client.end();
|
|
30
|
+
rclient.quit();
|
|
31
|
+
});
|
|
32
|
+
});
|
package/test/funcs/redis.test.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
|
|
4
|
-
import '../config.js';
|
|
5
|
-
|
|
6
|
-
import rclient from '../../redis/client.js';
|
|
7
|
-
|
|
8
|
-
test('funcs redis', async (t) => {
|
|
9
|
-
await t.test('get/set', async () => {
|
|
10
|
-
await rclient.set('test', '1');
|
|
11
|
-
const d = await rclient.get('test');
|
|
12
|
-
// console.log(columns)
|
|
13
|
-
assert.equal(d, '1');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
t.after(() => {
|
|
17
|
-
rclient.quit();
|
|
18
|
-
});
|
|
19
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import '../config.js';
|
|
5
|
+
|
|
6
|
+
import rclient from '../../redis/client.js';
|
|
7
|
+
|
|
8
|
+
test('funcs redis', async (t) => {
|
|
9
|
+
await t.test('get/set', async () => {
|
|
10
|
+
await rclient.set('test', '1');
|
|
11
|
+
const d = await rclient.get('test');
|
|
12
|
+
// console.log(columns)
|
|
13
|
+
assert.equal(d, '1');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
t.after(() => {
|
|
17
|
+
rclient.quit();
|
|
18
|
+
});
|
|
19
|
+
});
|
package/test/funcs/table.test.js
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import '../config.js';
|
|
4
|
-
import pgClients from '../../pg/pgClients.js';
|
|
5
|
-
import rclient from '../../redis/client.js';
|
|
6
|
-
import getFilterSQL from '../../table/funcs/getFilterSQL/index.js';
|
|
7
|
-
import getTableSql from '../../table/funcs/getFilterSQL/index.js';
|
|
8
|
-
import getCustomQuery from '../../table/funcs/getFilterSQL/index.js';
|
|
9
|
-
import getFilterQuery from '../../table/funcs/getFilterSQL/index.js';
|
|
10
|
-
import formatValue from '../../table/funcs/getFilterSQL/index.js';
|
|
11
|
-
import getOptimizedQuery from '../../table/funcs/getFilterSQL/index.js';
|
|
12
|
-
|
|
13
|
-
test('fucns table', async (t) => {
|
|
14
|
-
await t.test('getMeta', async () => {
|
|
15
|
-
const data = await getFilterSQL({ table: 'gis.dataset', filter: 'service_type=1' });
|
|
16
|
-
// console.log(data);
|
|
17
|
-
assert.ok(data.q);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
await t.test('formatValue', async (t) => {
|
|
21
|
-
const data = await formatValue( {table: 'gis.dataset'} );
|
|
22
|
-
assert.ok(data);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
await t.test('getCustomQuery', async (t) => {
|
|
26
|
-
const data = await getCustomQuery( {table: 'gis.dataset'} );
|
|
27
|
-
assert.ok(data);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await t.test('getFilterQuery', async (t) => {
|
|
31
|
-
const data = await getFilterQuery( {table: 'gis.dataset'} );
|
|
32
|
-
assert.ok(data);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
await t.test('getOptimizedQuery', async (t) => {
|
|
36
|
-
const data = await getOptimizedQuery( {table: 'gis.dataset'} );
|
|
37
|
-
assert.ok(data);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await t.test('getTableSql', async (t) => {
|
|
41
|
-
const data = await getTableSql( {table: 'gis.dataset'} );
|
|
42
|
-
assert.ok(data);
|
|
43
|
-
});
|
|
44
|
-
t.after(() => {
|
|
45
|
-
pgClients.client.end();
|
|
46
|
-
rclient.quit();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import '../config.js';
|
|
4
|
+
import pgClients from '../../pg/pgClients.js';
|
|
5
|
+
import rclient from '../../redis/client.js';
|
|
6
|
+
import getFilterSQL from '../../table/funcs/getFilterSQL/index.js';
|
|
7
|
+
import getTableSql from '../../table/funcs/getFilterSQL/index.js';
|
|
8
|
+
import getCustomQuery from '../../table/funcs/getFilterSQL/index.js';
|
|
9
|
+
import getFilterQuery from '../../table/funcs/getFilterSQL/index.js';
|
|
10
|
+
import formatValue from '../../table/funcs/getFilterSQL/index.js';
|
|
11
|
+
import getOptimizedQuery from '../../table/funcs/getFilterSQL/index.js';
|
|
12
|
+
|
|
13
|
+
test('fucns table', async (t) => {
|
|
14
|
+
await t.test('getMeta', async () => {
|
|
15
|
+
const data = await getFilterSQL({ table: 'gis.dataset', filter: 'service_type=1' });
|
|
16
|
+
// console.log(data);
|
|
17
|
+
assert.ok(data.q);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await t.test('formatValue', async (t) => {
|
|
21
|
+
const data = await formatValue( {table: 'gis.dataset'} );
|
|
22
|
+
assert.ok(data);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await t.test('getCustomQuery', async (t) => {
|
|
26
|
+
const data = await getCustomQuery( {table: 'gis.dataset'} );
|
|
27
|
+
assert.ok(data);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await t.test('getFilterQuery', async (t) => {
|
|
31
|
+
const data = await getFilterQuery( {table: 'gis.dataset'} );
|
|
32
|
+
assert.ok(data);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await t.test('getOptimizedQuery', async (t) => {
|
|
36
|
+
const data = await getOptimizedQuery( {table: 'gis.dataset'} );
|
|
37
|
+
assert.ok(data);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await t.test('getTableSql', async (t) => {
|
|
41
|
+
const data = await getTableSql( {table: 'gis.dataset'} );
|
|
42
|
+
assert.ok(data);
|
|
43
|
+
});
|
|
44
|
+
t.after(() => {
|
|
45
|
+
pgClients.client.end();
|
|
46
|
+
rclient.quit();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3",
|
|
4
|
+
"text": "Заміна",
|
|
5
|
+
"en": "Replacement",
|
|
6
|
+
"color": "#B8860B"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "2",
|
|
10
|
+
"text": "Видалення",
|
|
11
|
+
"en": "Removal",
|
|
12
|
+
"color": "#8B0000"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "1",
|
|
16
|
+
"text": "Обрізка",
|
|
17
|
+
"en": "Cutting",
|
|
18
|
+
"color": "#DEB887"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "4",
|
|
22
|
+
"text": "Відсутні",
|
|
23
|
+
"en": "None",
|
|
24
|
+
"color": "#2E8B57"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "4",
|
|
4
|
+
"text": "Газони",
|
|
5
|
+
"en": "Lawns",
|
|
6
|
+
"icon": "/assets/image/icon/60936458728884429/3f714a00-6dec-11ea-b853-074b5683525e.svg",
|
|
7
|
+
"color": "#87a96b",
|
|
8
|
+
"data": "lawns"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "8",
|
|
12
|
+
"text": "Пам’ятка природи",
|
|
13
|
+
"en": "Landmark",
|
|
14
|
+
"color": "blue",
|
|
15
|
+
"data": "landmark"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "7",
|
|
19
|
+
"text": "Пеньок",
|
|
20
|
+
"en": "Stump",
|
|
21
|
+
"icon": "/assets/image/icon/2322523888191276250/52ec1310-0d2b-11eb-ab6a-23ffd484ced7.svg",
|
|
22
|
+
"color": "#63594C",
|
|
23
|
+
"data": "stump"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "2",
|
|
27
|
+
"text": "Дерева",
|
|
28
|
+
"en": "Tree",
|
|
29
|
+
"icon": "/assets/image/icon/2202400955901674510/ba471bb0-5b13-11ea-9003-07912ed5d347.svg",
|
|
30
|
+
"color": "#66cd00",
|
|
31
|
+
"data": "tree"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "6",
|
|
35
|
+
"text": "Лунка",
|
|
36
|
+
"en": "Digger",
|
|
37
|
+
"icon": "/assets/image/icon/2322523536775709909/50634550-0d2b-11eb-ab6a-23ffd484ced7.svg",
|
|
38
|
+
"color": "#cdb79e",
|
|
39
|
+
"data": "digger"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "1",
|
|
43
|
+
"text": "Кущі",
|
|
44
|
+
"en": "Bush",
|
|
45
|
+
"icon": "/assets/image/icon/2202401113020302354/c62b1580-5b13-11ea-9003-07912ed5d347.svg",
|
|
46
|
+
"color": "#556b2f",
|
|
47
|
+
"data": "bush"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "3",
|
|
51
|
+
"text": "Живопліт",
|
|
52
|
+
"en": "Hedge",
|
|
53
|
+
"icon": "/assets/image/icon/59524404264212563/dd365ec0-6e6d-11ea-bc2a-d3dca36653f0.svg",
|
|
54
|
+
"color": "#9fa91f",
|
|
55
|
+
"data": "hedge"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "5",
|
|
59
|
+
"text": "Квітники",
|
|
60
|
+
"en": "Flowers",
|
|
61
|
+
"icon": "/assets/image/icon/2254649206885057992/e961fa00-5b13-11ea-9003-07912ed5d347.svg",
|
|
62
|
+
"color": "#ff7f24",
|
|
63
|
+
"data": "flowers"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"id": 1,
|
|
4
|
-
"text": "test"
|
|
5
|
-
},
|
|
6
|
-
{
|
|
7
|
-
"id": 2,
|
|
8
|
-
"text": "test2"
|
|
9
|
-
}
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": 1,
|
|
4
|
+
"text": "test"
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"id": 2,
|
|
8
|
+
"text": "test2"
|
|
9
|
+
}
|
|
10
10
|
]
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema": {
|
|
3
|
-
|
|
4
|
-
"cp_umuni_id": {
|
|
5
|
-
"type": "Text",
|
|
6
|
-
"ua": "ID UMUNI"
|
|
7
|
-
},
|
|
8
|
-
"cp_year": {
|
|
9
|
-
"type": "Text",
|
|
10
|
-
"ua": "Рік будівництва"
|
|
11
|
-
},
|
|
12
|
-
"cp_date_en_audit": {
|
|
13
|
-
"type": "DatePicker",
|
|
14
|
-
"ua": "Дата проведення останнього енергоаудиту"
|
|
15
|
-
},
|
|
16
|
-
"cp_certificate": {
|
|
17
|
-
"type": "Text",
|
|
18
|
-
"ua": "Сертифікат енергоефективності будівлі",
|
|
19
|
-
"help": "Вкажіть посилання"
|
|
20
|
-
},
|
|
21
|
-
"cp_pkd": {
|
|
22
|
-
"type": "Autocomplete",
|
|
23
|
-
"data": "customer_name",
|
|
24
|
-
"add": {
|
|
25
|
-
"model": "crm_acc.crm_account",
|
|
26
|
-
"ua": "Додати",
|
|
27
|
-
"form": "account_light.form"
|
|
28
|
-
},
|
|
29
|
-
"ua": "Замовник ПКД"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"label_style": "vertical"
|
|
1
|
+
{
|
|
2
|
+
"schema": {
|
|
3
|
+
|
|
4
|
+
"cp_umuni_id": {
|
|
5
|
+
"type": "Text",
|
|
6
|
+
"ua": "ID UMUNI"
|
|
7
|
+
},
|
|
8
|
+
"cp_year": {
|
|
9
|
+
"type": "Text",
|
|
10
|
+
"ua": "Рік будівництва"
|
|
11
|
+
},
|
|
12
|
+
"cp_date_en_audit": {
|
|
13
|
+
"type": "DatePicker",
|
|
14
|
+
"ua": "Дата проведення останнього енергоаудиту"
|
|
15
|
+
},
|
|
16
|
+
"cp_certificate": {
|
|
17
|
+
"type": "Text",
|
|
18
|
+
"ua": "Сертифікат енергоефективності будівлі",
|
|
19
|
+
"help": "Вкажіть посилання"
|
|
20
|
+
},
|
|
21
|
+
"cp_pkd": {
|
|
22
|
+
"type": "Autocomplete",
|
|
23
|
+
"data": "customer_name",
|
|
24
|
+
"add": {
|
|
25
|
+
"model": "crm_acc.crm_account",
|
|
26
|
+
"ua": "Додати",
|
|
27
|
+
"form": "account_light.form"
|
|
28
|
+
},
|
|
29
|
+
"ua": "Замовник ПКД"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"label_style": "vertical"
|
|
33
33
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{
|
|
2
|
-
"db": "mbk_lviv_dma",
|
|
3
|
-
"searchColumn": "alternative_name"
|
|
1
|
+
{
|
|
2
|
+
"db": "mbk_lviv_dma",
|
|
3
|
+
"searchColumn": "alternative_name"
|
|
4
4
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
select contact_id, coalesce(last_name,'')||' '||coalesce(first_name,'') from crm_acc.crm_contact order by last_name
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
{
|
|
2
|
-
"key": "dataset_id"
|
|
1
|
+
{
|
|
2
|
+
"key": "dataset_id"
|
|
3
3
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"columns": [
|
|
3
|
-
{
|
|
4
|
-
"name": "dataset_id",
|
|
5
|
-
"title": "22"
|
|
6
|
-
},
|
|
7
|
-
{
|
|
8
|
-
"name": "dataset_name",
|
|
9
|
-
"title": "dataset_name"
|
|
10
|
-
}
|
|
11
|
-
],
|
|
12
|
-
"table": "gis.dataset",
|
|
13
|
-
"order": "dataset_name",
|
|
14
|
-
"filters": [
|
|
15
|
-
{
|
|
16
|
-
"ua": "Назва набору",
|
|
17
|
-
"name": "dataset_name",
|
|
18
|
-
"type": "text"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"columns": [
|
|
3
|
+
{
|
|
4
|
+
"name": "dataset_id",
|
|
5
|
+
"title": "22"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "dataset_name",
|
|
9
|
+
"title": "dataset_name"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"table": "gis.dataset",
|
|
13
|
+
"order": "dataset_name",
|
|
14
|
+
"filters": [
|
|
15
|
+
{
|
|
16
|
+
"ua": "Назва набору",
|
|
17
|
+
"name": "dataset_name",
|
|
18
|
+
"type": "text"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
21
|
}
|