@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.
@@ -24,3 +24,14 @@ GraphQLサーバーエンドポイントを指定します。
24
24
  ### ドメイン
25
25
 
26
26
  - 対象サーバーで動作させたいドメインを指定する必要があります。
27
+
28
+
29
+ ### タグ
30
+
31
+ - ターゲットサーバーで pubsub ベースのデータを購読する際に使用されるタグ名を指定します。
32
+
33
+
34
+ ### 场景
35
+
36
+ - タグが設定されている場合、購読されたメッセージを処理するシナリオを指定します。
37
+
@@ -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.
@@ -24,3 +24,13 @@
24
24
  ### 领域
25
25
 
26
26
  - 您需要指定要在目标服务器上运行的实际域。
27
+
28
+
29
+ ### 标签
30
+
31
+ - 指定在目标服务器上订阅基于 pubsub 的数据时使用的标签名称。
32
+
33
+
34
+ ### 场景
35
+
36
+ - 如果设置了标签,则指定用于处理已订阅消息的场景。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/integration-base",
3
- "version": "6.2.38",
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": "df14ab133528fadeb146f3dc2652f499df30a918"
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
- var ERROR_HANDLER: any = ({ graphQLErrors, networkError }) => {
41
- if (graphQLErrors)
42
- graphQLErrors.map(({ message, locations, path }) => {
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
- if (networkError) {
47
- ConnectionManager.logger.error(`[Network error - ${networkError.statusCode}] ${networkError}`)
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
- ConnectionManager.addConnectionInstance(
56
- connection,
57
- new ApolloClient({
58
- defaultOptions,
59
- cache,
60
- link: setContext((_, { headers }) => {
61
- return {
62
- headers: {
63
- ...headers,
64
- 'x-things-factory-domain': domain,
65
- authorization: authKey ? `Bearer ${authKey}` : ''
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
- }).concat(httpLink)
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 Grpahql Connector'
275
+ return 'Operato Graphql Connector'
110
276
  }
111
277
  }
112
278
 
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }