@opengis/fastify-table 1.0.37 → 1.0.38

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.
Files changed (37) hide show
  1. package/.eslintrc.cjs +42 -42
  2. package/Changelog.md +109 -109
  3. package/README.md +26 -26
  4. package/config.js +12 -12
  5. package/crud/controllers/deleteCrud.js +14 -14
  6. package/crud/controllers/utils/checkXSS.js +45 -45
  7. package/crud/controllers/utils/xssInjection.js +72 -72
  8. package/crud/funcs/dataDelete.js +15 -15
  9. package/crud/funcs/dataInsert.js +24 -24
  10. package/crud/funcs/dataUpdate.js +24 -24
  11. package/crud/funcs/getToken.js +27 -27
  12. package/crud/funcs/isFileExists.js +13 -13
  13. package/crud/funcs/setToken.js +53 -53
  14. package/index.js +39 -39
  15. package/package.json +22 -22
  16. package/pg/funcs/getPG.js +29 -29
  17. package/redis/funcs/getRedis.js +23 -23
  18. package/server/migrations/crm.sql +95 -95
  19. package/server/migrations/log.sql +41 -41
  20. package/server.js +14 -14
  21. package/table/controllers/data.js +1 -1
  22. package/table/controllers/filter.js +37 -37
  23. package/table/controllers/search.js +41 -41
  24. package/table/funcs/getFilterSQL/util/getTableSql.js +34 -34
  25. package/test/api/notification.test.js +2 -2
  26. package/test/api/table.test.js +6 -6
  27. package/test/api/widget.test.js +114 -114
  28. package/test/config.example +18 -18
  29. package/test/funcs/crud.test.js +76 -76
  30. package/test/funcs/notification.test.js +9 -9
  31. package/test/funcs/pg.test.js +34 -34
  32. package/test/funcs/redis.test.js +19 -19
  33. package/test/templates/cls/test.json +9 -9
  34. package/test/templates/form/cp_building.form.json +32 -32
  35. package/test/templates/select/account_id.json +3 -3
  36. package/test/templates/select/storage.data.json +2 -2
  37. package/test/templates/table/gis.dataset.table.json +20 -20
