@things-factory/dataset 8.0.0-beta.9 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. package/client/activities/activity-data-collect-edit.ts +105 -0
  2. package/client/activities/activity-data-collect-view.ts +91 -0
  3. package/client/activities/activity-data-review-edit.ts +133 -0
  4. package/client/activities/activity-data-review-view.ts +145 -0
  5. package/client/activities/activity-ooc-resolve-edit.ts +195 -0
  6. package/client/activities/activity-ooc-resolve-view.ts +143 -0
  7. package/client/activities/activity-ooc-review-edit.ts +173 -0
  8. package/client/activities/activity-ooc-review-view.ts +129 -0
  9. package/client/bootstrap.ts +35 -0
  10. package/client/components/data-entry-form.ts +109 -0
  11. package/client/index.ts +1 -0
  12. package/client/pages/data-archive/data-archive-list-page.ts +277 -0
  13. package/client/pages/data-archive/data-archive-request-popup.ts +177 -0
  14. package/client/pages/data-entry/data-entry-list-page.ts +464 -0
  15. package/client/pages/data-key-set/data-key-item-list.ts +183 -0
  16. package/client/pages/data-key-set/data-key-set-importer.ts +89 -0
  17. package/client/pages/data-key-set/data-key-set-list-page.ts +413 -0
  18. package/client/pages/data-ooc/data-ooc-list-page.ts +549 -0
  19. package/client/pages/data-ooc/data-ooc-page.ts +164 -0
  20. package/client/pages/data-ooc/data-ooc-view.ts +236 -0
  21. package/client/pages/data-ooc/data-oocs-page.ts +200 -0
  22. package/client/pages/data-report/data-report-embed-page.ts +108 -0
  23. package/client/pages/data-report/data-report-list-page.ts +454 -0
  24. package/client/pages/data-report/data-report-samples-page.ts +174 -0
  25. package/client/pages/data-report/jasper-report-oocs-page.ts +110 -0
  26. package/client/pages/data-report/jasper-report-samples-crosstab-page.ts +110 -0
  27. package/client/pages/data-report/jasper-report-samples-page.ts +110 -0
  28. package/client/pages/data-sample/data-sample-list-page.ts +442 -0
  29. package/client/pages/data-sample/data-sample-page.ts +55 -0
  30. package/client/pages/data-sample/data-sample-search-page.ts +424 -0
  31. package/client/pages/data-sample/data-sample-view.ts +292 -0
  32. package/client/pages/data-sample/data-samples-page.ts +249 -0
  33. package/client/pages/data-sensor/data-sensor-list-page.ts +456 -0
  34. package/client/pages/data-set/data-item-list.ts +304 -0
  35. package/client/pages/data-set/data-set-importer.ts +89 -0
  36. package/client/pages/data-set/data-set-list-page.ts +1078 -0
  37. package/client/pages/data-summary/data-summary-list-page.ts +363 -0
  38. package/client/pages/data-summary/data-summary-period-page.ts +439 -0
  39. package/client/pages/data-summary/data-summary-search-page.ts +426 -0
  40. package/client/pages/data-summary/data-summary-view.ts +133 -0
  41. package/client/route.ts +91 -0
  42. package/client/tsconfig.json +13 -0
  43. package/dist-client/activities/activity-data-review-edit.d.ts +1 -5
  44. package/dist-client/activities/activity-data-review-edit.js +5 -143
  45. package/dist-client/activities/activity-data-review-edit.js.map +1 -1
  46. package/dist-client/pages/data-entry/data-entry-list-page.js +2 -2
  47. package/dist-client/pages/data-entry/data-entry-list-page.js.map +1 -1
  48. package/dist-client/tsconfig.tsbuildinfo +1 -1
  49. package/dist-server/activities/activity-data-review.js +5 -18
  50. package/dist-server/activities/activity-data-review.js.map +1 -1
  51. package/dist-server/activities/activity-ooc-review.js +52 -13
  52. package/dist-server/activities/activity-ooc-review.js.map +1 -1
  53. package/dist-server/controllers/create-data-sample.js +94 -4
  54. package/dist-server/controllers/create-data-sample.js.map +1 -1
  55. package/dist-server/controllers/index.d.ts +0 -3
  56. package/dist-server/controllers/index.js +0 -3
  57. package/dist-server/controllers/index.js.map +1 -1
  58. package/dist-server/service/data-sample/data-sample-query.d.ts +1 -1
  59. package/dist-server/service/data-sample/data-sample-query.js +3 -3
  60. package/dist-server/service/data-sample/data-sample-query.js.map +1 -1
  61. package/dist-server/service/index.d.ts +1 -1
  62. package/dist-server/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +26 -26
  64. package/server/activities/activity-data-collect.ts +100 -0
  65. package/server/activities/activity-data-review.ts +82 -0
  66. package/server/activities/activity-ooc-resolve.ts +123 -0
  67. package/server/activities/activity-ooc-review.ts +144 -0
  68. package/server/activities/index.ts +11 -0
  69. package/server/controllers/create-data-sample.ts +426 -0
  70. package/server/controllers/data-use-case.ts +98 -0
  71. package/server/controllers/finalize-data-collection.ts +388 -0
  72. package/server/controllers/index.ts +3 -0
  73. package/server/controllers/issue-data-collection-task.ts +70 -0
  74. package/server/controllers/jasper-report.ts +186 -0
  75. package/server/controllers/query-data-summary-by-period.ts +178 -0
  76. package/server/controllers/shiny-report.ts +54 -0
  77. package/server/engine/index.ts +1 -0
  78. package/server/engine/task/create-data-sample.ts +100 -0
  79. package/server/engine/task/index.ts +2 -0
  80. package/server/engine/task/issue-collect-data.ts +45 -0
  81. package/server/index.ts +8 -0
  82. package/server/routes.ts +188 -0
  83. package/server/service/data-archive/data-archive-mutation.ts +273 -0
  84. package/server/service/data-archive/data-archive-query.ts +58 -0
  85. package/server/service/data-archive/data-archive-type.ts +48 -0
  86. package/server/service/data-archive/data-archive.ts +69 -0
  87. package/server/service/data-archive/index.ts +6 -0
  88. package/server/service/data-key-set/data-key-item-type.ts +31 -0
  89. package/server/service/data-key-set/data-key-set-mutation.ts +201 -0
  90. package/server/service/data-key-set/data-key-set-query.ts +68 -0
  91. package/server/service/data-key-set/data-key-set-type.ts +70 -0
  92. package/server/service/data-key-set/data-key-set.ts +86 -0
  93. package/server/service/data-key-set/index.ts +6 -0
  94. package/server/service/data-ooc/data-ooc-mutation.ts +154 -0
  95. package/server/service/data-ooc/data-ooc-query.ts +106 -0
  96. package/server/service/data-ooc/data-ooc-subscription.ts +48 -0
  97. package/server/service/data-ooc/data-ooc-type.ts +71 -0
  98. package/server/service/data-ooc/data-ooc.ts +259 -0
  99. package/server/service/data-ooc/index.ts +7 -0
  100. package/server/service/data-sample/data-sample-mutation.ts +18 -0
  101. package/server/service/data-sample/data-sample-query.ts +215 -0
  102. package/server/service/data-sample/data-sample-type.ts +47 -0
  103. package/server/service/data-sample/data-sample.ts +193 -0
  104. package/server/service/data-sample/index.ts +6 -0
  105. package/server/service/data-sensor/data-sensor-mutation.ts +116 -0
  106. package/server/service/data-sensor/data-sensor-query.ts +76 -0
  107. package/server/service/data-sensor/data-sensor-type.ts +104 -0
  108. package/server/service/data-sensor/data-sensor.ts +126 -0
  109. package/server/service/data-sensor/index.ts +6 -0
  110. package/server/service/data-set/data-item-type.ts +155 -0
  111. package/server/service/data-set/data-set-mutation.ts +552 -0
  112. package/server/service/data-set/data-set-query.ts +461 -0
  113. package/server/service/data-set/data-set-type.ts +204 -0
  114. package/server/service/data-set/data-set.ts +326 -0
  115. package/server/service/data-set/index.ts +6 -0
  116. package/server/service/data-set-history/data-set-history-query.ts +126 -0
  117. package/server/service/data-set-history/data-set-history-type.ts +12 -0
  118. package/server/service/data-set-history/data-set-history.ts +217 -0
  119. package/server/service/data-set-history/event-subscriber.ts +17 -0
  120. package/server/service/data-set-history/index.ts +7 -0
  121. package/server/service/data-spec/data-spec-manager.ts +21 -0
  122. package/server/service/data-spec/data-spec-query.ts +21 -0
  123. package/server/service/data-spec/data-spec.ts +45 -0
  124. package/server/service/data-spec/index.ts +5 -0
  125. package/server/service/data-summary/data-summary-mutation.ts +45 -0
  126. package/server/service/data-summary/data-summary-query.ts +179 -0
  127. package/server/service/data-summary/data-summary-type.ts +86 -0
  128. package/server/service/data-summary/data-summary.ts +170 -0
  129. package/server/service/data-summary/index.ts +7 -0
  130. package/server/service/index.ts +57 -0
  131. package/server/tsconfig.json +10 -0
  132. package/server/utils/config-resolver.ts +29 -0
  133. package/server/utils/index.ts +1 -0
  134. package/translations/en.json +0 -3
  135. package/translations/ja.json +0 -3
  136. package/translations/ko.json +0 -3
  137. package/translations/ms.json +0 -3
  138. package/translations/zh.json +0 -3
  139. package/dist-server/controllers/create-data-ooc.d.ts +0 -4
  140. package/dist-server/controllers/create-data-ooc.js +0 -65
  141. package/dist-server/controllers/create-data-ooc.js.map +0 -1
  142. package/dist-server/controllers/issue-ooc-resolve.d.ts +0 -3
  143. package/dist-server/controllers/issue-ooc-resolve.js +0 -49
  144. package/dist-server/controllers/issue-ooc-resolve.js.map +0 -1
  145. package/dist-server/controllers/issue-ooc-review.d.ts +0 -3
  146. package/dist-server/controllers/issue-ooc-review.js +0 -47
  147. package/dist-server/controllers/issue-ooc-review.js.map +0 -1
