@things-factory/integration-label-studio 9.1.19

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 (152) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/EXTERNAL_DATA_SOURCING.md +484 -0
  3. package/IMPLEMENTATION_GUIDE.md +469 -0
  4. package/INTEGRATION.md +279 -0
  5. package/README.md +1014 -0
  6. package/SETUP_GUIDE.md +577 -0
  7. package/TEST_GUIDE.md +387 -0
  8. package/UI_CUSTOMIZATION.md +395 -0
  9. package/USER_SYNC_GUIDE.md +514 -0
  10. package/client/bootstrap.ts +1 -0
  11. package/client/index.ts +1 -0
  12. package/client/label-studio-label-page.ts +52 -0
  13. package/client/label-studio-project-create.ts +216 -0
  14. package/client/label-studio-project-list.ts +214 -0
  15. package/client/label-studio-wrapper.ts +294 -0
  16. package/client/route.ts +15 -0
  17. package/client/tsconfig.json +13 -0
  18. package/config/config.development.js +124 -0
  19. package/config/config.production.js +182 -0
  20. package/dist-client/bootstrap.d.ts +1 -0
  21. package/dist-client/bootstrap.js +2 -0
  22. package/dist-client/bootstrap.js.map +1 -0
  23. package/dist-client/index.d.ts +1 -0
  24. package/dist-client/index.js +2 -0
  25. package/dist-client/index.js.map +1 -0
  26. package/dist-client/label-studio-label-page.d.ts +8 -0
  27. package/dist-client/label-studio-label-page.js +54 -0
  28. package/dist-client/label-studio-label-page.js.map +1 -0
  29. package/dist-client/label-studio-project-create.d.ts +16 -0
  30. package/dist-client/label-studio-project-create.js +235 -0
  31. package/dist-client/label-studio-project-create.js.map +1 -0
  32. package/dist-client/label-studio-project-list.d.ts +16 -0
  33. package/dist-client/label-studio-project-list.js +222 -0
  34. package/dist-client/label-studio-project-list.js.map +1 -0
  35. package/dist-client/label-studio-wrapper.d.ts +57 -0
  36. package/dist-client/label-studio-wrapper.js +304 -0
  37. package/dist-client/label-studio-wrapper.js.map +1 -0
  38. package/dist-client/route.d.ts +1 -0
  39. package/dist-client/route.js +14 -0
  40. package/dist-client/route.js.map +1 -0
  41. package/dist-client/tsconfig.tsbuildinfo +1 -0
  42. package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
  43. package/dist-server/controller/label-studio-role-mapper.js +65 -0
  44. package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
  45. package/dist-server/controller/user-provisioning-service.d.ts +66 -0
  46. package/dist-server/controller/user-provisioning-service.js +264 -0
  47. package/dist-server/controller/user-provisioning-service.js.map +1 -0
  48. package/dist-server/index.d.ts +7 -0
  49. package/dist-server/index.js +19 -0
  50. package/dist-server/index.js.map +1 -0
  51. package/dist-server/route/label-studio-sso.d.ts +2 -0
  52. package/dist-server/route/label-studio-sso.js +156 -0
  53. package/dist-server/route/label-studio-sso.js.map +1 -0
  54. package/dist-server/route/webhook.d.ts +65 -0
  55. package/dist-server/route/webhook.js +248 -0
  56. package/dist-server/route/webhook.js.map +1 -0
  57. package/dist-server/route.d.ts +1 -0
  58. package/dist-server/route.js +21 -0
  59. package/dist-server/route.js.map +1 -0
  60. package/dist-server/service/ai-prediction-service.d.ts +27 -0
  61. package/dist-server/service/ai-prediction-service.js +222 -0
  62. package/dist-server/service/ai-prediction-service.js.map +1 -0
  63. package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
  64. package/dist-server/service/dataset-labeling-integration.js +512 -0
  65. package/dist-server/service/dataset-labeling-integration.js.map +1 -0
  66. package/dist-server/service/external-data-source-service.d.ts +78 -0
  67. package/dist-server/service/external-data-source-service.js +415 -0
  68. package/dist-server/service/external-data-source-service.js.map +1 -0
  69. package/dist-server/service/index.d.ts +12 -0
  70. package/dist-server/service/index.js +27 -0
  71. package/dist-server/service/index.js.map +1 -0
  72. package/dist-server/service/label-studio-sso-service.d.ts +38 -0
  73. package/dist-server/service/label-studio-sso-service.js +98 -0
  74. package/dist-server/service/label-studio-sso-service.js.map +1 -0
  75. package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
  76. package/dist-server/service/ml/ml-backend-service.js +153 -0
  77. package/dist-server/service/ml/ml-backend-service.js.map +1 -0
  78. package/dist-server/service/prediction/prediction-management.d.ts +32 -0
  79. package/dist-server/service/prediction/prediction-management.js +299 -0
  80. package/dist-server/service/prediction/prediction-management.js.map +1 -0
  81. package/dist-server/service/project/project-management.d.ts +36 -0
  82. package/dist-server/service/project/project-management.js +309 -0
  83. package/dist-server/service/project/project-management.js.map +1 -0
  84. package/dist-server/service/task/task-management.d.ts +42 -0
  85. package/dist-server/service/task/task-management.js +372 -0
  86. package/dist-server/service/task/task-management.js.map +1 -0
  87. package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
  88. package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
  89. package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
  90. package/dist-server/service/webhook/webhook-management.d.ts +21 -0
  91. package/dist-server/service/webhook/webhook-management.js +134 -0
  92. package/dist-server/service/webhook/webhook-management.js.map +1 -0
  93. package/dist-server/tsconfig.tsbuildinfo +1 -0
  94. package/dist-server/types/dataset-labeling-types.d.ts +71 -0
  95. package/dist-server/types/dataset-labeling-types.js +259 -0
  96. package/dist-server/types/dataset-labeling-types.js.map +1 -0
  97. package/dist-server/types/label-studio-types.d.ts +128 -0
  98. package/dist-server/types/label-studio-types.js +494 -0
  99. package/dist-server/types/label-studio-types.js.map +1 -0
  100. package/dist-server/types/prediction-types.d.ts +39 -0
  101. package/dist-server/types/prediction-types.js +121 -0
  102. package/dist-server/types/prediction-types.js.map +1 -0
  103. package/dist-server/utils/annotation-exporter.d.ts +104 -0
  104. package/dist-server/utils/annotation-exporter.js +261 -0
  105. package/dist-server/utils/annotation-exporter.js.map +1 -0
  106. package/dist-server/utils/label-config-builder.d.ts +117 -0
  107. package/dist-server/utils/label-config-builder.js +286 -0
  108. package/dist-server/utils/label-config-builder.js.map +1 -0
  109. package/dist-server/utils/label-studio-api-client.d.ts +180 -0
  110. package/dist-server/utils/label-studio-api-client.js +401 -0
  111. package/dist-server/utils/label-studio-api-client.js.map +1 -0
  112. package/dist-server/utils/media-url-extractor.d.ts +45 -0
  113. package/dist-server/utils/media-url-extractor.js +152 -0
  114. package/dist-server/utils/media-url-extractor.js.map +1 -0
  115. package/dist-server/utils/task-transformer.d.ts +108 -0
  116. package/dist-server/utils/task-transformer.js +260 -0
  117. package/dist-server/utils/task-transformer.js.map +1 -0
  118. package/package.json +47 -0
  119. package/server/SERVER_STRUCTURE.md +351 -0
  120. package/server/controller/label-studio-role-mapper.ts +76 -0
  121. package/server/controller/user-provisioning-service.ts +340 -0
  122. package/server/index.ts +19 -0
  123. package/server/route/label-studio-sso.ts +194 -0
  124. package/server/route/webhook.ts +304 -0
  125. package/server/route.ts +35 -0
  126. package/server/service/ai-prediction-service.ts +239 -0
  127. package/server/service/dataset-labeling-integration.ts +590 -0
  128. package/server/service/external-data-source-service.ts +438 -0
  129. package/server/service/index.ts +24 -0
  130. package/server/service/label-studio-sso-service.ts +108 -0
  131. package/server/service/labeling-scenario-service.ts.deprecated +566 -0
  132. package/server/service/ml/ml-backend-service.ts +127 -0
  133. package/server/service/prediction/prediction-management.ts +281 -0
  134. package/server/service/project/project-management.ts +284 -0
  135. package/server/service/task/task-management.ts +363 -0
  136. package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
  137. package/server/service/webhook/webhook-management.ts +109 -0
  138. package/server/tsconfig.json +11 -0
  139. package/server/types/dataset-labeling-types.ts +181 -0
  140. package/server/types/global.d.ts +23 -0
  141. package/server/types/label-studio-types.ts +346 -0
  142. package/server/types/prediction-types.ts +86 -0
  143. package/server/types/scenario-types.ts.deprecated +362 -0
  144. package/server/utils/annotation-exporter.ts +340 -0
  145. package/server/utils/label-config-builder.ts +340 -0
  146. package/server/utils/label-studio-api-client.ts +487 -0
  147. package/server/utils/media-url-extractor.ts +193 -0
  148. package/server/utils/task-transformer.ts +342 -0
  149. package/test-ai-prediction.js +268 -0
  150. package/test-dataset-integration.js +449 -0
  151. package/test-simple.js +89 -0
  152. package/things-factory.config.js +12 -0