package/.eslintrc.cjs CHANGED
@@ -1,42 +1,42 @@
1
- /* eslint-env node */
2
-
3
- module.exports = {
4
- env: {
5
- node: true,
6
- },
7
- root: true,
8
- extends: [
9
- 'eslint:recommended',
10
- 'airbnb-base',
11
-
12
- ],
13
- rules: {
14
- 'brace-style': [2, 'stroustrup', { allowSingleLine: true }],
15
- 'vue/max-attributes-per-line': 0,
16
- 'vue/valid-v-for': 0,
17
-
18
- // allow async-await
19
- 'generator-star-spacing': 'off',
20
-
21
- // allow paren-less arrow functions
22
- 'arrow-parens': 0,
23
- 'one-var': 0,
24
- 'max-len': 0,
25
- 'import/first': 0,
26
- 'import/named': 2,
27
- 'import/namespace': 2,
28
- 'import/default': 2,
29
- 'import/export': 2,
30
- 'import/extensions': 0,
31
- 'no-console': ['warn', { allow: ['warn', 'error'] }],
32
- 'import/no-unresolved': 0,
33
- 'import/no-extraneous-dependencies': 0,
34
- 'linebreak-style': ['error', 'unix'],
35
- // allow debugger during development
36
- 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
37
- },
38
-
39
- parserOptions: {
40
- ecmaVersion: 'latest',
41
- },
42
- };
1
+ /* eslint-env node */
2
+
3
+ module.exports = {
4
+ env: {
5
+ node: true,
6
+ },
7
+ root: true,
8
+ extends: [
9
+ 'eslint:recommended',
10
+ 'airbnb-base',
11
+
12
+ ],
13
+ rules: {
14
+ 'brace-style': [2, 'stroustrup', { allowSingleLine: true }],
15
+ 'vue/max-attributes-per-line': 0,
16
+ 'vue/valid-v-for': 0,
17
+
18
+ // allow async-await
19
+ 'generator-star-spacing': 'off',
20
+
21
+ // allow paren-less arrow functions
22
+ 'arrow-parens': 0,
23
+ 'one-var': 0,
24
+ 'max-len': 0,
25
+ 'import/first': 0,
26
+ 'import/named': 2,
27
+ 'import/namespace': 2,
28
+ 'import/default': 2,
29
+ 'import/export': 2,
30
+ 'import/extensions': 0,
31
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
32
+ 'import/no-unresolved': 0,
33
+ 'import/no-extraneous-dependencies': 0,
34
+ 'linebreak-style': ['error', 'unix'],
35
+ // allow debugger during development
36
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
37
+ },
38
+
39
+ parserOptions: {
40
+ ecmaVersion: 'latest',
41
+ },
42
+ };
package/Changelog.md CHANGED
@@ -1,109 +1,109 @@
1
- # fastify-table
2
-
3
- ## 1.0.37 - 23.05.2024
4
-
5
- - add gallery widget
6
-
7
- ## 1.0.36 - 22.05.2024
8
-
9
- - fix filter api array processing
10
-
11
- ## 1.0.31 - 20.05.2024
12
-
13
- - widget db structure refactor
14
-
15
- ## 1.0.30 - 17.05.2024
16
-
17
- - code optimization
18
-
19
- ## 1.0.29 - 17.05.2024
20
-
21
- - widget api post (file)
22
-
23
- ## 1.0.28 - 14.05.2024
24
-
25
- - dblist api set
26
-
27
- ## 1.0.27 - 12.05.2024
28
-
29
- - code optimization
30
-
31
- ## 1.0.26 - 09.05.2024
32
-
33
- - fix getTableSql
34
-
35
- ## 1.0.25 - 08.05.2024
36
-
37
- - decorator to hook
38
-
39
- ## 1.0.24 - 07.05.2024
40
-
41
- - getTemplate page
42
-
43
- ## 1.0.23 - 07.05.2024
44
-
45
- - getTemplate funcs
46
- - dblist api
47
-
48
- ## 1.0.22 - 03.05.2024
49
-
50
- - getFilterSQL funcs
51
-
52
- ## 1.0.21 - 03.05.2024
53
-
54
- - fix widget db structure
55
-
56
- ## 1.0.20 - 03.05.2024
57
-
58
- - fix filter separator
59
-
60
- ## 1.0.19 - 02.05.2024
61
-
62
- - widget plugin
63
- - notification plugin
64
-
65
- ## 1.0.9 - 29.04.2024
66
-
67
- - crud token support
68
- - security - xss restriction
69
-
70
- ## 1.0.8 - 29.04.2024
71
-
72
- - filter fix
73
-
74
- ## 1.0.7 - 26.04.2024
75
-
76
- - code optimization
77
-
78
- ## 1.0.6 - 25.04.2024
79
-
80
- - code optimization
81
-
82
- ## 1.0.5 - 24.04.2024
83
-
84
- - code optimization
85
-
86
- ## 1.0.4 - 20.04.2024
87
-
88
- - data api - order
89
- - suggest api - db support
90
- - del api fix
91
-
92
- ## 1.0.3 - 17.04.2024
93
-
94
- - fix unit test
95
-
96
- ## 1.0.2 - 14.04.2024
97
-
98
- - fix redis
99
-
100
- ## 1.0.1 - 14.04.2024
101
-
102
- - fix redis
103
-
104
- ## 1.0.0 - 14.04.2024
105
-
106
- - crud
107
- - pg
108
- - redis
109
- - table
1
+ # fastify-table
2
+
3
+ ## 1.0.37 - 23.05.2024
4
+
5
+ - add gallery widget
6
+
7
+ ## 1.0.36 - 22.05.2024
8
+
9
+ - fix filter api array processing
10
+
11
+ ## 1.0.31 - 20.05.2024
12
+
13
+ - widget db structure refactor
14
+
15
+ ## 1.0.30 - 17.05.2024
16
+
17
+ - code optimization
18
+
19
+ ## 1.0.29 - 17.05.2024
20
+
21
+ - widget api post (file)
22
+
23
+ ## 1.0.28 - 14.05.2024
24
+
25
+ - dblist api set
26
+
27
+ ## 1.0.27 - 12.05.2024
28
+
29
+ - code optimization
30
+
31
+ ## 1.0.26 - 09.05.2024
32
+
33
+ - fix getTableSql
34
+
35
+ ## 1.0.25 - 08.05.2024
36
+
37
+ - decorator to hook
38
+
39
+ ## 1.0.24 - 07.05.2024
40
+
41
+ - getTemplate page
42
+
43
+ ## 1.0.23 - 07.05.2024
44
+
45
+ - getTemplate funcs
46
+ - dblist api
47
+
48
+ ## 1.0.22 - 03.05.2024
49
+
50
+ - getFilterSQL funcs
51
+
52
+ ## 1.0.21 - 03.05.2024
53
+
54
+ - fix widget db structure
55
+
56
+ ## 1.0.20 - 03.05.2024
57
+
58
+ - fix filter separator
59
+
60
+ ## 1.0.19 - 02.05.2024
61
+
62
+ - widget plugin
63
+ - notification plugin
64
+
65
+ ## 1.0.9 - 29.04.2024
66
+
67
+ - crud token support
68
+ - security - xss restriction
69
+
70
+ ## 1.0.8 - 29.04.2024
71
+
72
+ - filter fix
73
+
74
+ ## 1.0.7 - 26.04.2024
75
+
76
+ - code optimization
77
+
78
+ ## 1.0.6 - 25.04.2024
79
+
80
+ - code optimization
81
+
82
+ ## 1.0.5 - 24.04.2024
83
+
84
+ - code optimization
85
+
86
+ ## 1.0.4 - 20.04.2024
87
+
88
+ - data api - order
89
+ - suggest api - db support
90
+ - del api fix
91
+
92
+ ## 1.0.3 - 17.04.2024
93
+
94
+ - fix unit test
95
+
96
+ ## 1.0.2 - 14.04.2024
97
+
98
+ - fix redis
99
+
100
+ ## 1.0.1 - 14.04.2024
101
+
102
+ - fix redis
103
+
104
+ ## 1.0.0 - 14.04.2024
105
+
106
+ - crud
107
+ - pg
108
+ - redis
109
+ - table
package/README.md CHANGED
@@ -1,26 +1,26 @@
1
- # fastify-table
2
-
3
- [![NPM version](https://img.shields.io/npm/v/@opengis/fastify-table)](https://www.npmjs.com/package/@opengis/fastify-table)
4
- [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
5
-
6
- It standardizes the entire form building process, while taking care of everything from rendering to validation and processing:
7
-
8
- - pg
9
- - redis
10
- - crud
11
-
12
- ## Install
13
-
14
- ```bash
15
- npm i @opengis/fastify-table
16
- ```
17
-
18
- ## Usage
19
-
20
- ```js
21
- fastify.register(import('@opengis/fastify-table'), config);
22
- ```
23
-
24
- ## Documenation
25
-
26
- For a detailed understanding fastify-table, its features, and how to use them, refer to our [Documentation](https://apidocs.softpro.ua/gis.storage/).
1
+ # fastify-table
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/@opengis/fastify-table)](https://www.npmjs.com/package/@opengis/fastify-table)
4
+ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
5
+
6
+ It standardizes the entire form building process, while taking care of everything from rendering to validation and processing:
7
+
8
+ - pg
9
+ - redis
10
+ - crud
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm i @opengis/fastify-table
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ fastify.register(import('@opengis/fastify-table'), config);
22
+ ```
23
+
24
+ ## Documenation
25
+
26
+ For a detailed understanding fastify-table, its features, and how to use them, refer to our [Documentation](https://apidocs.softpro.ua/gis.storage/).
package/config.js CHANGED
@@ -1,12 +1,12 @@
1
- import fs from 'fs';
2
-
3
- import { readFile } from 'fs/promises';
4
-
5
- const fileName = ['/data/local/config.json', 'config.json'].find(el => (fs.existsSync(el) ? el : null));
6
- const config = fileName ? await readFile(fileName).then(el => JSON.parse(el)) : {};
7
-
8
- Object.assign(config, {
9
- allTemplates: config?.allTemplates || {},
10
- });
11
-
12
- export default config;
1
+ import fs from 'fs';
2
+
3
+ import { readFile } from 'fs/promises';
4
+
5
+ const fileName = ['/data/local/config.json', 'config.json'].find(el => (fs.existsSync(el) ? el : null));
6
+ const config = fileName ? await readFile(fileName).then(el => JSON.parse(el)) : {};
7
+
8
+ Object.assign(config, {
9
+ allTemplates: config?.allTemplates || {},
10
+ });
11
+
12
+ export default config;
@@ -1,14 +1,14 @@
1
- import dataDelete from '../funcs/dataDelete.js';
2
- import getTemplate from '../../table/controllers/utils/getTemplate.js';
3
-
4
- export default async function deleteCrud(req) {
5
- const loadTemplate = await getTemplate('table', req.params.table);
6
- const { table } = loadTemplate || req.params || {};
7
- const { id } = req.params || {};
8
-
9
- if (!table) return { status: 404, message: 'table is required' };
10
-
11
- const data = await dataDelete({ table, id });
12
-
13
- return { rowCount: data.rowCount, msg: !data.rowCount ? data : null };
14
- }
1
+ import dataDelete from '../funcs/dataDelete.js';
2
+ import getTemplate from '../../table/controllers/utils/getTemplate.js';
3
+
4
+ export default async function deleteCrud(req) {
5
+ const loadTemplate = await getTemplate('table', req.params.table);
6
+ const { table } = loadTemplate || req.params || {};
7
+ const { id } = req.params || {};
8
+
9
+ if (!table) return { status: 404, message: 'table is required' };
10
+
11
+ const data = await dataDelete({ table, id });
12
+
13
+ return { rowCount: data.rowCount, msg: !data.rowCount ? data : null };
14
+ }
@@ -1,45 +1,45 @@
1
- /* import sqlInjection from '../../../policy/funcs/sqlInjection.js'; */
2
- import xssInjection from './xssInjection.js';
3
-
4
- /* const checkList = xssInjection.concat(sqlInjection); */
5
-
6
- // RTE - rich text editor
7
-
8
- function checkXSS({ body, schema = {} }) {
9
- const data = typeof body === 'string' ? body : JSON.stringify(body);
10
- const stopWords = xssInjection.filter((el) => data.toLowerCase().includes(el));
11
-
12
- // check sql injection
13
- const stopSpecialSymbols = data.match(/\p{S}OR\p{S}|\p{P}OR\p{P}| OR |\+OR\+/gi);
14
- if (stopSpecialSymbols?.length) stopSpecialSymbols?.forEach((el) => stopWords.push(el));
15
-
16
- // escape arrows on non-RTE
17
- Object.keys(body)
18
- .filter((key) => ['<', '>'].find((el) => body[key]?.includes?.(el))
19
- && !['Summernote', 'Tiny', 'Ace'].includes(schema[key]?.type))
20
- ?.forEach((key) => {
21
- Object.assign(body, { [key]: body[key].replace(/</g, '&lt;').replace(/>/g, '&gt;') });
22
- });
23
- // try { } catch (err) { return { error: err.toString() }; }
24
-
25
- if (!stopWords.length) return { body };
26
-
27
- const disabledCheckFields = Object.keys(schema)?.filter((el) => schema[el]?.xssCheck === false); // exclude specific columns
28
-
29
- // check RTE
30
- /* const richTextFields = Object.keys(schema).filter((el) => ['Summernote', 'Tiny', 'Ace'].includes(schema[el]?.type));
31
- richTextFields.filter((key) => !checkList.find((el) => body[key].includes(el)))?.forEach((key) => {
32
- disabledCheckFields.push(key);
33
- }); */
34
-
35
- const field = Object.keys(body)
36
- ?.find((key) => body[key]
37
- && !disabledCheckFields.includes(key)
38
- && body[key].toLowerCase().includes(stopWords[0]));
39
- if (field) {
40
- return { error: `rule: ${stopWords[0]} | attr: ${field} | val: ${body[field]}`, body };
41
- }
42
- return { body };
43
- }
44
-
45
- export default checkXSS;
1
+ /* import sqlInjection from '../../../policy/funcs/sqlInjection.js'; */
2
+ import xssInjection from './xssInjection.js';
3
+
4
+ /* const checkList = xssInjection.concat(sqlInjection); */
5
+
6
+ // RTE - rich text editor
7
+
8
+ function checkXSS({ body, schema = {} }) {
9
+ const data = typeof body === 'string' ? body : JSON.stringify(body);
10
+ const stopWords = xssInjection.filter((el) => data.toLowerCase().includes(el));
11
+
12
+ // check sql injection
13
+ const stopSpecialSymbols = data.match(/\p{S}OR\p{S}|\p{P}OR\p{P}| OR |\+OR\+/gi);
14
+ if (stopSpecialSymbols?.length) stopSpecialSymbols?.forEach((el) => stopWords.push(el));
15
+
16
+ // escape arrows on non-RTE
17
+ Object.keys(body)
18
+ .filter((key) => ['<', '>'].find((el) => body[key]?.includes?.(el))
19
+ && !['Summernote', 'Tiny', 'Ace'].includes(schema[key]?.type))
20
+ ?.forEach((key) => {
21
+ Object.assign(body, { [key]: body[key].replace(/</g, '&lt;').replace(/>/g, '&gt;') });
22
+ });
23
+ // try { } catch (err) { return { error: err.toString() }; }
24
+
25
+ if (!stopWords.length) return { body };
26
+
27
+ const disabledCheckFields = Object.keys(schema)?.filter((el) => schema[el]?.xssCheck === false); // exclude specific columns
28
+
29
+ // check RTE
30
+ /* const richTextFields = Object.keys(schema).filter((el) => ['Summernote', 'Tiny', 'Ace'].includes(schema[el]?.type));
31
+ richTextFields.filter((key) => !checkList.find((el) => body[key].includes(el)))?.forEach((key) => {
32
+ disabledCheckFields.push(key);
33
+ }); */
34
+
35
+ const field = Object.keys(body)
36
+ ?.find((key) => body[key]
37
+ && !disabledCheckFields.includes(key)
38
+ && body[key].toLowerCase().includes(stopWords[0]));
39
+ if (field) {
40
+ return { error: `rule: ${stopWords[0]} | attr: ${field} | val: ${body[field]}`, body };
41
+ }
42
+ return { body };
43
+ }
44
+
45
+ export default checkXSS;
@@ -1,72 +1,72 @@
1
- const xssInjection = [
2
- 'onkeypress=',
3
- 'onkeyup=',
4
- 'ondblclick=',
5
- 'onerror=',
6
- 'onmouseover=',
7
- '<meta',
8
- '<script',
9
- 'vascript:',
10
- 'onkeydown=',
11
- 'onmousedown=',
12
- 'onmouseenter=',
13
- 'onmouseleave=',
14
- 'onmousemove=',
15
- 'onmouseout=',
16
- 'onmouseup=',
17
- 'onmousewheel=',
18
- 'onpaste=',
19
- 'onscroll=',
20
- 'onwheel=',
21
- 'javascript:',
22
- '\\x',
23
- 'eval(',
24
- 'onmouseover=',
25
- 'action=',
26
- 'xlink:',
27
- 'allowscriptaccess',
28
- 'href=',
29
- 'behavior:',
30
- 'onreadystatechange=',
31
- 'onstart=',
32
- 'offline=',
33
- 'onabort=',
34
- 'onafterprint=',
35
- 'onbeforeonload=',
36
- 'onbeforeprint=',
37
- 'onblur=',
38
- 'oncanplay=',
39
- 'oncanplaythrough=',
40
- 'onchange=',
41
- 'onclick=',
42
- 'oncontextmenu=',
43
- 'ondblclick=',
44
- 'ondrag=',
45
- 'ondragend=',
46
- 'ondragenter=',
47
- 'ondragleave=',
48
- 'ondragover=',
49
- 'ondragstart=',
50
- 'ondrop=',
51
- 'ondurationchange=',
52
- 'onemptied=',
53
- 'onended=',
54
- 'onerror=',
55
- 'onfocus=',
56
- 'onformchange=',
57
- 'onforminput=',
58
- 'onhaschange=',
59
- 'oninput=',
60
- 'oninvalid=',
61
- 'onkeydown=',
62
- 'onkeypress=',
63
- 'onkeyup=',
64
- 'onload=',
65
- 'onloadeddata=',
66
- 'onloadedmetadata=',
67
- 'onloadstart=',
68
- 'alert(',
69
- 'script:',
70
- ];
71
-
72
- export default xssInjection;
1
+ const xssInjection = [
2
+ 'onkeypress=',
3
+ 'onkeyup=',
4
+ 'ondblclick=',
5
+ 'onerror=',
6
+ 'onmouseover=',
7
+ '<meta',
8
+ '<script',
9
+ 'vascript:',
10
+ 'onkeydown=',
11
+ 'onmousedown=',
12
+ 'onmouseenter=',
13
+ 'onmouseleave=',
14
+ 'onmousemove=',
15
+ 'onmouseout=',
16
+ 'onmouseup=',
17
+ 'onmousewheel=',
18
+ 'onpaste=',
19
+ 'onscroll=',
20
+ 'onwheel=',
21
+ 'javascript:',
22
+ '\\x',
23
+ 'eval(',
24
+ 'onmouseover=',
25
+ 'action=',
26
+ 'xlink:',
27
+ 'allowscriptaccess',
28
+ 'href=',
29
+ 'behavior:',
30
+ 'onreadystatechange=',
31
+ 'onstart=',
32
+ 'offline=',
33
+ 'onabort=',
34
+ 'onafterprint=',
35
+ 'onbeforeonload=',
36
+ 'onbeforeprint=',
37
+ 'onblur=',
38
+ 'oncanplay=',
39
+ 'oncanplaythrough=',
40
+ 'onchange=',
41
+ 'onclick=',
42
+ 'oncontextmenu=',
43
+ 'ondblclick=',
44
+ 'ondrag=',
45
+ 'ondragend=',
46
+ 'ondragenter=',
47
+ 'ondragleave=',
48
+ 'ondragover=',
49
+ 'ondragstart=',
50
+ 'ondrop=',
51
+ 'ondurationchange=',
52
+ 'onemptied=',
53
+ 'onended=',
54
+ 'onerror=',
55
+ 'onfocus=',
56
+ 'onformchange=',
57
+ 'onforminput=',
58
+ 'onhaschange=',
59
+ 'oninput=',
60
+ 'oninvalid=',
61
+ 'onkeydown=',
62
+ 'onkeypress=',
63
+ 'onkeyup=',
64
+ 'onload=',
65
+ 'onloadeddata=',
66
+ 'onloadedmetadata=',
67
+ 'onloadstart=',
68
+ 'alert(',
69
+ 'script:',
70
+ ];
71
+
72
+ export default xssInjection;
@@ -1,15 +1,15 @@
1
- import getPG from '../../pg/funcs/getPG.js';
2
-
3
- import getMeta from '../../pg/funcs/getMeta.js';
4
-
5
- export default async function dataDelete({
6
- table, id, pg: pg1
7
- }) {
8
- const pg = pg1 || getPG({ name: 'client' });
9
- const { pk } = await getMeta(table);
10
- if (!pg.tlist?.includes(table)) return 'table not exist';
11
- const delQuery = `delete from ${table} WHERE ${pk} = $1 returning *`;
12
- // console.log(updateDataset);
13
- const res = await pg.one(delQuery, [id]) || {};
14
- return res;
15
- }
1
+ import getPG from '../../pg/funcs/getPG.js';
2
+
3
+ import getMeta from '../../pg/funcs/getMeta.js';
4
+
5
+ export default async function dataDelete({
6
+ table, id, pg: pg1
7
+ }) {
8
+ const pg = pg1 || getPG({ name: 'client' });
9
+ const { pk } = await getMeta(table);
10
+ if (!pg.tlist?.includes(table)) return 'table not exist';
11
+ const delQuery = `delete from ${table} WHERE ${pk} = $1 returning *`;
12
+ // console.log(updateDataset);
13
+ const res = await pg.one(delQuery, [id]) || {};
14
+ return res;
15
+ }