@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.
- package/CHANGELOG.md +85 -0
- package/EXTERNAL_DATA_SOURCING.md +484 -0
- package/IMPLEMENTATION_GUIDE.md +469 -0
- package/INTEGRATION.md +279 -0
- package/README.md +1014 -0
- package/SETUP_GUIDE.md +577 -0
- package/TEST_GUIDE.md +387 -0
- package/UI_CUSTOMIZATION.md +395 -0
- package/USER_SYNC_GUIDE.md +514 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +1 -0
- package/client/label-studio-label-page.ts +52 -0
- package/client/label-studio-project-create.ts +216 -0
- package/client/label-studio-project-list.ts +214 -0
- package/client/label-studio-wrapper.ts +294 -0
- package/client/route.ts +15 -0
- package/client/tsconfig.json +13 -0
- package/config/config.development.js +124 -0
- package/config/config.production.js +182 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/label-studio-label-page.d.ts +8 -0
- package/dist-client/label-studio-label-page.js +54 -0
- package/dist-client/label-studio-label-page.js.map +1 -0
- package/dist-client/label-studio-project-create.d.ts +16 -0
- package/dist-client/label-studio-project-create.js +235 -0
- package/dist-client/label-studio-project-create.js.map +1 -0
- package/dist-client/label-studio-project-list.d.ts +16 -0
- package/dist-client/label-studio-project-list.js +222 -0
- package/dist-client/label-studio-project-list.js.map +1 -0
- package/dist-client/label-studio-wrapper.d.ts +57 -0
- package/dist-client/label-studio-wrapper.js +304 -0
- package/dist-client/label-studio-wrapper.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +14 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
- package/dist-server/controller/label-studio-role-mapper.js +65 -0
- package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
- package/dist-server/controller/user-provisioning-service.d.ts +66 -0
- package/dist-server/controller/user-provisioning-service.js +264 -0
- package/dist-server/controller/user-provisioning-service.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +19 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/route/label-studio-sso.d.ts +2 -0
- package/dist-server/route/label-studio-sso.js +156 -0
- package/dist-server/route/label-studio-sso.js.map +1 -0
- package/dist-server/route/webhook.d.ts +65 -0
- package/dist-server/route/webhook.js +248 -0
- package/dist-server/route/webhook.js.map +1 -0
- package/dist-server/route.d.ts +1 -0
- package/dist-server/route.js +21 -0
- package/dist-server/route.js.map +1 -0
- package/dist-server/service/ai-prediction-service.d.ts +27 -0
- package/dist-server/service/ai-prediction-service.js +222 -0
- package/dist-server/service/ai-prediction-service.js.map +1 -0
- package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
- package/dist-server/service/dataset-labeling-integration.js +512 -0
- package/dist-server/service/dataset-labeling-integration.js.map +1 -0
- package/dist-server/service/external-data-source-service.d.ts +78 -0
- package/dist-server/service/external-data-source-service.js +415 -0
- package/dist-server/service/external-data-source-service.js.map +1 -0
- package/dist-server/service/index.d.ts +12 -0
- package/dist-server/service/index.js +27 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/label-studio-sso-service.d.ts +38 -0
- package/dist-server/service/label-studio-sso-service.js +98 -0
- package/dist-server/service/label-studio-sso-service.js.map +1 -0
- package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
- package/dist-server/service/ml/ml-backend-service.js +153 -0
- package/dist-server/service/ml/ml-backend-service.js.map +1 -0
- package/dist-server/service/prediction/prediction-management.d.ts +32 -0
- package/dist-server/service/prediction/prediction-management.js +299 -0
- package/dist-server/service/prediction/prediction-management.js.map +1 -0
- package/dist-server/service/project/project-management.d.ts +36 -0
- package/dist-server/service/project/project-management.js +309 -0
- package/dist-server/service/project/project-management.js.map +1 -0
- package/dist-server/service/task/task-management.d.ts +42 -0
- package/dist-server/service/task/task-management.js +372 -0
- package/dist-server/service/task/task-management.js.map +1 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
- package/dist-server/service/webhook/webhook-management.d.ts +21 -0
- package/dist-server/service/webhook/webhook-management.js +134 -0
- package/dist-server/service/webhook/webhook-management.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/types/dataset-labeling-types.d.ts +71 -0
- package/dist-server/types/dataset-labeling-types.js +259 -0
- package/dist-server/types/dataset-labeling-types.js.map +1 -0
- package/dist-server/types/label-studio-types.d.ts +128 -0
- package/dist-server/types/label-studio-types.js +494 -0
- package/dist-server/types/label-studio-types.js.map +1 -0
- package/dist-server/types/prediction-types.d.ts +39 -0
- package/dist-server/types/prediction-types.js +121 -0
- package/dist-server/types/prediction-types.js.map +1 -0
- package/dist-server/utils/annotation-exporter.d.ts +104 -0
- package/dist-server/utils/annotation-exporter.js +261 -0
- package/dist-server/utils/annotation-exporter.js.map +1 -0
- package/dist-server/utils/label-config-builder.d.ts +117 -0
- package/dist-server/utils/label-config-builder.js +286 -0
- package/dist-server/utils/label-config-builder.js.map +1 -0
- package/dist-server/utils/label-studio-api-client.d.ts +180 -0
- package/dist-server/utils/label-studio-api-client.js +401 -0
- package/dist-server/utils/label-studio-api-client.js.map +1 -0
- package/dist-server/utils/media-url-extractor.d.ts +45 -0
- package/dist-server/utils/media-url-extractor.js +152 -0
- package/dist-server/utils/media-url-extractor.js.map +1 -0
- package/dist-server/utils/task-transformer.d.ts +108 -0
- package/dist-server/utils/task-transformer.js +260 -0
- package/dist-server/utils/task-transformer.js.map +1 -0
- package/package.json +47 -0
- package/server/SERVER_STRUCTURE.md +351 -0
- package/server/controller/label-studio-role-mapper.ts +76 -0
- package/server/controller/user-provisioning-service.ts +340 -0
- package/server/index.ts +19 -0
- package/server/route/label-studio-sso.ts +194 -0
- package/server/route/webhook.ts +304 -0
- package/server/route.ts +35 -0
- package/server/service/ai-prediction-service.ts +239 -0
- package/server/service/dataset-labeling-integration.ts +590 -0
- package/server/service/external-data-source-service.ts +438 -0
- package/server/service/index.ts +24 -0
- package/server/service/label-studio-sso-service.ts +108 -0
- package/server/service/labeling-scenario-service.ts.deprecated +566 -0
- package/server/service/ml/ml-backend-service.ts +127 -0
- package/server/service/prediction/prediction-management.ts +281 -0
- package/server/service/project/project-management.ts +284 -0
- package/server/service/task/task-management.ts +363 -0
- package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
- package/server/service/webhook/webhook-management.ts +109 -0
- package/server/tsconfig.json +11 -0
- package/server/types/dataset-labeling-types.ts +181 -0
- package/server/types/global.d.ts +23 -0
- package/server/types/label-studio-types.ts +346 -0
- package/server/types/prediction-types.ts +86 -0
- package/server/types/scenario-types.ts.deprecated +362 -0
- package/server/utils/annotation-exporter.ts +340 -0
- package/server/utils/label-config-builder.ts +340 -0
- package/server/utils/label-studio-api-client.ts +487 -0
- package/server/utils/media-url-extractor.ts +193 -0
- package/server/utils/task-transformer.ts +342 -0
- package/test-ai-prediction.js +268 -0
- package/test-dataset-integration.js +449 -0
- package/test-simple.js +89 -0
- 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
|
+
}
|