@@ -0,0 +1,363 @@
1
+ import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { labelStudioApi } from '../../utils/label-studio-api-client.js'
3
+ import { TaskTransformer } from '../../utils/task-transformer.js'
4
+ import { exportAnnotations } from '../../utils/annotation-exporter.js'
5
+ import {
6
+ LabelStudioTask,
7
+ TaskDataInput,
8
+ TaskImportResult,
9
+ LabelStudioAnnotation,
10
+ ExportResult,
11
+ ImportTasksWithTransformInput,
12
+ ExportAnnotationsInput,
13
+ AnnotationExportResult
14
+ } from '../../types/label-studio-types.js'
15
+
16
+ @Resolver()
17
+ export class TaskManagement {
18
+ /**
19
+ * Get tasks for a project
20
+ */
21
+ @Query(returns => [LabelStudioTask], {
22
+ description: 'Get all tasks for a Label Studio project'
23
+ })
24
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
25
+ async labelStudioTasks(
26
+ @Arg('projectId', type => Int) projectId: number,
27
+ @Arg('page', type => Int, { nullable: true, defaultValue: 1 }) page: number,
28
+ @Arg('pageSize', type => Int, { nullable: true, defaultValue: 100 }) pageSize: number,
29
+ @Ctx() context: ResolverContext
30
+ ): Promise<LabelStudioTask[]> {
31
+ try {
32
+ const response = await labelStudioApi.getTasks(projectId, {
33
+ page,
34
+ page_size: pageSize
35
+ })
36
+
37
+ const tasks = response.tasks || response.results || response
38
+
39
+ return tasks.map((task: any) => ({
40
+ id: task.id,
41
+ data: JSON.stringify(task.data),
42
+ annotationCount: task.annotations?.length || task.total_annotations || 0,
43
+ isCompleted: task.is_labeled || false,
44
+ createdAt: task.created_at ? new Date(task.created_at) : undefined
45
+ }))
46
+ } catch (error) {
47
+ console.error(`Failed to fetch tasks for project ${projectId}:`, error)
48
+ throw new Error(`Failed to fetch tasks: ${error.message}`)
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Get single task by ID
54
+ */
55
+ @Query(returns => LabelStudioTask, {
56
+ description: 'Get a single task by ID',
57
+ nullable: true
58
+ })
59
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
60
+ async labelStudioTask(
61
+ @Arg('taskId', type => Int) taskId: number,
62
+ @Ctx() context: ResolverContext
63
+ ): Promise<LabelStudioTask | null> {
64
+ try {
65
+ const task = await labelStudioApi.getTask(taskId)
66
+
67
+ if (!task) {
68
+ return null
69
+ }
70
+
71
+ return {
72
+ id: task.id,
73
+ data: JSON.stringify(task.data),
74
+ annotationCount: task.annotations?.length || task.total_annotations || 0,
75
+ isCompleted: task.is_labeled || false,
76
+ createdAt: task.created_at ? new Date(task.created_at) : undefined
77
+ }
78
+ } catch (error) {
79
+ console.error(`Failed to fetch task ${taskId}:`, error)
80
+ return null
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Import tasks to Label Studio project
86
+ */
87
+ @Mutation(returns => TaskImportResult, {
88
+ description: 'Import tasks to a Label Studio project in bulk'
89
+ })
90
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
91
+ async importTasksToLabelStudio(
92
+ @Arg('projectId', type => Int) projectId: number,
93
+ @Arg('tasks', type => [TaskDataInput]) tasks: TaskDataInput[],
94
+ @Ctx() context: ResolverContext
95
+ ): Promise<TaskImportResult> {
96
+ const errors: string[] = []
97
+ const taskIds: number[] = []
98
+ let imported = 0
99
+ let failed = 0
100
+
101
+ try {
102
+ // Parse task data
103
+ const parsedTasks = tasks.map(task => {
104
+ try {
105
+ return JSON.parse(task.data)
106
+ } catch (e) {
107
+ failed++
108
+ errors.push(`Invalid JSON in task data: ${task.data}`)
109
+ return null
110
+ }
111
+ })
112
+
113
+ const validTasks = parsedTasks.filter(task => task !== null)
114
+
115
+ if (validTasks.length === 0) {
116
+ return {
117
+ imported: 0,
118
+ failed: tasks.length,
119
+ taskIds: [],
120
+ errors
121
+ }
122
+ }
123
+
124
+ // Import tasks using Label Studio API
125
+ const response = await labelStudioApi.importTasks(projectId, validTasks)
126
+
127
+ // Parse response
128
+ if (response.task_count !== undefined) {
129
+ imported = response.task_count
130
+ } else if (response.annotation_count !== undefined) {
131
+ imported = response.annotation_count
132
+ } else if (Array.isArray(response)) {
133
+ imported = response.length
134
+ taskIds.push(...response.map((t: any) => t.id))
135
+ }
136
+
137
+ return {
138
+ imported,
139
+ failed,
140
+ taskIds,
141
+ errors: errors.length > 0 ? errors : undefined
142
+ }
143
+ } catch (error) {
144
+ console.error(`Failed to import tasks to project ${projectId}:`, error)
145
+ throw new Error(`Failed to import tasks: ${error.message}`)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Delete task
151
+ */
152
+ @Mutation(returns => Boolean, {
153
+ description: 'Delete a task from Label Studio'
154
+ })
155
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
156
+ async deleteLabelStudioTask(
157
+ @Arg('taskId', type => Int) taskId: number,
158
+ @Ctx() context: ResolverContext
159
+ ): Promise<boolean> {
160
+ try {
161
+ await labelStudioApi.deleteTask(taskId)
162
+ return true
163
+ } catch (error) {
164
+ console.error(`Failed to delete task ${taskId}:`, error)
165
+ throw new Error(`Failed to delete task: ${error.message}`)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get annotations for a task
171
+ */
172
+ @Query(returns => [LabelStudioAnnotation], {
173
+ description: 'Get all annotations for a task'
174
+ })
175
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
176
+ async labelStudioTaskAnnotations(
177
+ @Arg('taskId', type => Int) taskId: number,
178
+ @Ctx() context: ResolverContext
179
+ ): Promise<LabelStudioAnnotation[]> {
180
+ try {
181
+ const annotations = await labelStudioApi.getAnnotations(taskId)
182
+
183
+ return annotations.map((annotation: any) => ({
184
+ id: annotation.id,
185
+ taskId: annotation.task || taskId,
186
+ result: JSON.stringify(annotation.result),
187
+ completedBy: annotation.completed_by?.email || 'unknown',
188
+ createdAt: new Date(annotation.created_at),
189
+ leadTime: annotation.lead_time || undefined
190
+ }))
191
+ } catch (error) {
192
+ console.error(`Failed to fetch annotations for task ${taskId}:`, error)
193
+ throw new Error(`Failed to fetch annotations: ${error.message}`)
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Export annotations from project
199
+ */
200
+ @Mutation(returns => ExportResult, {
201
+ description: 'Export annotations from a Label Studio project'
202
+ })
203
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
204
+ async exportLabelStudioAnnotations(
205
+ @Arg('projectId', type => Int) projectId: number,
206
+ @Arg('format', { defaultValue: 'JSON' })
207
+ format: 'JSON' | 'JSON_MIN' | 'CSV' | 'TSV' | 'CONLL2003' | 'COCO' | 'VOC' | 'YOLO',
208
+ @Ctx() context: ResolverContext
209
+ ): Promise<ExportResult> {
210
+ try {
211
+ const exportData = await labelStudioApi.exportAnnotations(projectId, format)
212
+
213
+ // For JSON exports, we get the data directly
214
+ // For file exports, we might get a download URL
215
+ const annotationCount = Array.isArray(exportData) ? exportData.length : 0
216
+
217
+ return {
218
+ exportPath: JSON.stringify(exportData), // In practice, this would be a file path or URL
219
+ annotationCount,
220
+ format
221
+ }
222
+ } catch (error) {
223
+ console.error(`Failed to export annotations from project ${projectId}:`, error)
224
+ throw new Error(`Failed to export annotations: ${error.message}`)
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Sync completed annotations to Things-Factory database
230
+ * This allows storing annotations in TF for further processing
231
+ */
232
+ @Mutation(returns => Int, {
233
+ description: 'Sync completed annotations from Label Studio to Things-Factory database'
234
+ })
235
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
236
+ async syncAnnotationsToDatabase(
237
+ @Arg('projectId', type => Int) projectId: number,
238
+ @Ctx() context: ResolverContext
239
+ ): Promise<number> {
240
+ try {
241
+ const { domain } = context.state
242
+
243
+ // Export annotations
244
+ const annotations = await labelStudioApi.exportAnnotations(projectId, 'JSON')
245
+
246
+ // TODO: Store annotations in Things-Factory database
247
+ // This would typically involve:
248
+ // 1. Creating an Annotation entity
249
+ // 2. Saving each annotation with project reference
250
+ // 3. Linking to Things-Factory business objects if needed
251
+
252
+ // For now, just return count
253
+ const count = Array.isArray(annotations) ? annotations.length : 0
254
+
255
+ console.log(`Synced ${count} annotations from project ${projectId} to database`)
256
+
257
+ return count
258
+ } catch (error) {
259
+ console.error(`Failed to sync annotations from project ${projectId}:`, error)
260
+ throw new Error(`Failed to sync annotations: ${error.message}`)
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Import tasks with flexible data transformation
266
+ * Uses TaskTransformer for mapping source data to Label Studio format
267
+ */
268
+ @Mutation(returns => TaskImportResult, {
269
+ description: 'Import tasks to Label Studio with flexible data transformation'
270
+ })
271
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
272
+ async importTasksWithTransform(
273
+ @Arg('projectId', type => Int) projectId: number,
274
+ @Arg('input') input: ImportTasksWithTransformInput,
275
+ @Ctx() context: ResolverContext
276
+ ): Promise<TaskImportResult> {
277
+ const errors: string[] = []
278
+ const taskIds: number[] = []
279
+ let imported = 0
280
+ let failed = 0
281
+
282
+ try {
283
+ // Parse source data
284
+ const sourceData = JSON.parse(input.sourceData)
285
+ if (!Array.isArray(sourceData)) {
286
+ throw new Error('Source data must be an array')
287
+ }
288
+
289
+ // Parse transform rule
290
+ const dataFields = JSON.parse(input.transformRule.dataFields)
291
+ const predictions = input.transformRule.predictions ? JSON.parse(input.transformRule.predictions) : undefined
292
+ const meta = input.transformRule.meta ? JSON.parse(input.transformRule.meta) : undefined
293
+
294
+ // Transform data using TaskTransformer
295
+ const lsTasks = TaskTransformer.transform(sourceData, {
296
+ dataFields,
297
+ predictions,
298
+ meta
299
+ })
300
+
301
+ // Import to Label Studio
302
+ const response = await labelStudioApi.importTasks(projectId, lsTasks)
303
+
304
+ if (response.task_count) {
305
+ imported = response.task_count
306
+ taskIds.push(...(response.task_ids || []))
307
+ }
308
+
309
+ return {
310
+ imported,
311
+ failed,
312
+ taskIds,
313
+ errors: errors.length > 0 ? errors : undefined
314
+ }
315
+ } catch (error) {
316
+ console.error(`Failed to import tasks with transform for project ${projectId}:`, error)
317
+ throw new Error(`Failed to import tasks: ${error.message}`)
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Export annotations with flexible format conversion
323
+ * Uses AnnotationExporter for custom format support
324
+ */
325
+ @Mutation(returns => AnnotationExportResult, {
326
+ description: 'Export annotations from Label Studio with flexible format conversion'
327
+ })
328
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
329
+ async exportAnnotationsWithFormat(
330
+ @Arg('projectId', type => Int) projectId: number,
331
+ @Arg('input') input: ExportAnnotationsInput,
332
+ @Ctx() context: ResolverContext
333
+ ): Promise<AnnotationExportResult> {
334
+ try {
335
+ // Get annotations from Label Studio
336
+ let annotations = await labelStudioApi.exportAnnotations(projectId, 'JSON')
337
+
338
+ // Filter by task IDs if specified
339
+ if (input.taskIds) {
340
+ const taskIds = JSON.parse(input.taskIds) as number[]
341
+ annotations = annotations.filter((a: any) => taskIds.includes(a.task))
342
+ }
343
+
344
+ // Export using custom format
345
+ const exportedData = await exportAnnotations(annotations, input.format, {
346
+ projectId,
347
+ exportedAt: new Date().toISOString()
348
+ })
349
+
350
+ // Convert to string if not already
351
+ const dataString = typeof exportedData === 'string' ? exportedData : JSON.stringify(exportedData)
352
+
353
+ return {
354
+ data: dataString,
355
+ count: annotations.length,
356
+ format: input.format
357
+ }
358
+ } catch (error) {
359
+ console.error(`Failed to export annotations for project ${projectId}:`, error)
360
+ throw new Error(`Failed to export annotations: ${error.message}`)
361
+ }
362
+ }
363
+ }
@@ -0,0 +1,80 @@
1
+ import { Resolver, Mutation, Ctx, Field, ObjectType, Int, Directive } from 'type-graphql'
2
+ import { UserProvisioningService } from '../../controller/user-provisioning-service.js'
3
+
4
+ @ObjectType({ description: 'User synchronization result' })
5
+ class UserSyncResult {
6
+ @Field()
7
+ success: boolean
8
+
9
+ @Field()
10
+ email: string
11
+
12
+ @Field()
13
+ action: string
14
+
15
+ @Field({ nullable: true })
16
+ lsUserId?: string
17
+
18
+ @Field({ nullable: true, description: 'Label Studio permissions (Admin/Staff/Inactive)' })
19
+ lsPermissions?: string
20
+
21
+ @Field({ nullable: true })
22
+ error?: string
23
+ }
24
+
25
+ @ObjectType({ description: 'User synchronization summary' })
26
+ class UserSyncSummary {
27
+ @Field(type => Int)
28
+ total: number
29
+
30
+ @Field(type => Int)
31
+ created: number
32
+
33
+ @Field(type => Int)
34
+ updated: number
35
+
36
+ @Field(type => Int)
37
+ deactivated: number
38
+
39
+ @Field(type => Int)
40
+ skipped: number
41
+
42
+ @Field(type => Int)
43
+ errors: number
44
+
45
+ @Field(type => [UserSyncResult])
46
+ results: UserSyncResult[]
47
+ }
48
+
49
+ @Resolver()
50
+ export class UserSyncMutation {
51
+ /**
52
+ * 현재 사용자를 Label Studio에 동기화
53
+ */
54
+ @Mutation(returns => UserSyncResult, {
55
+ description: 'Synchronize current user to Label Studio'
56
+ })
57
+ @Directive('@privilege(domainOwnerGranted: true)')
58
+ async syncMyUserToLabelStudio(@Ctx() context: ResolverContext): Promise<UserSyncResult> {
59
+ const { user, domain } = context.state
60
+
61
+ const result = await UserProvisioningService.syncUser(domain, user)
62
+
63
+ return result as UserSyncResult
64
+ }
65
+
66
+ /**
67
+ * 도메인의 모든 사용자를 Label Studio에 일괄 동기화
68
+ */
69
+ @Mutation(returns => UserSyncSummary, {
70
+ description: 'Synchronize all domain users to Label Studio (batch operation)'
71
+ })
72
+ @Directive('@privilege(domainOwnerGranted: true)')
73
+ async syncAllUsersToLabelStudio(@Ctx() context: ResolverContext): Promise<UserSyncSummary> {
74
+ const { domain } = context.state
75
+
76
+ const summary = await UserProvisioningService.syncAllUsers(domain)
77
+
78
+ return summary as UserSyncSummary
79
+ }
80
+ }
@@ -0,0 +1,109 @@
1
+ import { Resolver, Mutation, Query, Arg, Ctx, Int, Directive, Field, ObjectType } from 'type-graphql'
2
+ import { labelStudioApi } from '../../utils/label-studio-api-client.js'
3
+
4
+ @ObjectType({ description: 'Webhook information' })
5
+ export class Webhook {
6
+ @Field(type => Int, { description: 'Webhook ID' })
7
+ id: number
8
+
9
+ @Field({ description: 'Webhook URL' })
10
+ url: string
11
+
12
+ @Field(type => Int, { description: 'Project ID' })
13
+ projectId: number
14
+
15
+ @Field({ description: 'Is webhook active' })
16
+ isActive: boolean
17
+
18
+ @Field({ description: 'Send payload with webhook' })
19
+ sendPayload: boolean
20
+ }
21
+
22
+ @Resolver()
23
+ export class WebhookManagement {
24
+ /**
25
+ * Register webhook for a project
26
+ */
27
+ @Mutation(returns => Webhook, {
28
+ description: 'Register webhook for Label Studio project to receive real-time updates'
29
+ })
30
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
31
+ async registerLabelStudioWebhook(
32
+ @Arg('projectId', type => Int) projectId: number,
33
+ @Ctx() context: ResolverContext
34
+ ): Promise<Webhook> {
35
+ try {
36
+ // Get Things-Factory server URL from environment or request
37
+ const serverUrl = process.env.SERVER_URL || 'http://localhost:3000'
38
+ const webhookUrl = `${serverUrl}/label-studio/webhook`
39
+
40
+ const webhook = await labelStudioApi.createWebhook({
41
+ project: projectId,
42
+ url: webhookUrl,
43
+ send_payload: true,
44
+ send_for_all_actions: true,
45
+ headers: {
46
+ 'Content-Type': 'application/json'
47
+ }
48
+ })
49
+
50
+ return {
51
+ id: webhook.id,
52
+ url: webhook.url,
53
+ projectId: webhook.project,
54
+ isActive: webhook.is_active || true,
55
+ sendPayload: webhook.send_payload || true
56
+ }
57
+ } catch (error) {
58
+ console.error(`Failed to register webhook for project ${projectId}:`, error)
59
+ throw new Error(`Failed to register webhook: ${error.message}`)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get webhooks for a project
65
+ */
66
+ @Query(returns => [Webhook], {
67
+ description: 'Get all webhooks registered for a Label Studio project'
68
+ })
69
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
70
+ async labelStudioWebhooks(
71
+ @Arg('projectId', type => Int) projectId: number,
72
+ @Ctx() context: ResolverContext
73
+ ): Promise<Webhook[]> {
74
+ try {
75
+ const webhooks = await labelStudioApi.getWebhooks(projectId)
76
+
77
+ return webhooks.map(webhook => ({
78
+ id: webhook.id,
79
+ url: webhook.url,
80
+ projectId: webhook.project,
81
+ isActive: webhook.is_active || true,
82
+ sendPayload: webhook.send_payload || true
83
+ }))
84
+ } catch (error) {
85
+ console.error(`Failed to fetch webhooks for project ${projectId}:`, error)
86
+ throw new Error(`Failed to fetch webhooks: ${error.message}`)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Delete webhook
92
+ */
93
+ @Mutation(returns => Boolean, {
94
+ description: 'Delete a webhook'
95
+ })
96
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
97
+ async deleteLabelStudioWebhook(
98
+ @Arg('webhookId', type => Int) webhookId: number,
99
+ @Ctx() context: ResolverContext
100
+ ): Promise<boolean> {
101
+ try {
102
+ await labelStudioApi.deleteWebhook(webhookId)
103
+ return true
104
+ } catch (error) {
105
+ console.error(`Failed to delete webhook ${webhookId}:`, error)
106
+ throw new Error(`Failed to delete webhook: ${error.message}`)
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "strict": false,
5
+ "declaration": true,
6
+ "module": "commonjs",
7
+ "outDir": "../dist-server",
8
+ "baseUrl": "./"
9
+ },
10
+ "include": ["./**/*"]
11
+ }