Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .kiro/specs/wikibase-rest-api-integration/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@
- Write integration tests for cached API operations
- _Requirements: 3.1, 3.2, 3.3_

- [ ] 4. Implement instance configuration management
- [ ] 4.1 Create configuration service and data models
- [x] 4. Implement instance configuration management
- [x] 4.1 Create configuration service and data models
- Implement WikibaseConfigService for managing instance configurations
- Create data models for WikibaseInstanceConfig with validation
- Add pre-defined instance configurations (Wikidata, Wikimedia Commons)
- Write unit tests for configuration validation and management
- _Requirements: 4.1, 4.2, 4.4_

- [ ] 4.2 Add instance validation and connectivity testing
- [x] 4.2 Add instance validation and connectivity testing
- Implement validateInstance method to test API connectivity
- Add health check endpoints for configured instances
- Handle authentication and authorization for different instances
Expand Down
323 changes: 323 additions & 0 deletions backend/src/api/wikibase/instances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import { Elysia, t } from 'elysia'
import { wikibaseConfigService } from '@backend/services/wikibase-config.service'

/**
* Wikibase instance management API endpoints
*/
export const wikibaseInstancesApi = new Elysia({ prefix: '/wikibase/instances' })
.get('/', async () => {
try {
const instances = await wikibaseConfigService.getInstances()
return {
success: true,
data: instances,
}
} catch (error) {
throw new Error(`Failed to get instances: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
Comment on lines +8 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Do not expose authToken in API responses (redact or omit it).

The GET endpoints currently return the full instance object as provided by the service. If authToken is present on an instance, this leaks a credential to any API consumer. Redact or omit the token consistently in:

  • GET /
  • GET /:instanceId
  • GET /default

A minimal redaction approach:

   .get('/', async () => {
     try {
-      const instances = await wikibaseConfigService.getInstances()
+      const instances = await wikibaseConfigService.getInstances()
+      const redacted = instances.map(({ authToken, ...rest }) => rest)
       return {
         success: true,
-        data: instances,
+        data: redacted,
       }
     } catch (error) {
       throw new Error(`Failed to get instances: ${error instanceof Error ? error.message : 'Unknown error'}`)
     }
   }, {

Similarly apply for GET /:instanceId and GET /default before returning the payload.

Also applies to: 26-33, 298-316

}, {
detail: {
summary: 'Get all Wikibase instances',
description: 'Retrieve all configured Wikibase instances (pre-defined and custom)',
tags: ['Wikibase', 'Instances'],
},
})

.get('/:instanceId', async ({ params: { instanceId }, status }) => {
try {
const instance = await wikibaseConfigService.getInstance(instanceId)
return {
success: true,
data: instance,
}
} catch (error) {
return status(404, {
data: [],
errors: [
{
code: 'NOT_FOUND',
message: error instanceof Error ? error.message : 'Instance not found',
details: [instanceId],
},
],
})
}
}, {
params: t.Object({
instanceId: t.String({ description: 'Wikibase instance ID' }),
}),
detail: {
summary: 'Get Wikibase instance by ID',
description: 'Retrieve a specific Wikibase instance configuration',
tags: ['Wikibase', 'Instances'],
},
})

.post('/', async ({ body, status }) => {
try {
await wikibaseConfigService.addInstance(body)
return {
success: true,
message: 'Instance added successfully',
}
} catch (error) {
return status(400, {
data: [],
errors: [
{
code: 'VALIDATION',
message: error instanceof Error ? error.message : 'Failed to add instance',
details: [body],
},
],
})
}
}, {
body: t.Object({
id: t.String({ description: 'Unique instance identifier' }),
name: t.String({ description: 'Human-readable instance name' }),
baseUrl: t.String({ description: 'Base URL for the Wikibase REST API' }),
userAgent: t.String({ description: 'User agent string for API requests' }),
authToken: t.Optional(t.String({ description: 'Authentication token (optional)' })),
isDefault: t.Optional(t.Boolean({ description: 'Whether this is the default instance' })),
metadata: t.Optional(t.Object({
description: t.Optional(t.String()),
language: t.Optional(t.String()),
version: t.Optional(t.String()),
})),
}),
detail: {
Comment on lines +76 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Strengthen input validation (URL format and non-empty constraints).

The schema allows empty strings and arbitrary baseUrl. Enforce stronger constraints to catch issues early at the API boundary.

  • Ensure required strings are non-empty: minLength: 1
  • Validate baseUrl as a proper URL (and optionally constrain to HTTP/S)
  • Optionally restrict id to a safe pattern (e.g., ^[a-z0-9-]+$)

Example:

-  id: t.String({ description: 'Unique instance identifier' }),
-  name: t.String({ description: 'Human-readable instance name' }),
-  baseUrl: t.String({ description: 'Base URL for the Wikibase REST API' }),
-  userAgent: t.String({ description: 'User agent string for API requests' }),
+  id: t.String({ description: 'Unique instance identifier', minLength: 1, pattern: '^[a-z0-9-]+$' }),
+  name: t.String({ description: 'Human-readable instance name', minLength: 1 }),
+  baseUrl: t.String({ description: 'Base URL for the Wikibase REST API', format: 'uri' }),
+  userAgent: t.String({ description: 'User agent string for API requests', minLength: 1 }),

Apply similar constraints to the /validate body schema as well.

Also applies to: 212-224

🤖 Prompt for AI Agents
In backend/src/api/wikibase/instances.ts around lines 76 to 89 (and similarly
update the /validate body schema at lines ~212-224), the current t.String()
fields allow empty strings and baseUrl is not validated; update the schema to
require non-empty strings (add minLength: 1) for id, name, userAgent and
metadata description/language/version, validate baseUrl as a proper URL
(restrict to http/https) using a URL pattern or a dedicated URL type, and
tighten id with a safe regex like ^[a-z0-9-]+$ (or equivalent pattern) to
prevent invalid identifiers; apply the same constraints to the duplicate
/validate schema.

summary: 'Add custom Wikibase instance',
description: 'Add a new custom Wikibase instance configuration',
tags: ['Wikibase', 'Instances'],
},
})

.put('/:instanceId', async ({ params: { instanceId }, body, status }) => {
try {
await wikibaseConfigService.updateInstance(instanceId, body)
return {
success: true,
message: 'Instance updated successfully',
}
} catch (error) {
return status(400, {
data: [],
errors: [
{
code: 'VALIDATION',
message: error instanceof Error ? error.message : 'Failed to update instance',
details: [instanceId, body],
},
],
})
}
}, {
params: t.Object({
instanceId: t.String({ description: 'Wikibase instance ID' }),
}),
body: t.Partial(t.Object({
name: t.String({ description: 'Human-readable instance name' }),
baseUrl: t.String({ description: 'Base URL for the Wikibase REST API' }),
userAgent: t.String({ description: 'User agent string for API requests' }),
authToken: t.String({ description: 'Authentication token' }),
isDefault: t.Boolean({ description: 'Whether this is the default instance' }),
metadata: t.Object({
description: t.Optional(t.String()),
language: t.Optional(t.String()),
version: t.Optional(t.String()),
}),
})),
detail: {
summary: 'Update Wikibase instance',
description: 'Update an existing custom Wikibase instance configuration',
tags: ['Wikibase', 'Instances'],
},
})

.delete('/:instanceId', async ({ params: { instanceId }, status }) => {
try {
await wikibaseConfigService.removeInstance(instanceId)
return {
success: true,
message: 'Instance removed successfully',
}
} catch (error) {
return status(400, {
data: [],
errors: [
{
code: 'VALIDATION',
message: error instanceof Error ? error.message : 'Failed to remove instance',
details: [instanceId],
},
],
})
}
}, {
params: t.Object({
instanceId: t.String({ description: 'Wikibase instance ID' }),
}),
detail: {
summary: 'Remove custom Wikibase instance',
description: 'Remove a custom Wikibase instance configuration',
tags: ['Wikibase', 'Instances'],
},
})

.post('/:instanceId/validate', async ({ params: { instanceId }, status }) => {
try {
const instance = await wikibaseConfigService.getInstance(instanceId)
const validation = await wikibaseConfigService.validateInstanceWithConnectivity(instance)

return {
success: true,
data: validation,
}
} catch (error) {
return status(404, {
data: [],
errors: [
{
code: 'NOT_FOUND',
message: error instanceof Error ? error.message : 'Instance not found',
details: [instanceId],
},
],
})
}
}, {
params: t.Object({
instanceId: t.String({ description: 'Wikibase instance ID' }),
}),
detail: {
summary: 'Validate Wikibase instance',
description: 'Validate instance configuration and test connectivity',
tags: ['Wikibase', 'Instances', 'Validation'],
},
})

.post('/validate', async ({ body }) => {
try {
const validation = await wikibaseConfigService.validateInstanceWithConnectivity(body)

return {
success: true,
data: validation,
}
} catch (error) {
throw new Error(`Failed to validate instance configuration: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}, {
body: t.Object({
id: t.String({ description: 'Unique instance identifier' }),
name: t.String({ description: 'Human-readable instance name' }),
baseUrl: t.String({ description: 'Base URL for the Wikibase REST API' }),
userAgent: t.String({ description: 'User agent string for API requests' }),
authToken: t.Optional(t.String({ description: 'Authentication token (optional)' })),
isDefault: t.Optional(t.Boolean({ description: 'Whether this is the default instance' })),
metadata: t.Optional(t.Object({
description: t.Optional(t.String()),
language: t.Optional(t.String()),
version: t.Optional(t.String()),
})),
}),
detail: {
summary: 'Validate instance configuration',
description: 'Validate a Wikibase instance configuration and test connectivity without saving',
tags: ['Wikibase', 'Instances', 'Validation'],
},
})

.get('/:instanceId/health', async ({ params: { instanceId } }) => {
try {
const healthCheck = await wikibaseConfigService.performHealthCheck(instanceId)

return {
success: true,
data: healthCheck,
}
} catch (error) {
// Health check should return the result even if the instance doesn't exist
// The health check itself will indicate the failure
const healthCheck = {
instanceId,
isHealthy: false,
lastChecked: new Date(),
responseTime: 0,
error: error instanceof Error ? error.message : 'Health check failed',
}

return {
success: true,
data: healthCheck,
}
}
}, {
params: t.Object({
instanceId: t.String({ description: 'Wikibase instance ID' }),
}),
detail: {
summary: 'Health check for Wikibase instance',
description: 'Perform a health check on a configured Wikibase instance',
tags: ['Wikibase', 'Instances', 'Health'],
},
})

.post('/:instanceId/set-default', async ({ params: { instanceId }, status }) => {
try {
await wikibaseConfigService.setDefaultInstance(instanceId)

return {
success: true,
message: 'Default instance set successfully',
}
} catch (error) {
return status(404, {
data: [],
errors: [
{
code: 'NOT_FOUND',
message: error instanceof Error ? error.message : 'Instance not found',
details: [instanceId],
},
],
})
}
}, {
params: t.Object({
instanceId: t.String({ description: 'Wikibase instance ID' }),
}),
detail: {
summary: 'Set default Wikibase instance',
description: 'Set a Wikibase instance as the default for new operations',
tags: ['Wikibase', 'Instances'],
},
})

.get('/default', async () => {
try {
const defaultInstance = await wikibaseConfigService.getDefaultInstance()

if (!defaultInstance) {
return {
success: true,
data: null,
message: 'No default instance configured',
}
}

return {
success: true,
data: defaultInstance,
}
} catch (error) {
throw new Error(`Failed to get default instance: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}, {
detail: {
summary: 'Get default Wikibase instance',
description: 'Retrieve the currently configured default Wikibase instance',
tags: ['Wikibase', 'Instances'],
},
})
2 changes: 2 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { projectRoutes } from '@backend/api/project'
import { metaProjectsRoutes } from '@backend/api/_meta_projects'
import { closeDb } from '@backend/plugins/database'
import { wikibaseRoutes } from './api/project/project.wikibase'
import { wikibaseInstancesApi } from './api/wikibase/instances'

export const elysiaApp = new Elysia({
serve: {
Expand All @@ -33,6 +34,7 @@ export const elysiaApp = new Elysia({
.use(metaProjectsRoutes)
.use(projectRoutes)
.use(wikibaseRoutes)
.use(wikibaseInstancesApi)
.listen(3000, () => {
console.log('🦊 Elysia is running at http://localhost:3000')
})
Expand Down
Loading
Loading