@things-factory/integration-base 6.0.67 → 6.0.69

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 (29) hide show
  1. package/dist-server/engine/connector/http-connector.js +1 -1
  2. package/dist-server/engine/connector/http-connector.js.map +1 -1
  3. package/dist-server/engine/task/data-mapper.js +37 -0
  4. package/dist-server/engine/task/data-mapper.js.map +1 -0
  5. package/dist-server/engine/task/headless-post.js +123 -0
  6. package/dist-server/engine/task/headless-post.js.map +1 -0
  7. package/dist-server/engine/task/http-post.js +3 -6
  8. package/dist-server/engine/task/http-post.js.map +1 -1
  9. package/dist-server/engine/task/index.js +2 -0
  10. package/dist-server/engine/task/index.js.map +1 -1
  11. package/dist-server/engine/task/map-data.js +37 -0
  12. package/dist-server/engine/task/map-data.js.map +1 -0
  13. package/dist-server/engine/task/utils/headless-pool-for-scenario.js +65 -0
  14. package/dist-server/engine/task/utils/headless-pool-for-scenario.js.map +1 -0
  15. package/dist-server/engine/task/utils/headless-pool-for-task.js +65 -0
  16. package/dist-server/engine/task/utils/headless-pool-for-task.js.map +1 -0
  17. package/dist-server/tsconfig.tsbuildinfo +1 -1
  18. package/helps/integration/task/data-mapper.md +14 -0
  19. package/package.json +2 -2
  20. package/server/engine/connector/http-connector.ts +1 -1
  21. package/server/engine/task/data-mapper.ts +44 -0
  22. package/server/engine/task/headless-post.ts +141 -0
  23. package/server/engine/task/http-post.ts +4 -8
  24. package/server/engine/task/index.ts +2 -0
  25. package/server/engine/task/utils/headless-pool-for-scenario.ts +71 -0
  26. package/translations/en.json +1 -1
  27. package/translations/ko.json +1 -1
  28. package/translations/ms.json +1 -1
  29. package/translations/zh.json +1 -1
@@ -0,0 +1,14 @@
1
+ # data-mapper Task
2
+
3
+ 특정 accessor의 결과 값을 새로운 데이터로 변환하는 기능을 제공한다.
4
+ 기존 데이터가 단일 오브젝트이면 새로운 데이터도 단일 오브젝트 형태로 변환되며,
5
+ 기존 데이터가 오브젝트의 배열 형태이면 새로운 데이터도 오브젝트의 배열 형태로 반환된다.
6
+
7
+ ## parameters
8
+
9
+ - [accessor](../concept/data-accessor.md)
10
+ - 변환하고자 하는 대상 데이터를 설정한다.
11
+ - 매핑 규칙(mapping-rule)
12
+ - 기존 데이터를 기반으로 새로운 데이터를 만드는 규칙을 설정한다.
13
+ - key 부분에 설정된 값들은 새로 만들어질 데이터의 key들이 된다.
14
+ - value 부분에는 기존 데이터의 key를 입력한다. 기존 데이터의 key에 해당되는 값이 새로운 데이터의 값이 된다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/integration-base",
3
- "version": "6.0.67",
3
+ "version": "6.0.69",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "client/index.js",
6
6
  "things-factory": true,
@@ -45,5 +45,5 @@
45
45
  "devDependencies": {
46
46
  "@types/cron": "^2.0.1"
47
47
  },
48
- "gitHead": "69de24faac65e08ba200e67402b19ed564125a87"
48
+ "gitHead": "d0198bba193b275f29fffd8c3963f556db4dda92"
49
49
  }
@@ -57,7 +57,7 @@ export class HttpConnector implements Connector {
57
57
  }
58
58
 
59
59
  get taskPrefixes() {
60
- return ['http']
60
+ return ['http', 'headless']
61
61
  }
62
62
  }
63
63
 
