@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,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
|
+
}
|