@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,438 @@
1
+ import { Resolver, Mutation, Query, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { Field, InputType, ObjectType } from 'type-graphql'
3
+ import axios from 'axios'
4
+ import { labelStudioApi } from '../utils/label-studio-api-client.js'
5
+
6
+ /**
7
+ * External Data Source Service
8
+ *
9
+ * Import data from external sources into Label Studio
10
+ * Supports:
11
+ * - REST API endpoints
12
+ * - S3/Cloud Storage URLs
13
+ * - Database queries (via Things Factory connections)
14
+ * - CSV/JSON file URLs
15
+ */
16
+
17
+ // ============================================================================
18
+ // Type Definitions
19
+ // ============================================================================
20
+
21
+ @InputType()
22
+ class ExternalDataSourceConfig {
23
+ @Field({ description: 'Data source type' })
24
+ sourceType: 'api' | 'url' | 's3' | 'database' | 'csv' | 'json'
25
+
26
+ @Field({ description: 'Source URL or endpoint' })
27
+ sourceUrl: string
28
+
29
+ @Field({ nullable: true, description: 'Authentication header if required' })
30
+ authHeader?: string
31
+
32
+ @Field({ nullable: true, description: 'HTTP method for API calls' })
33
+ httpMethod?: string
34
+
35
+ @Field({ nullable: true, description: 'Request body for POST/PUT' })
36
+ requestBody?: string
37
+
38
+ @Field({ nullable: true, description: 'JSONPath or XPath for data extraction' })
39
+ dataPath?: string
40
+ }
41
+
42
+ @InputType()
43
+ class ImportFromExternalSourceRequest {
44
+ @Field(type => Int, { description: 'Label Studio project ID' })
45
+ projectId: number
46
+
47
+ @Field(type => ExternalDataSourceConfig, { description: 'External data source configuration' })
48
+ source: ExternalDataSourceConfig
49
+
50
+ @Field({ nullable: true, description: 'Field mapping JSON (source -> Label Studio)' })
51
+ fieldMapping?: string
52
+
53
+ @Field({ nullable: true, description: 'Image field name in source data' })
54
+ imageField?: string
55
+
56
+ @Field({ nullable: true, defaultValue: false, description: 'Auto-generate AI predictions' })
57
+ autoGeneratePredictions?: boolean
58
+
59
+ @Field({ nullable: true, description: 'Maximum number of items to import' })
60
+ limit?: number
61
+ }
62
+
63
+ @ObjectType()
64
+ class ExternalImportResult {
65
+ @Field(type => Int, { description: 'Total items fetched from source' })
66
+ totalFetched: number
67
+
68
+ @Field(type => Int, { description: 'Tasks successfully imported' })
69
+ tasksImported: number
70
+
71
+ @Field(type => Int, { description: 'Tasks failed to import' })
72
+ tasksFailed: number
73
+
74
+ @Field(type => [Int], { description: 'Imported task IDs' })
75
+ taskIds: number[]
76
+
77
+ @Field({ nullable: true, description: 'Error message if any' })
78
+ error?: string
79
+ }
80
+
81
+ @ObjectType()
82
+ class DataSourcePreview {
83
+ @Field(type => Int, { description: 'Number of items available' })
84
+ itemCount: number
85
+
86
+ @Field({ description: 'Sample data (first item)' })
87
+ sampleData: string
88
+
89
+ @Field({ description: 'Detected schema/fields' })
90
+ schema: string
91
+
92
+ @Field({ nullable: true, description: 'Preview error if any' })
93
+ error?: string
94
+ }
95
+
96
+ // ============================================================================
97
+ // Resolver
98
+ // ============================================================================
99
+
100
+ @Resolver()
101
+ export class ExternalDataSourceService {
102
+ /**
103
+ * Preview external data source before import
104
+ */
105
+ @Query(returns => DataSourcePreview, {
106
+ description: 'Preview external data source to verify connection and data structure'
107
+ })
108
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
109
+ async previewExternalDataSource(
110
+ @Arg('source', type => ExternalDataSourceConfig) source: ExternalDataSourceConfig,
111
+ @Ctx() context: ResolverContext
112
+ ): Promise<DataSourcePreview> {
113
+ console.log(`[External Data] Previewing source: ${source.sourceType} - ${source.sourceUrl}`)
114
+
115
+ try {
116
+ const data = await this.fetchFromSource(source, 1)
117
+
118
+ if (!data || data.length === 0) {
119
+ return {
120
+ itemCount: 0,
121
+ sampleData: '{}',
122
+ schema: '{}',
123
+ error: 'No data found'
124
+ }
125
+ }
126
+
127
+ const firstItem = data[0]
128
+ const schema = this.detectSchema(firstItem)
129
+
130
+ return {
131
+ itemCount: data.length,
132
+ sampleData: JSON.stringify(firstItem, null, 2),
133
+ schema: JSON.stringify(schema, null, 2)
134
+ }
135
+ } catch (error) {
136
+ console.error('[External Data] Preview failed:', error)
137
+ return {
138
+ itemCount: 0,
139
+ sampleData: '{}',
140
+ schema: '{}',
141
+ error: error.message
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Import data from external source to Label Studio
148
+ */
149
+ @Mutation(returns => ExternalImportResult, {
150
+ description: 'Import data from external source into Label Studio project'
151
+ })
152
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
153
+ async importFromExternalSource(
154
+ @Arg('input') input: ImportFromExternalSourceRequest,
155
+ @Ctx() context: ResolverContext
156
+ ): Promise<ExternalImportResult> {
157
+ console.log(`[External Data] Importing from ${input.source.sourceType}: ${input.source.sourceUrl}`)
158
+
159
+ try {
160
+ // 1. Fetch data from external source
161
+ const rawData = await this.fetchFromSource(input.source, input.limit)
162
+
163
+ console.log(`[External Data] Fetched ${rawData.length} items`)
164
+
165
+ if (rawData.length === 0) {
166
+ return {
167
+ totalFetched: 0,
168
+ tasksImported: 0,
169
+ tasksFailed: 0,
170
+ taskIds: [],
171
+ error: 'No data fetched from source'
172
+ }
173
+ }
174
+
175
+ // 2. Transform data to Label Studio format
176
+ const fieldMapping = input.fieldMapping ? JSON.parse(input.fieldMapping) : {}
177
+ const imageField = input.imageField || 'image'
178
+
179
+ const tasks = rawData.map(item => this.transformToLabelStudioTask(item, fieldMapping, imageField))
180
+
181
+ // 3. Import tasks to Label Studio
182
+ const result: ExternalImportResult = {
183
+ totalFetched: rawData.length,
184
+ tasksImported: 0,
185
+ tasksFailed: 0,
186
+ taskIds: []
187
+ }
188
+
189
+ // Import in batches
190
+ const BATCH_SIZE = 50
191
+ for (let i = 0; i < tasks.length; i += BATCH_SIZE) {
192
+ const batch = tasks.slice(i, i + BATCH_SIZE)
193
+
194
+ try {
195
+ const importResult = await labelStudioApi.importTasks(input.projectId, batch)
196
+
197
+ // Label Studio import returns task IDs
198
+ if (importResult.task_count) {
199
+ result.tasksImported += importResult.task_count
200
+ } else if (Array.isArray(importResult)) {
201
+ result.tasksImported += importResult.length
202
+ result.taskIds.push(...importResult.map((t: any) => t.id))
203
+ }
204
+
205
+ console.log(`[External Data] Imported batch ${i / BATCH_SIZE + 1}: ${batch.length} tasks`)
206
+ } catch (batchError) {
207
+ console.error(`[External Data] Batch import failed:`, batchError)
208
+ result.tasksFailed += batch.length
209
+ }
210
+ }
211
+
212
+ console.log(
213
+ `[External Data] Import completed: ${result.tasksImported} imported, ${result.tasksFailed} failed`
214
+ )
215
+
216
+ return result
217
+ } catch (error) {
218
+ console.error('[External Data] Import failed:', error)
219
+ throw new Error(`Failed to import from external source: ${error.message}`)
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Fetch data from external source
225
+ */
226
+ private async fetchFromSource(
227
+ source: ExternalDataSourceConfig,
228
+ limit?: number
229
+ ): Promise<any[]> {
230
+ switch (source.sourceType) {
231
+ case 'api':
232
+ return await this.fetchFromApi(source, limit)
233
+
234
+ case 'url':
235
+ case 'json':
236
+ return await this.fetchFromUrl(source, limit)
237
+
238
+ case 'csv':
239
+ return await this.fetchFromCsv(source, limit)
240
+
241
+ case 's3':
242
+ throw new Error('S3 source not yet implemented. Use URL with S3 presigned URL instead.')
243
+
244
+ case 'database':
245
+ throw new Error('Database source not yet implemented. Use API endpoint instead.')
246
+
247
+ default:
248
+ throw new Error(`Unsupported source type: ${source.sourceType}`)
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Fetch from REST API
254
+ */
255
+ private async fetchFromApi(source: ExternalDataSourceConfig, limit?: number): Promise<any[]> {
256
+ const method = (source.httpMethod || 'GET').toUpperCase()
257
+ const headers: any = {
258
+ 'Content-Type': 'application/json'
259
+ }
260
+
261
+ if (source.authHeader) {
262
+ headers['Authorization'] = source.authHeader
263
+ }
264
+
265
+ const config: any = {
266
+ method,
267
+ url: source.sourceUrl,
268
+ headers,
269
+ timeout: 30000
270
+ }
271
+
272
+ if (method === 'POST' || method === 'PUT') {
273
+ config.data = source.requestBody ? JSON.parse(source.requestBody) : {}
274
+ }
275
+
276
+ const response = await axios(config)
277
+
278
+ let data = response.data
279
+
280
+ // Extract data using JSONPath if specified
281
+ if (source.dataPath) {
282
+ data = this.extractDataByPath(data, source.dataPath)
283
+ }
284
+
285
+ // Ensure data is an array
286
+ if (!Array.isArray(data)) {
287
+ data = [data]
288
+ }
289
+
290
+ // Apply limit
291
+ if (limit && data.length > limit) {
292
+ data = data.slice(0, limit)
293
+ }
294
+
295
+ return data
296
+ }
297
+
298
+ /**
299
+ * Fetch from URL (JSON file)
300
+ */
301
+ private async fetchFromUrl(source: ExternalDataSourceConfig, limit?: number): Promise<any[]> {
302
+ const headers: any = {}
303
+
304
+ if (source.authHeader) {
305
+ headers['Authorization'] = source.authHeader
306
+ }
307
+
308
+ const response = await axios.get(source.sourceUrl, { headers })
309
+
310
+ let data = response.data
311
+
312
+ // Extract data using JSONPath if specified
313
+ if (source.dataPath) {
314
+ data = this.extractDataByPath(data, source.dataPath)
315
+ }
316
+
317
+ // Ensure data is an array
318
+ if (!Array.isArray(data)) {
319
+ data = [data]
320
+ }
321
+
322
+ // Apply limit
323
+ if (limit && data.length > limit) {
324
+ data = data.slice(0, limit)
325
+ }
326
+
327
+ return data
328
+ }
329
+
330
+ /**
331
+ * Fetch from CSV
332
+ */
333
+ private async fetchFromCsv(source: ExternalDataSourceConfig, limit?: number): Promise<any[]> {
334
+ const headers: any = {}
335
+
336
+ if (source.authHeader) {
337
+ headers['Authorization'] = source.authHeader
338
+ }
339
+
340
+ const response = await axios.get(source.sourceUrl, { headers })
341
+ const csvText = response.data
342
+
343
+ // Simple CSV parsing (for production, use a proper CSV library)
344
+ const lines = csvText.trim().split('\n')
345
+ const headerLine = lines[0]
346
+ const headers_csv = headerLine.split(',').map((h: string) => h.trim())
347
+
348
+ const data = []
349
+ const maxLines = limit ? Math.min(lines.length, limit + 1) : lines.length
350
+
351
+ for (let i = 1; i < maxLines; i++) {
352
+ const line = lines[i]
353
+ const values = line.split(',').map((v: string) => v.trim())
354
+
355
+ const obj: any = {}
356
+ headers_csv.forEach((header, index) => {
357
+ obj[header] = values[index]
358
+ })
359
+
360
+ data.push(obj)
361
+ }
362
+
363
+ return data
364
+ }
365
+
366
+ /**
367
+ * Extract data by JSON path (simple implementation)
368
+ */
369
+ private extractDataByPath(data: any, path: string): any {
370
+ const parts = path.split('.')
371
+ let current = data
372
+
373
+ for (const part of parts) {
374
+ if (current && typeof current === 'object' && part in current) {
375
+ current = current[part]
376
+ } else {
377
+ throw new Error(`Path "${path}" not found in data`)
378
+ }
379
+ }
380
+
381
+ return current
382
+ }
383
+
384
+ /**
385
+ * Transform raw data item to Label Studio task format
386
+ */
387
+ private transformToLabelStudioTask(
388
+ item: any,
389
+ fieldMapping: Record<string, string>,
390
+ imageField: string
391
+ ): any {
392
+ // Apply field mapping
393
+ const mappedData: any = {}
394
+
395
+ if (Object.keys(fieldMapping).length > 0) {
396
+ // Use explicit mapping
397
+ for (const [sourceField, targetField] of Object.entries(fieldMapping)) {
398
+ if (item[sourceField] !== undefined) {
399
+ mappedData[targetField] = item[sourceField]
400
+ }
401
+ }
402
+ } else {
403
+ // No mapping, use data as-is
404
+ Object.assign(mappedData, item)
405
+ }
406
+
407
+ // Ensure image field exists
408
+ const imageUrl = mappedData[imageField] || item[imageField] || item.url || item.image_url
409
+
410
+ if (!imageUrl) {
411
+ console.warn('[External Data] No image URL found in item:', item)
412
+ }
413
+
414
+ return {
415
+ data: {
416
+ image: imageUrl,
417
+ ...mappedData
418
+ },
419
+ meta: {
420
+ source: 'external-import',
421
+ originalData: JSON.stringify(item)
422
+ }
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Detect schema from data item
428
+ */
429
+ private detectSchema(item: any): Record<string, string> {
430
+ const schema: Record<string, string> = {}
431
+
432
+ for (const [key, value] of Object.entries(item)) {
433
+ schema[key] = typeof value
434
+ }
435
+
436
+ return schema
437
+ }
438
+ }
@@ -0,0 +1,24 @@
1
+ import { UserSyncMutation } from './user-provisioning/user-sync-mutation.js'
2
+ import { ProjectManagement } from './project/project-management.js'
3
+ import { TaskManagement } from './task/task-management.js'
4
+ import { WebhookManagement } from './webhook/webhook-management.js'
5
+ import { MLBackendService } from './ml/ml-backend-service.js'
6
+ import { PredictionManagement } from './prediction/prediction-management.js'
7
+ import { LabelStudioAIPredictionService } from './ai-prediction-service.js'
8
+ import { DatasetLabelingIntegration } from './dataset-labeling-integration.js'
9
+ import { ExternalDataSourceService } from './external-data-source-service.js'
10
+
11
+ export const schema = {
12
+ resolverClasses: [
13
+ /* RESOLVER CLASSES */
14
+ UserSyncMutation,
15
+ ProjectManagement,
16
+ TaskManagement,
17
+ WebhookManagement,
18
+ MLBackendService,
19
+ PredictionManagement,
20
+ LabelStudioAIPredictionService,
21
+ DatasetLabelingIntegration,
22
+ ExternalDataSourceService
23
+ ]
24
+ }
@@ -0,0 +1,108 @@
1
+ import axios from 'axios'
2
+ import { config } from '@things-factory/env'
3
+
4
+ /**
5
+ * Label Studio SSO Service
6
+ *
7
+ * Handles JWT token acquisition from Label Studio for SSO authentication.
8
+ * The client application requests JWT tokens from Label Studio's token API,
9
+ * then uses cookie-based authentication to automatically log users in.
10
+ */
11
+ export class LabelStudioSSOService {
12
+ /**
13
+ * Get Label Studio SSO configuration
14
+ */
15
+ private static getConfig() {
16
+ const labelStudioConfig = config.get('labelStudio', {
17
+ serverUrl: 'http://localhost:8080',
18
+ apiToken: ''
19
+ })
20
+
21
+ return {
22
+ serverUrl: labelStudioConfig.serverUrl,
23
+ apiToken: labelStudioConfig.apiToken
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Request JWT token from Label Studio for SSO authentication
29
+ *
30
+ * @param email User email address
31
+ * @returns JWT token and expiry time, or null if failed
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const tokenData = await LabelStudioSSOService.getSSOToken('user@example.com')
36
+ * if (tokenData) {
37
+ * console.log('Token:', tokenData.token)
38
+ * console.log('Expires in:', tokenData.expires_in, 'seconds')
39
+ * }
40
+ * ```
41
+ */
42
+ static async getSSOToken(email: string): Promise<{ token: string; expires_in: number } | null> {
43
+ const { serverUrl, apiToken } = this.getConfig()
44
+
45
+ if (!apiToken) {
46
+ console.error('[LSS SSO] Label Studio API token not configured')
47
+ return null
48
+ }
49
+
50
+ if (!email) {
51
+ console.error('[LSS SSO] Email is required')
52
+ return null
53
+ }
54
+
55
+ try {
56
+ console.log(`[LSS SSO] Requesting JWT token for: ${email}`)
57
+
58
+ const response = await axios.post(
59
+ `${serverUrl}/api/sso/token`,
60
+ { email },
61
+ {
62
+ headers: {
63
+ 'Authorization': `Token ${apiToken}`,
64
+ 'Content-Type': 'application/json'
65
+ },
66
+ timeout: 5000
67
+ }
68
+ )
69
+
70
+ const { token, expires_in } = response.data
71
+
72
+ console.log(`[LSS SSO] JWT token acquired for ${email} (expires in ${expires_in}s)`)
73
+
74
+ return { token, expires_in }
75
+ } catch (error: any) {
76
+ if (error.response) {
77
+ console.error(`[LSS SSO] Label Studio API error: ${error.response.status}`, error.response.data)
78
+ } else if (error.request) {
79
+ console.error('[LSS SSO] No response from Label Studio:', error.message)
80
+ } else {
81
+ console.error('[LSS SSO] Request error:', error.message)
82
+ }
83
+
84
+ return null
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Verify SSO configuration is properly set up
90
+ *
91
+ * @returns True if configuration is valid
92
+ */
93
+ static verifyConfig(): boolean {
94
+ const { serverUrl, apiToken } = this.getConfig()
95
+
96
+ if (!serverUrl) {
97
+ console.error('[LSS SSO] Label Studio server URL not configured')
98
+ return false
99
+ }
100
+
101
+ if (!apiToken) {
102
+ console.error('[LSS SSO] Label Studio API token not configured')
103
+ return false
104
+ }
105
+
106
+ return true
107
+ }
108
+ }