@@ -0,0 +1,44 @@
1
+ import { access } from '@things-factory/utils'
2
+ import { TaskRegistry } from '../task-registry'
3
+
4
+ function mapping(rule, input) {
5
+ if (Array.isArray(input)) {
6
+ return input.map(i => mapping(rule, i))
7
+ }
8
+
9
+ return Object.keys(rule).reduce((sum, key) => {
10
+ sum[key] = input[rule[key]]
11
+ return sum
12
+ }, {} as any)
13
+ }
14
+
15
+ async function DataMapper(step, { logger, data }) {
16
+ var {
17
+ params: { accessor, mappingRule }
18
+ } = step
19
+
20
+ const input = access(accessor, data)
21
+ const output = mapping(mappingRule || {}, input)
22
+
23
+ return {
24
+ data: output
25
+ }
26
+ }
27
+
28
+ DataMapper.parameterSpec = [
29
+ {
30
+ type: 'scenario-step-input',
31
+ name: 'accessor',
32
+ label: 'accessor'
33
+ },
34
+ {
35
+ type: 'key-values',
36
+ name: 'mappingRule',
37
+ label: 'mapping-rule'
38
+ }
39
+ ]
40
+
41
+ DataMapper.connectorFree = true
42
+ DataMapper.help = 'integration/task/data-mapper'
43
+
44
+ TaskRegistry.registerTaskHandler('data-mapper', DataMapper)
@@ -0,0 +1,141 @@
1
+ import https from 'https'
2
+ import { URL } from 'url'
3
+
4
+ import { access } from '@things-factory/utils'
5
+ import { TaskRegistry } from '../task-registry'
6
+ import { ConnectionManager } from '../connection-manager'
7
+
8
+ import { getHeadlessPool } from './utils/headless-pool-for-scenario'
9
+
10
+ async function HeadlessPost(step, { logger, data, domain }) {
11
+ var { connection: connectionName, params: stepOptions } = step
12
+ var { headers: requestHeaders, contentType, path, accessor } = stepOptions || {}
13
+
14
+ var connection = ConnectionManager.getConnectionInstanceByName(domain, connectionName)
15
+
16
+ if (!connection) {
17
+ throw new Error(`connection '${connectionName}' is not established.`)
18
+ }
19
+
20
+ var { endpoint, params: connectionParams, authHeaders = {} } = connection
21
+
22
+ var headers = {
23
+ ...authHeaders,
24
+ ...requestHeaders
25
+ }
26
+
27
+ var body = access(accessor, data)
28
+ if (contentType && body) {
29
+ headers['content-type'] = contentType
30
+ switch (contentType) {
31
+ case 'text/plain':
32
+ body = JSON.stringify(body)
33
+ break
34
+ case 'application/json':
35
+ body = JSON.stringify(body)
36
+ break
37
+ case 'application/x-www-form-urlencoded':
38
+ const searchParams = new URLSearchParams()
39
+ for (const prop in body) {
40
+ searchParams.set(prop, body[prop])
41
+ }
42
+ body = searchParams
43
+ break
44
+ }
45
+ }
46
+
47
+ var options: any = {
48
+ method: 'POST',
49
+ headers,
50
+ body
51
+ }
52
+
53
+ var { rejectUnauthorized } = connectionParams
54
+
55
+ if (!rejectUnauthorized) {
56
+ const httpsAgent = new https.Agent({
57
+ rejectUnauthorized
58
+ })
59
+ options.agent = httpsAgent
60
+ }
61
+
62
+ const browser = (await getHeadlessPool().acquire()) as any
63
+ const page = await browser.newPage()
64
+
65
+ try {
66
+ page.on('console', msg => {
67
+ console.log(`[browser ${msg.type()}] ${msg.text()}`)
68
+ for (let i = 0; i < msg.args().length; ++i) console.log(`${i}: ${msg.args()[i]}`)
69
+ })
70
+
71
+ await page.goto(endpoint, { waitUntil: 'networkidle2' })
72
+
73
+ const response = await page.evaluate(
74
+ async (urlString, options) => {
75
+ const response = await fetch(urlString, options)
76
+
77
+ if (response.ok && response.headers.get('content-type').includes('application/json')) {
78
+ return await response.json()
79
+ } else {
80
+ return await response.text()
81
+ }
82
+ },
83
+ new URL(path, endpoint),
84
+ options
85
+ )
86
+
87
+ return {
88
+ data: response
89
+ }
90
+ } catch (e) {
91
+ console.error(e)
92
+ } finally {
93
+ page.close()
94
+ getHeadlessPool().release(browser)
95
+ }
96
+ }
97
+
98
+ HeadlessPost.parameterSpec = [
99
+ {
100
+ type: 'string',
101
+ name: 'path',
102
+ label: 'path'
103
+ },
104
+ {
105
+ type: 'http-headers',
106
+ name: 'headers',
107
+ label: 'headers'
108
+ },
109
+ {
110
+ type: 'select',
111
+ name: 'contentType',
112
+ label: 'content-type',
113
+ property: {
114
+ options: [
115
+ {
116
+ display: '',
117
+ value: ''
118
+ },
119
+ {
120
+ display: 'application/json',
121
+ value: 'application/json'
122
+ },
123
+ {
124
+ display: 'text/plain',
125
+ value: 'text/plain'
126
+ },
127
+ {
128
+ display: 'application/x-www-form-urlencoded',
129
+ value: 'application/x-www-form-urlencoded'
130
+ }
131
+ ]
132
+ }
133
+ },
134
+ {
135
+ type: 'scenario-step-input',
136
+ name: 'accessor',
137
+ label: 'accessor'
138
+ }
139
+ ]
140
+
141
+ TaskRegistry.registerTaskHandler('headless-post', HeadlessPost)
@@ -61,15 +61,11 @@ async function HttpPost(step, { logger, data, domain }) {
61
61
 
62
62
  var response = await fetch(url, fetchOptions)
63
63
 
64
- var responseData = await response.text()
65
-
66
- const responseContentType = response.headers.get('content-type')
67
- if (responseContentType && responseContentType.indexOf('application/json') !== -1) {
68
- responseData = JSON.stringify(responseData)
69
- }
70
-
71
64
  return {
72
- data: responseData
65
+ data:
66
+ response.ok && response.headers.get('content-type').includes('application/json')
67
+ ? await response.json()
68
+ : await response.text()
73
69
  }
74
70
  }
