@things-factory/integration-base 6.2.38 → 6.2.40
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/operato-connector.js +151 -17
- package/dist-server/engine/connector/operato-connector.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/helps/integration/connector/operato-connector.ja.md +11 -0
- package/helps/integration/connector/operato-connector.kr.md +11 -1
- package/helps/integration/connector/operato-connector.md +10 -0
- package/helps/integration/connector/operato-connector.ms.md +10 -0
- package/helps/integration/connector/operato-connector.zh.md +10 -0
- package/package.json +2 -2
- package/server/engine/connector/operato-connector.ts +189 -23
- package/translations/en.json +2 -1
- package/translations/ja.json +2 -1
- package/translations/ko.json +2 -1
- package/translations/ms.json +2 -1
- package/translations/zh.json +2 -1
@@ -25,4 +25,14 @@ ex. *https://abcd.hatioalb.com/graphql*
|
|
25
25
|
|
26
26
|
### 도메인
|
27
27
|
|
28
|
-
- 대상 서버에서 동작하고자 하는 도메인을 지정해야 합니다.
|
28
|
+
- 대상 서버에서 동작하고자 하는 도메인을 지정해야 합니다.
|
29
|
+
|
30
|
+
|
31
|
+
### 태그
|
32
|
+
|
33
|
+
- 대상 서버에서 pubsub 기반의 데이터를 구독할 때 사용되는 태그 이름을 지정합니다.
|
34
|
+
|
35
|
+
|
36
|
+
### 시나리오
|
37
|
+
|
38
|
+
- 태그가 설정되어 있을 경우, 구독된 메시지를 처리하기 위한 시나리오를 지정합니다.
|
@@ -24,3 +24,13 @@ ex. *https://abcd.hatioalb.com/graphql*
|
|
24
24
|
### Domain
|
25
25
|
|
26
26
|
- Specify the domain you want to operate on the target server.
|
27
|
+
|
28
|
+
|
29
|
+
### tag
|
30
|
+
|
31
|
+
- Specifies the tag names used when subscribing to pubsub-based data on the target server.
|
32
|
+
|
33
|
+
|
34
|
+
### Scenario
|
35
|
+
|
36
|
+
- Specifies the scenario for processing subscribed messages if the tag is set.
|
@@ -24,3 +24,13 @@ contoh: *https://abcd.hatioalb.com/graphql*
|
|
24
24
|
### Domain
|
25
25
|
|
26
26
|
- Anda perlu menentukan domain yang ingin beroperasi dari server sasaran.
|
27
|
+
|
28
|
+
|
29
|
+
### Tag
|
30
|
+
|
31
|
+
- Menentukan nama tag yang digunakan saat berlangganan data berbasis pubsub pada server target.
|
32
|
+
|
33
|
+
|
34
|
+
### Skenario
|
35
|
+
|
36
|
+
- Menentukan skenario untuk memproses pesan yang telah di-subscribe jika tag sudah ditetapkan.
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@things-factory/integration-base",
|
3
|
-
"version": "6.2.
|
3
|
+
"version": "6.2.40",
|
4
4
|
"main": "dist-server/index.js",
|
5
5
|
"browser": "client/index.js",
|
6
6
|
"things-factory": true,
|
@@ -46,5 +46,5 @@
|
|
46
46
|
"devDependencies": {
|
47
47
|
"@types/cron": "^2.0.1"
|
48
48
|
},
|
49
|
-
"gitHead": "
|
49
|
+
"gitHead": "fd7decc71972d25ef12ad0f44ab19eb66c6d289b"
|
50
50
|
}
|
@@ -1,10 +1,23 @@
|
|
1
1
|
import 'cross-fetch/polyfill'
|
2
2
|
|
3
|
-
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client/core'
|
3
|
+
import { ApolloClient, InMemoryCache, createHttpLink, split } from '@apollo/client/core'
|
4
4
|
import { setContext } from '@apollo/client/link/context'
|
5
5
|
|
6
|
+
// for subscription
|
7
|
+
import WebSocket from 'ws'
|
8
|
+
import { createClient } from 'graphql-ws'
|
9
|
+
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
10
|
+
import { Observable, getMainDefinition } from '@apollo/client/utilities'
|
11
|
+
import gql from 'graphql-tag'
|
12
|
+
|
6
13
|
import { ConnectionManager } from '../connection-manager'
|
7
14
|
import { Connector } from '../types'
|
15
|
+
import { Scenario, ScenarioInstance } from '../../service'
|
16
|
+
|
17
|
+
import { getRepository, GraphqlLocalClient, Domain } from '@things-factory/shell'
|
18
|
+
import { PrivilegeObject, User } from '@things-factory/auth-base'
|
19
|
+
|
20
|
+
const debug = require('debug')('things-factory:integration-base:operato-connector-subscription')
|
8
21
|
|
9
22
|
const defaultOptions: any = {
|
10
23
|
watchQuery: {
|
@@ -24,7 +37,34 @@ const cache = new InMemoryCache({
|
|
24
37
|
addTypename: false
|
25
38
|
})
|
26
39
|
|
40
|
+
export const GRAPHQL_URI = '/graphql'
|
41
|
+
export const SUBSCRIPTION_URI = GRAPHQL_URI
|
42
|
+
|
43
|
+
async function checkPermission(privilegeObject: PrivilegeObject, user: User, domain: Domain): Promise<boolean> {
|
44
|
+
if (!privilegeObject || !privilegeObject.privilege || !privilegeObject.category) {
|
45
|
+
return true
|
46
|
+
}
|
47
|
+
|
48
|
+
const { owner: domainOwnerGranted, super: superUserGranted, category, privilege } = privilegeObject
|
49
|
+
|
50
|
+
return (
|
51
|
+
(domainOwnerGranted && (await process.domainOwnerGranted(domain, user))) ||
|
52
|
+
(superUserGranted && (await process.superUserGranted(domain, user))) ||
|
53
|
+
(category && privilege && (await User.hasPrivilege(privilege, category, domain, user)))
|
54
|
+
)
|
55
|
+
}
|
56
|
+
|
57
|
+
interface SubscriberData {
|
58
|
+
tag: string
|
59
|
+
scenario: any
|
60
|
+
subscriptionObserver: any
|
61
|
+
}
|
62
|
+
;``
|
63
|
+
|
27
64
|
export class OperatoConnector implements Connector {
|
65
|
+
private subscriptions: SubscriberData[] = []
|
66
|
+
private context: any
|
67
|
+
|
28
68
|
async ready(connectionConfigs) {
|
29
69
|
await Promise.all(connectionConfigs.map(this.connect.bind(this)))
|
30
70
|
|
@@ -34,41 +74,128 @@ export class OperatoConnector implements Connector {
|
|
34
74
|
async connect(connection) {
|
35
75
|
var {
|
36
76
|
endpoint: uri,
|
37
|
-
params: { authKey, domain }
|
77
|
+
params: { authKey, domain, subscriptionHandlers = {} }
|
38
78
|
} = connection
|
39
79
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
ConnectionManager.logger.error(`[GraphQL error] Message: ${message}, Location: ${locations}, Path: ${path}`)
|
44
|
-
})
|
80
|
+
if (!authKey || !domain) {
|
81
|
+
throw new Error('some connection paramter missing.')
|
82
|
+
}
|
45
83
|
|
46
|
-
|
47
|
-
|
84
|
+
var domainOwner = await getRepository(User).findOne({
|
85
|
+
where: {
|
86
|
+
id: connection.domain.owner
|
48
87
|
}
|
88
|
+
})
|
89
|
+
|
90
|
+
this.context = {
|
91
|
+
domain: connection.domain,
|
92
|
+
user: domainOwner
|
93
|
+
/* TODO: domainOwner 대신 특정 유저를 지정할 수 있도록 개선해야함. 모든 커넥션에 유저를 지정하는 기능으로 일반화할 필요가 있는 지 고민해야함 */
|
49
94
|
}
|
50
95
|
|
51
96
|
const httpLink = createHttpLink({
|
52
97
|
uri: uri
|
53
98
|
})
|
54
99
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
100
|
+
/*
|
101
|
+
CHECKPOINT:
|
102
|
+
1. GraphqQLWsLink를 사용하면 setContext를 통한 추가 헤더 설정이 무시됩니다.
|
103
|
+
따라서, GraphQLWsLink를 사용하려면, connectionParams를 통해 헤더를 설정해야 합니다.
|
104
|
+
|
105
|
+
2. 서버에서 실행시, webSocketImpl을 명시적으로 지정해야 합니다.
|
106
|
+
*/
|
107
|
+
const wsLink = new GraphQLWsLink(
|
108
|
+
createClient({
|
109
|
+
url: uri.replace(/^http/, 'ws'),
|
110
|
+
keepAlive: 10_000,
|
111
|
+
retryAttempts: 1_000_000,
|
112
|
+
shouldRetry: e => true,
|
113
|
+
webSocketImpl: WebSocket,
|
114
|
+
connectionParams: {
|
115
|
+
headers: {
|
116
|
+
'x-things-factory-domain': domain,
|
117
|
+
authorization: authKey ? `Bearer ${authKey}` : ''
|
67
118
|
}
|
68
|
-
}
|
119
|
+
}
|
69
120
|
})
|
70
121
|
)
|
71
122
|
|
123
|
+
const splitLink = split(
|
124
|
+
({ query }) => {
|
125
|
+
const def = getMainDefinition(query)
|
126
|
+
return def.kind === 'OperationDefinition' && def.operation === 'subscription'
|
127
|
+
},
|
128
|
+
wsLink,
|
129
|
+
setContext((_, { headers }) => {
|
130
|
+
return {
|
131
|
+
headers: {
|
132
|
+
...headers,
|
133
|
+
'x-things-factory-domain': domain,
|
134
|
+
authorization: authKey ? `Bearer ${authKey}` : ''
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}).concat(httpLink)
|
138
|
+
)
|
139
|
+
|
140
|
+
var client = new ApolloClient({
|
141
|
+
defaultOptions,
|
142
|
+
cache,
|
143
|
+
link: splitLink
|
144
|
+
})
|
145
|
+
|
146
|
+
var subscriptions: SubscriberData[] = []
|
147
|
+
Object.keys(subscriptionHandlers).forEach(async tag => {
|
148
|
+
if (!tag || !subscriptionHandlers[tag]) return
|
149
|
+
|
150
|
+
let scenarioName = subscriptionHandlers[tag]
|
151
|
+
|
152
|
+
// fetch a scenario
|
153
|
+
var selectedScenario = await getRepository(Scenario).findOne({
|
154
|
+
where: {
|
155
|
+
name: scenarioName
|
156
|
+
},
|
157
|
+
relations: ['steps', 'domain']
|
158
|
+
})
|
159
|
+
|
160
|
+
const subscription = client.subscribe({
|
161
|
+
query: gql`
|
162
|
+
subscription {
|
163
|
+
data(tag: "${tag}") {
|
164
|
+
tag
|
165
|
+
data
|
166
|
+
}
|
167
|
+
}
|
168
|
+
`
|
169
|
+
})
|
170
|
+
|
171
|
+
var subscriptionObserver = subscription.subscribe({
|
172
|
+
next: async data => {
|
173
|
+
debug('received pubsub msg.:', data?.data)
|
174
|
+
await this.runScenario(subscriptions, data?.data?.data)
|
175
|
+
},
|
176
|
+
error: error => {
|
177
|
+
ConnectionManager.logger.error(`(${connection.name}:${connection.endpoint}) subscription error: ${error}`)
|
178
|
+
},
|
179
|
+
complete: () => {
|
180
|
+
ConnectionManager.logger.info(`(${connection.name}:${connection.endpoint}) subscription complete`)
|
181
|
+
}
|
182
|
+
})
|
183
|
+
|
184
|
+
ConnectionManager.logger.info(
|
185
|
+
`(${connection.name}:${connection.endpoint}) subscription closed flag: ${subscriptionObserver.closed}`
|
186
|
+
)
|
187
|
+
|
188
|
+
subscriptions.push({
|
189
|
+
tag,
|
190
|
+
scenario: selectedScenario,
|
191
|
+
subscriptionObserver
|
192
|
+
})
|
193
|
+
ConnectionManager.logger.info(`(${tag}:${scenarioName}) subscription closed flag: ${subscriptionObserver.closed}`)
|
194
|
+
})
|
195
|
+
|
196
|
+
client['subscriptions'] = subscriptions
|
197
|
+
ConnectionManager.addConnectionInstance(connection, client)
|
198
|
+
|
72
199
|
ConnectionManager.logger.info(
|
73
200
|
`graphql-connector connection(${connection.name}:${connection.endpoint}) is connected`
|
74
201
|
)
|
@@ -76,12 +203,46 @@ export class OperatoConnector implements Connector {
|
|
76
203
|
|
77
204
|
async disconnect(connection) {
|
78
205
|
var client = ConnectionManager.getConnectionInstance(connection)
|
206
|
+
let subscriptions: SubscriberData[] = client['subscriptions']
|
207
|
+
subscriptions.forEach(subscription => subscription.subscriptionObserver.unsubscribe())
|
79
208
|
client.stop()
|
80
209
|
ConnectionManager.removeConnectionInstance(connection)
|
81
210
|
|
82
211
|
ConnectionManager.logger.info(`graphql-connector connection(${connection.name}) is disconnected`)
|
83
212
|
}
|
84
213
|
|
214
|
+
async runScenario(subscriptions: SubscriberData[], variables: any): Promise<ScenarioInstance> {
|
215
|
+
const { domain, user } = this.context
|
216
|
+
const { tag } = variables
|
217
|
+
|
218
|
+
if (!tag) {
|
219
|
+
throw new Error(`tag is invalid - ${tag}`)
|
220
|
+
}
|
221
|
+
|
222
|
+
var scenario = subscriptions.find(subscription => subscription.tag === tag)?.scenario
|
223
|
+
if (!scenario) {
|
224
|
+
throw new Error(`scenario is not found - ${tag}`)
|
225
|
+
}
|
226
|
+
|
227
|
+
if (!(await checkPermission(scenario.privilege, user, domain))) {
|
228
|
+
const { category, privilege } = scenario.privilege || {}
|
229
|
+
throw new Error(`Unauthorized! ${category}-${privilege} privilege required`)
|
230
|
+
}
|
231
|
+
|
232
|
+
/* create a scenario instance */
|
233
|
+
let instanceName = scenario.name + '-' + String(Date.now())
|
234
|
+
var instance = new ScenarioInstance(instanceName, scenario, {
|
235
|
+
user,
|
236
|
+
domain,
|
237
|
+
variables,
|
238
|
+
client: GraphqlLocalClient.client
|
239
|
+
})
|
240
|
+
|
241
|
+
// run scenario
|
242
|
+
await instance.run()
|
243
|
+
return instance
|
244
|
+
}
|
245
|
+
|
85
246
|
get parameterSpec() {
|
86
247
|
return [
|
87
248
|
{
|
@@ -93,6 +254,11 @@ export class OperatoConnector implements Connector {
|
|
93
254
|
type: 'string',
|
94
255
|
name: 'domain',
|
95
256
|
label: 'domain'
|
257
|
+
},
|
258
|
+
{
|
259
|
+
type: 'tag-scenarios',
|
260
|
+
name: 'subscriptionHandlers',
|
261
|
+
label: 'subscription-handlers'
|
96
262
|
}
|
97
263
|
]
|
98
264
|
}
|
@@ -106,7 +272,7 @@ export class OperatoConnector implements Connector {
|
|
106
272
|
}
|
107
273
|
|
108
274
|
get description() {
|
109
|
-
return 'Operato
|
275
|
+
return 'Operato Graphql Connector'
|
110
276
|
}
|
111
277
|
}
|
112
278
|
|
package/translations/en.json
CHANGED
@@ -3,5 +3,6 @@
|
|
3
3
|
"error.schedule is not set": "schedule should be set for the scenario '{scenario}' in order to register as a schedule",
|
4
4
|
"error.timezone is not set": "timezone should be set for the scenario '{scenario}' in order to register as a schedule",
|
5
5
|
"error.scenario instance not found": "scenario instance '{instance}' not found.",
|
6
|
-
"label.auth-key": "authentication key"
|
6
|
+
"label.auth-key": "authentication key",
|
7
|
+
"label.subscription-handlers": "subscription handlers"
|
7
8
|
}
|
package/translations/ja.json
CHANGED
@@ -3,5 +3,6 @@
|
|
3
3
|
"error.schedule is not set": "スケジュールとして登録するためにはシナリオ'{scenario}'にスケジュール情報が設定される必要があります.",
|
4
4
|
"error.timezone is not set": "スケジュールとして登録するためにはシナリオ'{scenario}'にタイム ゾーン情報が設定される必要があります.",
|
5
5
|
"error.scenario instance not found": "シナリオ インスタンス'{instance}'が見つかりません. 既に終了している可能性があります.",
|
6
|
-
"label.auth-key": "認証キー"
|
6
|
+
"label.auth-key": "認証キー",
|
7
|
+
"label.subscription-handlers": "サブスクリプションハンドラー"
|
7
8
|
}
|
package/translations/ko.json
CHANGED
@@ -3,5 +3,6 @@
|
|
3
3
|
"error.schedule is not set": "스케쥴로 등록하기 위해서는 시나리오 '{scenario}'에 스케쥴 정보가 설정되어야 합니다.",
|
4
4
|
"error.timezone is not set": "스케쥴로 등록하기 위해서는 시나리오 '{scenario}'에 타임존 정보가 설정되어야 합니다.",
|
5
5
|
"error.scenario instance not found": "시나리오 인스턴스 '{instance}'를 찾을 수 없습니다. 이미 종료되었을 수 있습니다.",
|
6
|
-
"label.auth-key": "인증 키"
|
6
|
+
"label.auth-key": "인증 키",
|
7
|
+
"label.subscription-handlers": "구독 핸들러"
|
7
8
|
}
|
package/translations/ms.json
CHANGED
@@ -3,5 +3,6 @@
|
|
3
3
|
"error.schedule is not set": "Untuk mendaftarkan sebagai jadual, maklumat jadual mesti diset dalam senario '{scenario}'.",
|
4
4
|
"error.timezone is not set": "Untuk mendaftarkan sebagai jadual, maklumat zon masa mesti diset dalam senario '{scenario}'.",
|
5
5
|
"error.scenario instance not found": "Tidak dapat mencari instans senario '{instance}'. Mungkin telah berakhir.",
|
6
|
-
"label.auth-key": "Kunci pengesahan"
|
6
|
+
"label.auth-key": "Kunci pengesahan",
|
7
|
+
"label.subscription-handlers": "pengendali langganan"
|
7
8
|
}
|
package/translations/zh.json
CHANGED
@@ -3,5 +3,6 @@
|
|
3
3
|
"error.schedule is not set": "要注册为计划,场景 '{scenario}' 需要设置计划信息。",
|
4
4
|
"error.timezone is not set": "要注册为计划,场景 '{scenario}' 需要设置时区信息。",
|
5
5
|
"error.scenario instance not found": "无法找到场景实例 '{instance}'。它可能已经结束。",
|
6
|
-
"label.auth-key": "认证密钥"
|
6
|
+
"label.auth-key": "认证密钥",
|
7
|
+
"label.subscription-handlers": "订阅处理程序"
|
7
8
|
}
|