@@ -0,0 +1,388 @@
1
+ const statistics = require('simple-statistics')
2
+ const deepClone = require('lodash/cloneDeep')
3
+
4
+ import moment from 'moment-timezone'
5
+ import { In } from 'typeorm'
6
+
7
+ import { Sorting, getQueryBuilderFromListParams } from '@things-factory/shell'
8
+ import { logger } from '@things-factory/env'
9
+ import {
10
+ getDateRangeForWorkDate,
11
+ getDateRangeForWorkShift,
12
+ getLatestWorkDateAndShift,
13
+ getSummaryScheduleForWorkDate,
14
+ getSummaryScheduleForWorkShift
15
+ } from '@things-factory/work-shift'
16
+
17
+ import { DataSample } from '../service/data-sample/data-sample'
18
+ import { DataSet, DataSetSummaryPeriodType } from '../service/data-set/data-set'
19
+
20
+ import { DataSummary } from '../service/data-summary/data-summary'
21
+ import { DataKeyItem } from '../service/data-key-set/data-key-item-type'
22
+
23
+ import { DataItem } from 'service'
24
+
25
+ const STAT_FUNCTION_MAP = {
26
+ sum: 'sum',
27
+ mean: 'mean',
28
+ stddev: 'standardDeviation',
29
+ variance: 'variance',
30
+ min: 'min',
31
+ max: 'max',
32
+ range: 'range',
33
+ median: 'median',
34
+ mode: 'mode'
35
+ }
36
+
37
+ const compareKeys = (dataKeyItems: DataKeyItem[], summary: Partial<DataSummary>, sample: DataSample): boolean => {
38
+ return dataKeyItems.every((item, index) => {
39
+ const prop = `key0${index + 1}`
40
+ return sample[prop] === summary[prop]
41
+ })
42
+ }
43
+
44
+ const buildKeysFromSample = (dataKeyItems: DataKeyItem[], sample: DataSample): Partial<DataSummary> => {
45
+ return dataKeyItems.reduce((sum, item, index) => {
46
+ const prop = `key0${index + 1}`
47
+ sum[prop] = sample[prop]
48
+
49
+ return sum
50
+ }, {} as Partial<DataSummary>)
51
+ }
52
+
53
+ const buildKeySortingList = (dataKeyItems: DataKeyItem[]): Sorting[] => {
54
+ return dataKeyItems.reduce((sum, item, index) => {
55
+ const name = `key0${index + 1}`
56
+ sum.push({ name, desc: true })
57
+ return sum
58
+ }, [])
59
+ }
60
+
61
+ const calculateSummary = (dataItems: DataItem[], base: { [tag: string]: any[] }) => {
62
+ return dataItems.reduce((summary, item) => {
63
+ const tag = item.tag
64
+
65
+ const data = base[tag]
66
+ .flat(Infinity)
67
+ .map(Number)
68
+ .filter(item => !isNaN(item))
69
+
70
+ if (data.length > 0) {
71
+ try {
72
+ switch (item.stat) {
73
+ case 'range':
74
+ summary[tag] = statistics.max(data) - statistics.min(data)
75
+ break
76
+
77
+ default:
78
+ const functionName = STAT_FUNCTION_MAP[item.stat]
79
+ summary[tag] = (functionName && statistics[functionName](data)) || ''
80
+ }
81
+ } catch (err) {
82
+ summary[tag] = null
83
+ console.error(err)
84
+ }
85
+ } else {
86
+ summary[tag] = null
87
+ }
88
+
89
+ return summary
90
+ }, {})
91
+ }
92
+
93
+ const fillSummaryResult = (dataSummary: Partial<DataSummary>, dataItems: DataItem[], base: { [tag: string]: any[] }): void => {
94
+ const summary = calculateSummary(dataItems, base)
95
+
96
+ dataSummary.summary = summary
97
+ dataItems.slice(0, 4).forEach((dataItem, idx) => {
98
+ const value = Number(summary[dataItem.tag])
99
+ dataSummary[`data0${idx + 1}`] = isNaN(value) ? null : value
100
+ })
101
+ }
102
+
103
+ async function getLatestTimesForPeriod(periodType: DataSetSummaryPeriodType, context: ResolverContext): Promise<{ date?: string; period?: string; range: Date[] }> {
104
+ const { domain } = context.state
105
+ const now = moment()
106
+
107
+ if (periodType == DataSetSummaryPeriodType.Hour) {
108
+ const begin = now.clone().subtract(1, 'hour').startOf('hour')
109
+ const end = now.clone().startOf('hour')
110
+ const date = begin.clone().tz(domain.timezone)
111
+
112
+ return {
113
+ date: date.format('YYYY-MM-DD'),
114
+ period: date.format('HH'),
115
+ range: [begin.toDate(), end.toDate()]
116
+ }
117
+ } else if (periodType == DataSetSummaryPeriodType.WorkShift) {
118
+ const { workDate, workShift, shiftRange } = await getLatestWorkDateAndShift(domain, new Date())
119
+
120
+ return { date: workDate, period: workShift, range: shiftRange }
121
+ } else if (periodType == DataSetSummaryPeriodType.WorkDate) {
122
+ const { workDate, dateRange } = await getLatestWorkDateAndShift(domain, new Date())
123
+
124
+ return { date: workDate, range: dateRange }
125
+ } else if (periodType == DataSetSummaryPeriodType.Day) {
126
+ const begin = now.clone().subtract(1, 'day').startOf('day')
127
+ const end = now.clone().startOf('day')
128
+ const date = begin.clone().tz(domain.timezone)
129
+
130
+ return {
131
+ date: date.format('YYYY-MM-DD'),
132
+ range: [begin.toDate(), end.toDate()]
133
+ }
134
+ }
135
+ }
136
+
137
+ async function getTimesForPeriod(
138
+ periodType: DataSetSummaryPeriodType,
139
+ date: string,
140
+ period: string,
141
+ context: ResolverContext
142
+ ): Promise<{ date?: string; period?: string; range: Date[] }> {
143
+ const { domain } = context.state
144
+
145
+ if (periodType == DataSetSummaryPeriodType.Hour) {
146
+ const theDate = moment.tz(`${date} ${period}:00:00`, 'YYYY-MM-DD HH:mm:ss', domain.timezone)
147
+
148
+ const begin = theDate.clone().startOf('hour').toDate()
149
+ const end = theDate.clone().add(+1, 'hour').startOf('hour').toDate()
150
+
151
+ return {
152
+ date,
153
+ period,
154
+ range: [begin, end]
155
+ }
156
+ } else if (periodType == DataSetSummaryPeriodType.WorkShift) {
157
+ const range = await getDateRangeForWorkShift(domain, date, period)
158
+
159
+ return { date, period, range }
160
+ } else if (periodType == DataSetSummaryPeriodType.WorkDate) {
161
+ const range = await getDateRangeForWorkDate(domain, date)
162
+
163
+ return { date, range }
164
+ } else if (periodType == DataSetSummaryPeriodType.Day) {
165
+ const theDate = moment.tz(`${date} 00:00:00`, 'YYYY-MM-DD HH:mm:ss', domain.timezone)
166
+
167
+ const begin = theDate.clone().startOf('day').toDate()
168
+ const end = theDate.clone().add(1, 'day').startOf('day').toDate()
169
+
170
+ return {
171
+ date: moment(begin).tz(domain.timezone).format('YYYY-MM-DD'),
172
+ range: [begin, end]
173
+ }
174
+ }
175
+ }
176
+
177
+ export async function getDataFinalizeCrontabSchedule(dataSet: DataSet, context: ResolverContext): Promise<string> {
178
+ const { domain, user, tx } = context.state
179
+
180
+ try {
181
+ const { summaryPeriod } = dataSet
182
+
183
+ if (summaryPeriod == DataSetSummaryPeriodType.Hour) {
184
+ return '0 5 * * * *'
185
+ } else if (summaryPeriod == DataSetSummaryPeriodType.WorkShift) {
186
+ return await getSummaryScheduleForWorkShift(domain)
187
+ } else if (summaryPeriod == DataSetSummaryPeriodType.WorkDate) {
188
+ return await getSummaryScheduleForWorkDate(domain)
189
+ } else if (summaryPeriod == DataSetSummaryPeriodType.Day) {
190
+ return '0 10 0 * * *'
191
+ }
192
+ } catch (err) {
193
+ console.error(err)
194
+ }
195
+ }
196
+
197
+ export async function finalizeLatestDataCollection(dataSetId: string, context: ResolverContext): Promise<boolean> {
198
+ const { domain, user, tx } = context.state
199
+
200
+ try {
201
+ const dataSet = await tx.getRepository(DataSet).findOne({
202
+ where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id: dataSetId },
203
+ relations: ['dataKeySet']
204
+ })
205
+
206
+ const dataKeyItems = dataSet.dataKeySet?.dataKeyItems || []
207
+ const dataItems = dataSet.dataItems.filter(item => item.stat)
208
+ const initialSummary = dataItems.reduce((sum, item) => {
209
+ sum[item.tag] = []
210
+ return sum
211
+ }, {})
212
+
213
+ const { date, period, range } = await getLatestTimesForPeriod(dataSet.summaryPeriod, context)
214
+ const limit = 100
215
+ var page = 1
216
+
217
+ var summaries: Partial<DataSummary>[] = []
218
+ var summary: Partial<DataSummary>
219
+
220
+ do {
221
+ const samples = await getQueryBuilderFromListParams({
222
+ repository: tx.getRepository(DataSample),
223
+ domain,
224
+ params: {
225
+ filters: [{ name: 'dataSetId', operator: 'eq', value: dataSetId }],
226
+ pagination: { page, limit },
227
+ sortings: [...buildKeySortingList(dataKeyItems), { name: 'collectedAt', desc: true }]
228
+ },
229
+ alias: 'datasample'
230
+ })
231
+ // The 'Between' operator includes the 'to' time in the filtering, making it unsuitable for the desired use case.
232
+ // .andWhere({ collectedAt: Between.apply(null, range) })
233
+ .andWhere('datasample.collectedAt >= :from', { from: range[0] })
234
+ .andWhere('datasample.collectedAt < :to', { to: range[1] })
235
+ .getMany()
236
+
237
+ for (const sample of samples) {
238
+ if (!summary || !compareKeys(dataKeyItems, summary, sample)) {
239
+ if (summary) {
240
+ fillSummaryResult(summary, dataItems, summary.summary)
241
+ summaries.push(summary)
242
+ }
243
+
244
+ summary = {
245
+ domain,
246
+ name: dataSet.name,
247
+ description: dataSet.description,
248
+ date,
249
+ period,
250
+ dataSet,
251
+ ...buildKeysFromSample(dataKeyItems, sample),
252
+ count: 0,
253
+ countOoc: 0,
254
+ countOos: 0,
255
+ summary: deepClone(initialSummary),
256
+ updater: user,
257
+ creator: user
258
+ }
259
+ }
260
+
261
+ summary.count++
262
+ sample.ooc && summary.countOoc++
263
+ sample.oos && summary.countOos++
264
+
265
+ dataItems.forEach(item => {
266
+ summary.summary[item.tag].push(sample.data[item.tag])
267
+ })
268
+ }
269
+
270
+ if (samples.length < limit) {
271
+ if (summary) {
272
+ fillSummaryResult(summary, dataItems, summary.summary)
273
+ summaries.push(summary)
274
+ }
275
+ break
276
+ }
277
+
278
+ page++
279
+ } while (true)
280
+
281
+ tx.getRepository(DataSummary).upsert(summaries, ['domain', 'dataSet', 'key01', 'key02', 'key03', 'key04', 'key05', 'date', 'period'])
282
+
283
+ return true
284
+ } catch (e) {
285
+ logger.error(e)
286
+ }
287
+
288
+ return false
289
+ }
290
+
291
+ export async function finalizeDataCollection(dataSetId: string, date: string, period: string, context: ResolverContext): Promise<boolean> {
292
+ const { domain, user, tx } = context.state
293
+
294
+ try {
295
+ const dataSet =
296
+ dataSetId &&
297
+ (await tx.getRepository(DataSet).findOne({
298
+ where: { domain: In([domain.id, domain.parentId].filter(Boolean)), id: dataSetId },
299
+ relations: ['dataKeySet']
300
+ }))
301
+
302
+ const dataKeyItems = dataSet.dataKeySet?.dataKeyItems || []
303
+ const dataItems = dataSet.dataItems.filter(item => item.stat)
304
+ const initialSummary = dataItems.reduce((sum, item) => {
305
+ sum[item.tag] = []
306
+ return sum
307
+ }, {})
308
+
309
+ const times = await getTimesForPeriod(dataSet.summaryPeriod, date, period, context)
310
+ const range = times.range
311
+ period = times.period
312
+
313
+ const limit = 100
314
+ var page = 1
315
+
316
+ var summaries: Partial<DataSummary>[] = []
317
+ var summary: Partial<DataSummary>
318
+
319
+ do {
320
+ const samples = await getQueryBuilderFromListParams({
321
+ repository: tx.getRepository(DataSample),
322
+ params: {
323
+ filters: [{ name: 'dataSetId', operator: 'eq', value: dataSetId }],
324
+ pagination: { page, limit },
325
+ sortings: [...buildKeySortingList(dataKeyItems), { name: 'collectedAt', desc: true }]
326
+ },
327
+ domain,
328
+ alias: 'datasample'
329
+ })
330
+ // The 'Between' operator includes the 'to' time in the filtering, making it unsuitable for the desired use case.
331
+ // .andWhere({ collectedAt: Between.apply(null, range) })
332
+ .andWhere('datasample.collectedAt >= :from', { from: range[0] })
333
+ .andWhere('datasample.collectedAt < :to', { to: range[1] })
334
+ .getMany()
335
+
336
+ for (const sample of samples) {
337
+ if (!summary || !compareKeys(dataKeyItems, summary, sample)) {
338
+ if (summary) {
339
+ fillSummaryResult(summary, dataItems, summary.summary)
340
+ summaries.push(summary)
341
+ }
342
+
343
+ summary = {
344
+ domain,
345
+ name: dataSet.name,
346
+ description: dataSet.description,
347
+ date,
348
+ period,
349
+ dataSet,
350
+ ...buildKeysFromSample(dataKeyItems, sample),
351
+ count: 0,
352
+ countOoc: 0,
353
+ countOos: 0,
354
+ summary: deepClone(initialSummary),
355
+ updater: user,
356
+ creator: user
357
+ }
358
+ }
359
+
360
+ summary.count++
361
+ sample.ooc && summary.countOoc++
362
+ sample.oos && summary.countOos++
363
+
364
+ dataItems.forEach(item => {
365
+ summary.summary[item.tag].push(sample.data[item.tag])
366
+ })
367
+ }
368
+
369
+ if (samples.length < limit) {
370
+ if (summary) {
371
+ fillSummaryResult(summary, dataItems, summary.summary)
372
+ summaries.push(summary)
373
+ }
374
+ break
375
+ }
376
+
377
+ page++
378
+ } while (true)
379
+
380
+ tx.getRepository(DataSummary).upsert(summaries, ['domain', 'dataSet', 'key01', 'key02', 'key03', 'key04', 'key05', 'date', 'period'])
381
+
382
+ return true
383
+ } catch (e) {
384
+ logger.error(e)
385
+ }
386
+
387
+ return false
388
+ }
@@ -0,0 +1,3 @@
1
+ export * from './create-data-sample'
2
+ export * from './data-use-case'
3
+ export * from './query-data-summary-by-period'
@@ -0,0 +1,70 @@
1
+ import { In } from 'typeorm'
2
+
3
+ import { Domain, getDataSource } from '@things-factory/shell'
4
+ import { Activity, ActivityInstance, issue } from '@things-factory/worklist'
5
+
6
+ import { DataSet } from '../service/data-set/data-set'
7
+
8
+ export async function issueDataCollectionTask(
9
+ domainId: string,
10
+ dataSetId: string,
11
+ context: ResolverContext
12
+ ): Promise<ActivityInstance | void> {
13
+ await getDataSource().transaction(async tx => {
14
+ const domain = await tx.getRepository(Domain).findOneBy({ id: domainId })
15
+
16
+ if (!domain) {
17
+ throw new Error(`domain(${domainId}) not found`)
18
+ }
19
+
20
+ const dataSet = await tx.getRepository(DataSet).findOne({
21
+ where: {
22
+ domain: {
23
+ id: In([domain.id, domain.parentId].filter(Boolean))
24
+ },
25
+ id: dataSetId
26
+ },
27
+ relations: ['entryRole']
28
+ })
29
+
30
+ const activity = (await tx.getRepository(Activity).findOneBy({
31
+ domain: { id: In([domain.id, domain.parentId].filter(Boolean)) },
32
+ name: 'Collect Data'
33
+ })) as Activity
34
+
35
+ if (activity) {
36
+ const { entryRole, timeLimit } = dataSet
37
+
38
+ /* 해당 dataset에 대한 데이타 수집 태스크를 dataset entryRole에게 할당한다. */
39
+ if (entryRole) {
40
+ const activityInstance = {
41
+ name: `[Data 수집] ${dataSet.name}`,
42
+ description: dataSet.description,
43
+ activityId: activity.id,
44
+ dueAt: new Date(Date.now() + (timeLimit || activity.standardTime || 24 * 60 * 60) * 1000),
45
+ input: {
46
+ dataSetId: dataSet.id,
47
+ dataSetName: dataSet.name
48
+ },
49
+ assigneeRole: entryRole,
50
+ threadsMin: 1,
51
+ threadsMax: 1
52
+ }
53
+
54
+ context.state = {
55
+ ...context.state,
56
+ domain,
57
+ tx
58
+ }
59
+
60
+ return await issue(activityInstance, context)
61
+ } else {
62
+ throw new Error(
63
+ `Data Entry Role not set. So Data Collect Activity for ${dataSet.name}($dataSet.id) could not be issued.`
64
+ )
65
+ }
66
+ } else {
67
+ throw new Error(`Data Collect Activity is not installed.`)
68
+ }
69
+ })
70
+ }
@@ -0,0 +1,186 @@
1
+ import FormData from 'form-data'
2
+ import fetch from 'node-fetch'
3
+
4
+ import { getEndpointUrl } from '../utils/config-resolver'
5
+
6
+ import { STORAGE } from '@things-factory/attachment-base'
7
+ import { AthenaController } from '@things-factory/aws-base'
8
+ import { config } from '@things-factory/env'
9
+
10
+ const dataReportConfig = config.get('dataReport')
11
+ const {
12
+ jasper: {
13
+ endpoint: ENDPOINT,
14
+ datasource: { database: DATABASE }
15
+ }
16
+ } = dataReportConfig || {
17
+ jasper: {
18
+ endpoint: {},
19
+ datasource: {}
20
+ }
21
+ }
22
+
23
+ function transformValuesToRows(queryResult) {
24
+ var parseData = []
25
+ let index = 1
26
+ for (let i = 0; i < queryResult.Items.length; i++) {
27
+ var j = 0
28
+ const data = JSON.parse(queryResult.Items[i].data)
29
+ const spec = JSON.parse(queryResult.Items[i].spec)
30
+
31
+ for (let key in data) {
32
+ if (Array.isArray(data[key])) {
33
+ for (j = 0; j < data[key].length; j++) {
34
+ for (let specKey in spec) {
35
+ if (key === specKey) {
36
+ parseData.push({
37
+ item: spec[specKey].name,
38
+ index: index + j,
39
+ value: String(data[key][j])
40
+ })
41
+ }
42
+ }
43
+ }
44
+ } else {
45
+ parseData.push({
46
+ item: key,
47
+ index,
48
+ value: String(data[key])
49
+ })
50
+ }
51
+ }
52
+ if (j !== 0) {
53
+ index = index + j
54
+ } else {
55
+ index = index + 1
56
+ }
57
+ }
58
+ }
59
+
60
+ /** @todo considering trasformation in lambda, as massive dataset */
61
+ function pivotData(rows) {
62
+ let parsedData = []
63
+ let index = 1
64
+ for (let i = 0; i < rows.length; i++) {
65
+ let j = 0
66
+ const data = JSON.parse(rows[i].data)
67
+ const spec = JSON.parse(rows[i].spec)
68
+
69
+ for (let key in data) {
70
+ /** @todo rule to display or not, about unspecified spec */
71
+ const value = data[key]
72
+ !spec[key]?.hidden &&
73
+ parsedData.push({
74
+ item: spec[key]?.name || key,
75
+ index,
76
+ value: Array.isArray(value) ? value.join(', ') : value
77
+ })
78
+ }
79
+ if (j !== 0) {
80
+ index = index + j
81
+ } else {
82
+ index = index + 1
83
+ }
84
+ }
85
+
86
+ return parsedData
87
+ }
88
+
89
+ function parseJsonDataField(rows) {
90
+ let parsedData = []
91
+ for (let i = 0; i < rows.length; i++) {
92
+ const row = rows[i]
93
+ const data = JSON.parse(row.data)
94
+ for (let key in data) {
95
+ if (Array.isArray(data[key])) {
96
+ data[key] = data[key].toString()
97
+ }
98
+ }
99
+ delete row.data
100
+ parsedData.push({ ...row, ...data })
101
+ }
102
+
103
+ return parsedData
104
+ }
105
+
106
+ const athenaClient = new AthenaController()
107
+
108
+ async function queryAthena(params) {
109
+ const { table, domain, datasetId, startDate, endDate, workShift, timezone } = params
110
+ const queryData = {
111
+ sql: `SELECT ds.name, ds.description, ds.data, dsh.data_items as spec, ds.workdate, ds.workshift,
112
+ DATE_FORMAT(
113
+ FROM_UNIXTIME(collected_at / 1000 / 1000) AT TIME ZONE '${timezone || 'UTC'}',
114
+ '%Y-%m-%d %H:%i:%s'
115
+ ) AS dscollected_at
116
+ FROM ${table} ds
117
+ JOIN data_set_histories dsh
118
+ ON (ds.datasetid = dsh.original_id
119
+ and ds.data_set_version = dsh.version)
120
+ WHERE ds.domain='${domain}'
121
+ AND ds.datasetid = '${datasetId}'
122
+ AND ds.workdate >= '${startDate}'
123
+ AND ds.workdate <= '${endDate}'
124
+ ${workShift ? "AND ds.workshift = '" + workShift + "'" : ''}
125
+ ORDER BY ds.collected_at`,
126
+ db: DATABASE
127
+ }
128
+ // and json_extract_scalar(data, '$.dauid') = 'A8032AD81730'
129
+
130
+ return await athenaClient.query(queryData)
131
+ }
132
+
133
+ export async function renderJasperReport(context: any) {
134
+ const {
135
+ state: { domain },
136
+ query
137
+ } = context
138
+
139
+ const template = await STORAGE.readFile(query['reportTemplate'] || 'dynamic_header_sample.jrxml', 'utf-8')
140
+ let templateType = query['templateType'] || 'crosstab'
141
+ let parsedData = []
142
+
143
+ // @todo: get dataset timezone
144
+ /**
145
+ * const variables = await gql(dataSet(id:${dataSetId}) {
146
+ * name, description, partition_keys, timezone
147
+ * })
148
+ */
149
+
150
+ query['domain'] = domain?.subdomain
151
+ query['timezone'] = domain?.timezone
152
+ const queryResult = await queryAthena(query)
153
+ const rows = queryResult.Items
154
+
155
+ if (!rows.length) {
156
+ return '<h3>Not found result.</h3>'
157
+ } else {
158
+ const firstRow = rows[0]
159
+ // uses the first row values as data-set has no history data.
160
+ const parameters = {
161
+ name: firstRow.name,
162
+ description: firstRow.description,
163
+ ...query
164
+ }
165
+
166
+ if (templateType === 'crosstab') {
167
+ parsedData = pivotData(rows)
168
+ } else {
169
+ parsedData = parseJsonDataField(rows)
170
+ }
171
+
172
+ const formData = new FormData()
173
+ formData.append('template', template)
174
+ formData.append('jsonString', JSON.stringify(parsedData))
175
+ formData.append('parameters', JSON.stringify(parameters))
176
+
177
+ const { reportView } = query
178
+ const url = getEndpointUrl(ENDPOINT, reportView)
179
+ const response = await fetch(url, {
180
+ method: 'POST',
181
+ body: formData
182
+ })
183
+
184
+ return await response.text()
185
+ }
186
+ }