75
71
 
@@ -32,3 +32,5 @@ import './floating-point'
32
32
  import './socket-listener'
33
33
  import './random'
34
34
  import './csv-readline'
35
+ import './data-mapper'
36
+ import './headless-post'
@@ -0,0 +1,71 @@
1
+ import * as genericPool from 'generic-pool'
2
+
3
+ import { config, logger } from '@things-factory/env'
4
+
5
+ try {
6
+ var puppeteer = require('puppeteer')
7
+ } catch (err) {
8
+ logger.error(err)
9
+ }
10
+
11
+ var headlessPool
12
+
13
+ export function getHeadlessPool() {
14
+ if (!headlessPool) {
15
+ headlessPool = genericPool.createPool(
16
+ {
17
+ create() {
18
+ console.log('headless-pool-for-scensrio about to create')
19
+ return initializeChromium()
20
+ },
21
+ validate(browser) {
22
+ return Promise.race([
23
+ new Promise(res => setTimeout(() => res(false), 1500)),
24
+ browser
25
+ //@ts-ignore
26
+ .version()
27
+ .then(_ => true)
28
+ .catch(_ => false)
29
+ ])
30
+ },
31
+ destroy(browser) {
32
+ //@ts-ignore
33
+ return browser.close()
34
+ }
35
+ },
36
+ {
37
+ min: 2,
38
+ max: 10,
39
+ testOnBorrow: true,
40
+ acquireTimeoutMillis: 15000
41
+ }
42
+ )
43
+ }
44
+
45
+ return headlessPool
46
+ }
47
+
48
+ const CHROMIUM_PATH = config.get('CHROMIUM_PATH')
49
+
50
+ async function initializeChromium() {
51
+ try {
52
+ if (!puppeteer) {
53
+ return
54
+ }
55
+
56
+ var launchSetting = {
57
+ args: ['--mute-audio', '--no-sandbox'],
58
+ headless: 'new'
59
+ }
60
+
61
+ if (CHROMIUM_PATH) {
62
+ launchSetting['executablePath'] = CHROMIUM_PATH
63
+ }
64
+
65
+ const browser = await puppeteer.launch(launchSetting)
66
+
67
+ return browser
68
+ } catch (err) {
69
+ logger.error(err)
70
+ }
71
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "error.scenario not found": "scenario '{scenario}' not found.",
3
3
  "error.scenario instance not found": "scenario instance '{instance}' not found."
4
- }
4
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "error.scenario not found": "시나리오 '{scenario}'를 찾을 수 없습니다.",
3
3
  "error.scenario instance not found": "시나리오 인스턴스 '{instance}'를 찾을 수 없습니다. 이미 종료되었을 수 있습니다."
4
- }
4
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "error.scenario not found": "[ms] scenario '{scenario}' not found.",
3
3
  "error.scenario instance not found": "[ms] scenario instance '{instance}' not found."
4
- }
4
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "error.scenario not found": "未找到场景'{scenario}'",
3
3
  "error.scenario instance not found": "[zh] scenario instance '{instance}' not found."
4
- }
4
+ }