@things-factory/operato-tools 8.0.0-beta.8 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,40 @@
1
+ module.exports = {
2
+ SECRET: '0xD58F835B69D207A76CC5F84a70a1D0d4C79dAC95', // should be changed
3
+ email: {
4
+ host: 'smtp.office365.com', // your sender-email smtp host
5
+ port: 587, // smtp server port
6
+ secure: false, // true for 465, false for other ports
7
+ auth: {
8
+ user: 'your sender-email',
9
+ pass: 'your sender-email password' // generated ethereal password
10
+ },
11
+ secureConnection: false,
12
+ tls: {
13
+ ciphers: 'SSLv3'
14
+ }
15
+ },
16
+ logger: {
17
+ file: {
18
+ filename: 'logs/application-%DATE%.log',
19
+ datePattern: 'YYYY-MM-DD-HH',
20
+ zippedArchive: false,
21
+ maxSize: '20m',
22
+ maxFiles: '2d',
23
+ level: 'info'
24
+ },
25
+ console: {
26
+ level: 'silly'
27
+ }
28
+ },
29
+ ormconfig: {
30
+ name: 'default',
31
+ type: 'postgres',
32
+ host: 'postgres',
33
+ port: 5432,
34
+ database: 'postgres',
35
+ username: 'postgres',
36
+ password: 'abcd1234',
37
+ synchronize: true,
38
+ logging: true
39
+ }
40
+ }
@@ -0,0 +1,42 @@
1
+ version: '3'
2
+ services:
3
+ nginx:
4
+ image: hatiolab/operato-nginx:latest
5
+ ports:
6
+ - ${HostPort}:80
7
+ depends_on:
8
+ - app
9
+ app:
10
+ build: .
11
+ container_name: operato-tools
12
+ image: hatiolab/operato-tools:latest
13
+ privileged: true
14
+ volumes:
15
+ - ./logs:/app/logs
16
+ - ./config.production.js:/app/config.production.js
17
+ ports:
18
+ - 4000:3000
19
+ depends_on:
20
+ - postgres
21
+ - mosquitto
22
+ logging:
23
+ driver: 'json-file'
24
+ options:
25
+ max-size: '100m'
26
+ max-file: '3'
27
+ postgres:
28
+ image: postgres:13.2
29
+ container_name: db-operato-tools
30
+ environment:
31
+ POSTGRES_PASSWORD: abcd1234
32
+ POSTGRES_USER: postgres
33
+ PGDATA: /var/lib/postgresql/data/pgdata
34
+ volumes:
35
+ - ./postgres_data:/var/lib/postgresql/data/pgdata
36
+ ports:
37
+ - '55432:5432'
38
+ mosquitto:
39
+ image: eclipse-mosquitto:latest
40
+ ports:
41
+ - 1883:1883
42
+ - 9001:9001
@@ -0,0 +1,54 @@
1
+ if [ -f "config.production.js" ] ; then
2
+ echo "config.production.js exist"
3
+ else
4
+ echo "config.production.js create"
5
+ curl -O https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/config.production.js
6
+ fi
7
+
8
+ if [ -f "start.sh" ] ; then
9
+ echo "start.sh exist"
10
+ else
11
+ echo "start.sh create"
12
+ curl -O https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/start.sh
13
+ fi
14
+
15
+ if [ -f "stop.sh" ] ; then
16
+ echo "stop.sh exist"
17
+ else
18
+ echo "stop.sh create"
19
+ curl -O https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/stop.sh
20
+ fi
21
+
22
+ if [ -f "upgrade.sh" ] ; then
23
+ echo "upgrade.sh exist"
24
+ else
25
+ echo "upgrade.sh create"
26
+ curl -O https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/upgrade.sh
27
+ fi
28
+
29
+ if [ -f "migrate.sh" ] ; then
30
+ echo "migrate.sh exist"
31
+ else
32
+ echo "migrate.sh create"
33
+ curl -O https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/migrate.sh
34
+ fi
35
+
36
+ if [ -f "docker-compose.yml" ] ; then
37
+ echo "docker-compose.yml exist"
38
+ else
39
+ echo "docker-compose.yml create"
40
+ curl -O https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/docker-compose.yml
41
+ fi
42
+
43
+ chmod u+x start.sh
44
+ chmod u+x stop.sh
45
+ chmod u+x upgrade.sh
46
+ chmod u+x migrate.sh
47
+
48
+ echo "HostPort=3000" > .env
49
+
50
+ docker pull hatiolab/operato-tools:latest
51
+
52
+ docker pull hatiolab/operato-nginx:latest
53
+
54
+ docker-compose create
@@ -0,0 +1 @@
1
+ docker exec -it operato-tools npm run migration -- --mode=production
@@ -0,0 +1,18 @@
1
+ HOST_PORT=3000
2
+
3
+ if [ $# -eq 0 ] ; then
4
+ echo "Warning: default port 3000"
5
+ else
6
+ HOST_PORT=$1
7
+ fi
8
+
9
+
10
+ echo "HOST_PORT : ${HOST_PORT}"
11
+
12
+ echo "HostPort="$HOST_PORT > .env
13
+
14
+ if [[ "$OSTYPE" == "linux-gnu"* ]]; then
15
+ xhost +"local:docker@"
16
+ fi
17
+
18
+ docker-compose up -d
@@ -0,0 +1 @@
1
+ docker-compose stop
@@ -0,0 +1 @@
1
+ curl -fsSL https://raw.githubusercontent.com/things-factory/things-factory/master/packages/operato-tools/installer/install.sh | bash -s
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/operato-tools",
3
- "version": "8.0.0-beta.8",
3
+ "version": "8.0.0",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "client/index.js",
6
6
  "things-factory": true,
@@ -38,8 +38,8 @@
38
38
  "@things-factory/operato-license-checker": "^4.0.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@things-factory/builder": "^8.0.0-beta.4",
42
- "@things-factory/meta-ui": "^8.0.0-beta.8"
41
+ "@things-factory/builder": "^8.0.0",
42
+ "@things-factory/meta-ui": "^8.0.0"
43
43
  },
44
- "gitHead": "bf5206511b2d84dfb95edc3dae7f54f6cbb9bcca"
44
+ "gitHead": "07ef27d272dd9a067a9648ac7013748510556a18"
45
45
  }
@@ -0,0 +1 @@
1
+ export * from './service'
@@ -0,0 +1,15 @@
1
+ /* EXPORT ENTITY TYPES */
2
+
3
+ /* IMPORT ENTITIES AND RESOLVERS */
4
+ import { resolvers as entityResolvers } from './tool-entity'
5
+
6
+ export const entities = [
7
+ /* ENTITIES */
8
+ ]
9
+
10
+ export const schema = {
11
+ resolverClasses: [
12
+ /* RESOLVER CLASSES */
13
+ ...entityResolvers
14
+ ]
15
+ }
@@ -0,0 +1,338 @@
1
+ import { Arg, Ctx, Mutation, Directive, Resolver } from 'type-graphql'
2
+ import { Entity, EntityColumn } from '@things-factory/resource-base'
3
+ import { Menu } from '@things-factory/menu-base'
4
+ const { camelCase, startCase, snakeCase, kebabCase } = require('lodash')
5
+ const { plural } = require('pluralize')
6
+ import crypto from 'crypto'
7
+
8
+ @Resolver()
9
+ export class OperatoToolCreateMenu {
10
+ @Directive('@transaction')
11
+ @Mutation(returns => Boolean, { description: 'Operato Tool Create Menu' })
12
+ async toolCreateMenu(@Arg('id') id: string, @Arg('parent_menu_id') parent_menu_id: string, @Ctx() context: any): Promise<Boolean> {
13
+ const { domain, user, tx } = context.state
14
+
15
+ // Entity 조회
16
+ const entity: Entity = await tx.getRepository(Entity).findOne({
17
+ where: {
18
+ domain: { id: domain.id },
19
+ id
20
+ }
21
+ })
22
+
23
+ // Entity 컬럼 조회
24
+ const entityColumns: EntityColumn[] = await tx.getRepository(EntityColumn).find({
25
+ where: {
26
+ domain: { id: domain.id },
27
+ entity: { id: entity.id }
28
+ }
29
+ })
30
+
31
+ let { name = '' } = entity || {}
32
+ let serviceName: string = kebabCase(name)
33
+
34
+ // 이름 치환 (타이틀 , grapql 관련 서비스 )
35
+ let pageTemplate = modelTemplate
36
+ .replace(/{{pascalCase name}}/g, startCase(camelCase(name)).replace(/ /g, ''))
37
+ .replace(/{{camelCase name}}/g, camelCase(name))
38
+ .replace(/{{snakeCase name}}/g, snakeCase(name))
39
+ .replace(/{{pluralPascalCase name}}/g, startCase(camelCase(plural(name))).replace(/ /g, ''))
40
+ .replace(/{{pluralCamelCase name}}/g, camelCase(plural(name)))
41
+ .replace(/{{name}}/g, name)
42
+
43
+ // 이력조회 버튼 추가
44
+ let historyButton = ''
45
+ if (entity.dataProp == 'JSON') {
46
+ historyButton = `
47
+ {
48
+ "name": "history",
49
+ "type": "basic",
50
+ "label": "data_history",
51
+ "icon": "history",
52
+ "logic": "history_json"
53
+ }
54
+ `
55
+ } else if (entity.dataProp == 'COPY') {
56
+ historyButton = `
57
+ {
58
+ "name": "history",
59
+ "type": "basic",
60
+ "label": "data_history",
61
+ "icon": "history",
62
+ "logic": "history_copy"
63
+ }
64
+ `
65
+ }
66
+ pageTemplate = pageTemplate.replace(/{{HistoryButton}}/g, historyButton)
67
+
68
+ // 그리드 정렬
69
+ let sortColumns = this.createSortCols(entityColumns)
70
+ pageTemplate = pageTemplate.replace(/{{SortColumns}}/g, sortColumns.join(','))
71
+
72
+ // 검색 필드
73
+ let searchColumns = this.createSearchCols(entityColumns)
74
+ pageTemplate = pageTemplate.replace(/{{SearchColumns}}/g, searchColumns.join(','))
75
+
76
+ // 그리드 컬럼
77
+ let gridColumns = this.createGridColumns(entityColumns)
78
+ pageTemplate = pageTemplate.replace(/{{GridColumns}}/g, gridColumns.join(','))
79
+
80
+ // 암호화
81
+ pageTemplate = this.enc(pageTemplate)
82
+
83
+ // 메뉴 생성
84
+ await this.createMenuData(tx, domain, user, parent_menu_id, serviceName, pageTemplate)
85
+ return true
86
+ }
87
+
88
+ /**
89
+ * 그리드 컬럼
90
+ * @param entityColumns
91
+ */
92
+ createGridColumns(entityColumns: EntityColumn[]) {
93
+ return entityColumns
94
+ .filter(x => x.name != 'id' && x.gridRank > 0)
95
+ .sort(function (a, b) {
96
+ return a.gridRank - b.gridRank
97
+ })
98
+ .map(x => {
99
+ let { gridEditor = 'string', name, term, gridWidth = 0, gridAlign = 'left', nullable = false } = x
100
+ if (gridEditor == null) gridEditor = 'string'
101
+ if (gridAlign == null) gridAlign = 'left'
102
+
103
+ // 생성 자 수정 자 기본 값
104
+ if (name == 'updater_id' || name == 'creator_id') {
105
+ name = name == 'updater_id' ? 'updater' : 'creator'
106
+ return `{"type": "object","name": "${name}","header": "${name}","width": 100,"editable":false, "align": "center","object_opt":{"queryName":"users"}}`
107
+ }
108
+ // 생성 시간 수정 시간 기본 값
109
+ if (name == 'updated_at' || name == 'created_at') {
110
+ return `{"type": "datetime","name": "${name == 'updated_at' ? 'updatedAt' : 'createdAt'}","header": "${name}","width": 160,"editable":false, "align": "center"}`
111
+ }
112
+
113
+ // 읽기 / 숨김 필드는 수정 불가
114
+ let editable = gridEditor == 'readonly' || gridEditor == 'hidden' ? false : true
115
+ // 히든 필드
116
+ let hidden = gridEditor == 'hidden' ? true : false
117
+ if (hidden == true || editable == false) {
118
+ // 히든 필드는 문자열로 변환 숨김
119
+ gridEditor = 'string'
120
+ }
121
+
122
+ let colTxt = `{"type": "${gridEditor}","name": "${camelCase(
123
+ name
124
+ )}","header": "${term}","hidden":${hidden} ,"editable": ${editable},"mandatory": ${!nullable},"sortable":true ,"align": "${gridAlign}","width":${gridWidth} ,"exportable": true`
125
+
126
+ // 참조 타입이 공통 코드
127
+ if (x.refType == 'code') {
128
+ colTxt = colTxt + `,"select_opt":{"type":"code","name":"${x.refName}"}`
129
+ }
130
+
131
+ // 참조 타입이 시나리오
132
+ if (x.refType == 'scenario') {
133
+ colTxt = colTxt + `,"select_opt":{"type":"scenario","name":"${x.refName}"}`
134
+ }
135
+
136
+ // 참조 타입이 엔티티
137
+ if (x.refType == 'entity') {
138
+ colTxt = colTxt + `,"select_opt":{"type": "entity","args": {"queryName": "${camelCase(plural(x.refName))}","codeField": "id","dispField": "name"}}`
139
+ }
140
+ colTxt = colTxt + '}'
141
+ return colTxt
142
+ })
143
+ }
144
+
145
+ /**
146
+ * 검색 컬럼
147
+ * @param entityColumns
148
+ */
149
+ createSearchCols(entityColumns: EntityColumn[]) {
150
+ return entityColumns
151
+ .filter(x => x.searchRank > 0)
152
+ .sort(function (a, b) {
153
+ return a.searchRank - b.searchRank
154
+ })
155
+ .map(x => {
156
+ let operator = x.searchOper
157
+
158
+ if (operator == 'filter') {
159
+ return `"${camelCase(x.name)}"`
160
+ }
161
+
162
+ if (!operator) operator = 'eq'
163
+ return `{"name": "${camelCase(x.name)}","operator": "${operator}"}`
164
+ })
165
+ }
166
+
167
+ /**
168
+ * 그리드 정렬
169
+ * @param entityColumns
170
+ * @returns
171
+ */
172
+ createSortCols(entityColumns: EntityColumn[]) {
173
+ return entityColumns
174
+ .filter(x => x.sortRank > 0)
175
+ .sort(function (a, b) {
176
+ return a.sortRank - b.sortRank
177
+ })
178
+ .map(x => {
179
+ return `{"name":"${camelCase(x.name)}","desc":${x.reverseSort}}`
180
+ })
181
+ }
182
+
183
+ /**
184
+ * 메뉴 데이터 생성
185
+ * @param tx
186
+ * @param domain
187
+ * @param user
188
+ * @param parent_menu_id
189
+ * @param name
190
+ * @param pageTemplate
191
+ */
192
+ async createMenuData(tx: any, domain: any, user: any, parent_menu_id: string, name: string, pageTemplate: string) {
193
+ // 상위 메뉴
194
+ let parentMenu = await tx.getRepository(Menu).findOne({
195
+ where: {
196
+ domain: { id: domain.id },
197
+ id: parent_menu_id
198
+ }
199
+ })
200
+
201
+ // 메뉴 랭크
202
+ let menuRank = await tx.getRepository(Menu).query(`select coalesce(max(rank),0) as rank from menus where parent_id = '${parent_menu_id}' or id = '${parent_menu_id}'`)
203
+ let rank = menuRank[0].rank + 10
204
+
205
+ // 메뉴 저장
206
+ let menu: Menu = new Menu()
207
+ menu.name = name
208
+ menu.menuType = 'SCREEN'
209
+ menu.category = 'meta-grist-page'
210
+ menu.rank = rank
211
+ menu.hiddenFlag = false
212
+ menu.routing = name
213
+ menu.template = pageTemplate
214
+ menu.resourceUrl = '@things-factory/meta-ui/client/pages/meta-grist-page'
215
+ menu.parent = parentMenu
216
+
217
+ await tx.getRepository(Menu).save({
218
+ ...menu,
219
+ domain,
220
+ creator: user,
221
+ updater: user
222
+ })
223
+ }
224
+
225
+ /**
226
+ * json 모델 암호화
227
+ * @param template
228
+ * @returns
229
+ */
230
+ enc(template: string) {
231
+ let jsonStr = JSON.stringify(template)
232
+ let templates = jsonStr.split(/(.{100})/).filter(O => O)
233
+ let filters = ['aes-256-ecb', 'aes-256-cbc', 'aes-256-cfb', 'aes-256-cfb8', 'aes-256-cfb1', 'aes-256-ofb', 'aes-256-ctr', 'aes256']
234
+ let salt = crypto.randomBytes(32).toString('hex')
235
+ let hexKey = Buffer.from(salt, 'hex')
236
+ let ivBase = salt.substring(0, 16)
237
+ let iv = ['', ivBase, ivBase, ivBase, ivBase, ivBase, ivBase, ivBase]
238
+ let filterIdx = 0
239
+ let encResults = []
240
+ encResults.push(salt)
241
+
242
+ for (var i = 0; i < templates.length; i++, filterIdx++) {
243
+ if (filterIdx == filters.length) filterIdx = 0
244
+
245
+ let cipher = crypto.createCipheriv(filters[filterIdx], hexKey, iv[filterIdx])
246
+ let encrypted = cipher.update(templates[i], 'utf8', 'hex')
247
+ encrypted += cipher.final('hex')
248
+ encResults.push(encrypted)
249
+ }
250
+
251
+ return encResults.join('h1z0q9i9x7q6')
252
+ }
253
+ }
254
+
255
+ const modelTemplate = `
256
+ {
257
+ "menu": {
258
+ "title": "{{name}}",
259
+ "name": "",
260
+ "desc": ""
261
+ },
262
+ "gql": {
263
+ "query": {
264
+ "list_func": "{{pluralCamelCase name}}",
265
+ "find_one_func":"{{camelCase name}}"
266
+ },
267
+ "mutation":{
268
+ "multiple":{
269
+ "func":"updateMultiple{{pascalCase name}}",
270
+ "type": "{{pascalCase name}}Patch"
271
+ },
272
+ "delete":{
273
+ "func":"delete{{pluralPascalCase name}}"
274
+ }
275
+ }
276
+ },
277
+ "button": [
278
+ {
279
+ "name":"export"
280
+ },
281
+ {
282
+ "name":"add"
283
+ },
284
+ {
285
+ "name":"delete"
286
+ },
287
+ {
288
+ "name":"save"
289
+ }
290
+ ],
291
+ "grid_column": [
292
+ {
293
+ "type": "string",
294
+ "name": "id",
295
+ "header": "id",
296
+ "hidden": true,
297
+ "editable": false,
298
+ "mandatory": false,
299
+ "sortable": false,
300
+ "align": "left",
301
+ "width": 0,
302
+ "exportable": false
303
+ },
304
+ {{GridColumns}}
305
+ ],
306
+ "grid": {
307
+ "button":[
308
+ {{HistoryButton}}
309
+ ],
310
+ "option": {
311
+ "mobile_mode": "LIST",
312
+ "desk_mode": "GRID",
313
+ "use_row_checker": true,
314
+ "pages": [
315
+ 20,
316
+ 50,
317
+ 100,
318
+ 300
319
+ ],
320
+ "view_mode": [
321
+ "GRID",
322
+ "LIST",
323
+ "CARD"
324
+ ],
325
+ "sorters": [
326
+ {{SortColumns}}
327
+ ]
328
+ },
329
+ "row": {
330
+ "multiple_select": true,
331
+ "click": "select-row-toggle"
332
+ }
333
+ },
334
+ "search": [
335
+ {{SearchColumns}}
336
+ ]
337
+ }
338
+ `