@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.
- package/dist-server/engine/connector/http-connector.js +1 -1
- package/dist-server/engine/connector/http-connector.js.map +1 -1
- package/dist-server/engine/task/data-mapper.js +37 -0
- package/dist-server/engine/task/data-mapper.js.map +1 -0
- package/dist-server/engine/task/headless-post.js +123 -0
- package/dist-server/engine/task/headless-post.js.map +1 -0
- package/dist-server/engine/task/http-post.js +3 -6
- package/dist-server/engine/task/http-post.js.map +1 -1
- package/dist-server/engine/task/index.js +2 -0
- package/dist-server/engine/task/index.js.map +1 -1
- package/dist-server/engine/task/map-data.js +37 -0
- package/dist-server/engine/task/map-data.js.map +1 -0
- package/dist-server/engine/task/utils/headless-pool-for-scenario.js +65 -0
- package/dist-server/engine/task/utils/headless-pool-for-scenario.js.map +1 -0
- package/dist-server/engine/task/utils/headless-pool-for-task.js +65 -0
- package/dist-server/engine/task/utils/headless-pool-for-task.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/helps/integration/task/data-mapper.md +14 -0
- package/package.json +2 -2
- package/server/engine/connector/http-connector.ts +1 -1
- package/server/engine/task/data-mapper.ts +44 -0
- package/server/engine/task/headless-post.ts +141 -0
- package/server/engine/task/http-post.ts +4 -8
- package/server/engine/task/index.ts +2 -0
- package/server/engine/task/utils/headless-pool-for-scenario.ts +71 -0
- package/translations/en.json +1 -1
- package/translations/ko.json +1 -1
- package/translations/ms.json +1 -1
- 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.
|
|
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": "
|
|
48
|
+
"gitHead": "d0198bba193b275f29fffd8c3963f556db4dda92"
|
|
49
49
|
}
|
|
@@ -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:
|
|
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
|
|
|
@@ -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
|
+
}
|
package/translations/en.json
CHANGED
package/translations/ko.json
CHANGED
package/translations/ms.json
CHANGED
package/translations/zh.json
CHANGED