@vfarcic/dot-ai 0.165.0 → 0.167.0
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/README.md +6 -0
- package/dist/core/resource-vector-service.d.ts +136 -0
- package/dist/core/resource-vector-service.d.ts.map +1 -0
- package/dist/core/resource-vector-service.js +274 -0
- package/dist/core/vector-db-service.d.ts +1 -0
- package/dist/core/vector-db-service.d.ts.map +1 -1
- package/dist/core/vector-db-service.js +26 -10
- package/dist/interfaces/resource-sync-handler.d.ts +41 -0
- package/dist/interfaces/resource-sync-handler.d.ts.map +1 -0
- package/dist/interfaces/resource-sync-handler.js +408 -0
- package/dist/interfaces/rest-api.d.ts +6 -1
- package/dist/interfaces/rest-api.d.ts.map +1 -1
- package/dist/interfaces/rest-api.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,12 @@ Get started in 3 steps:
|
|
|
105
105
|
- **[MCP Setup Guide](docs/setup/mcp-setup.md)** - Complete configuration instructions
|
|
106
106
|
- **[Tools Overview](docs/guides/mcp-tools-overview.md)** - All available tools and features
|
|
107
107
|
|
|
108
|
+
### Deployment Options
|
|
109
|
+
- **[Docker Setup](docs/setup/docker-setup.md)** - Recommended for local development
|
|
110
|
+
- **[Kubernetes Setup](docs/setup/kubernetes-setup.md)** - Production deployment with Ingress
|
|
111
|
+
- **[ToolHive Setup](docs/setup/kubernetes-toolhive-setup.md)** - Operator-managed deployment
|
|
112
|
+
- **[NPX Setup](docs/setup/npx-setup.md)** - Quick trials with Node.js
|
|
113
|
+
|
|
108
114
|
### Feature Guides
|
|
109
115
|
- **[Resource Provisioning](docs/guides/mcp-recommendation-guide.md)** - AI-powered deployment recommendations
|
|
110
116
|
- **[Capability Management](docs/guides/mcp-capability-management-guide.md)** - Semantic resource discovery
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Vector Service
|
|
3
|
+
*
|
|
4
|
+
* Vector-based storage and retrieval for Kubernetes cluster resources.
|
|
5
|
+
* Extends BaseVectorService to provide resource-specific operations.
|
|
6
|
+
*
|
|
7
|
+
* This service receives resource data from the dot-ai-controller and stores
|
|
8
|
+
* it in Qdrant for semantic search capabilities.
|
|
9
|
+
*/
|
|
10
|
+
import { BaseVectorService } from './base-vector-service';
|
|
11
|
+
import { VectorDBService } from './vector-db-service';
|
|
12
|
+
import { EmbeddingService } from './embedding-service';
|
|
13
|
+
/**
|
|
14
|
+
* Cluster resource data structure
|
|
15
|
+
* Matches the format sent by dot-ai-controller
|
|
16
|
+
* Note: ID is constructed by MCP from namespace/apiVersion/kind/name
|
|
17
|
+
*/
|
|
18
|
+
export interface ClusterResource {
|
|
19
|
+
namespace: string;
|
|
20
|
+
name: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
apiVersion: string;
|
|
23
|
+
apiGroup?: string;
|
|
24
|
+
labels: Record<string, string>;
|
|
25
|
+
annotations?: Record<string, string>;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resource sync request from controller
|
|
31
|
+
*/
|
|
32
|
+
export interface ResourceSyncRequest {
|
|
33
|
+
upserts?: ClusterResource[];
|
|
34
|
+
deletes?: string[];
|
|
35
|
+
isResync?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Result of a sync operation
|
|
39
|
+
*/
|
|
40
|
+
export interface SyncResult {
|
|
41
|
+
upserted: number;
|
|
42
|
+
deleted: number;
|
|
43
|
+
failures: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
error: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Result of a diff and sync operation
|
|
50
|
+
*/
|
|
51
|
+
export interface DiffSyncResult {
|
|
52
|
+
inserted: number;
|
|
53
|
+
updated: number;
|
|
54
|
+
deleted: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract API group from apiVersion
|
|
58
|
+
* e.g., 'apps/v1' -> 'apps', 'v1' -> ''
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractApiGroup(apiVersion: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Build embedding text from resource data
|
|
63
|
+
* Creates a semantic representation for vector search
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildEmbeddingText(resource: ClusterResource): string;
|
|
66
|
+
/**
|
|
67
|
+
* Generate resource ID from components
|
|
68
|
+
* Format: namespace:apiVersion:kind:name
|
|
69
|
+
*/
|
|
70
|
+
export declare function generateResourceId(namespace: string, apiVersion: string, kind: string, name: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Generate a deterministic UUID from resource ID for Qdrant storage
|
|
73
|
+
* Qdrant requires UUIDs or positive integers as point IDs
|
|
74
|
+
* The hash is deterministic so the same resource ID always maps to the same UUID
|
|
75
|
+
*/
|
|
76
|
+
export declare function generateResourceUuid(resourceId: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Check if two resources have meaningful differences
|
|
79
|
+
* Used for resync diff logic
|
|
80
|
+
*/
|
|
81
|
+
export declare function hasResourceChanged(existing: ClusterResource, incoming: ClusterResource): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Vector service for storing and searching Kubernetes cluster resources
|
|
84
|
+
*/
|
|
85
|
+
export declare class ResourceVectorService extends BaseVectorService<ClusterResource> {
|
|
86
|
+
constructor(collectionName?: string, vectorDB?: VectorDBService, embeddingService?: EmbeddingService);
|
|
87
|
+
/**
|
|
88
|
+
* Create searchable text from resource data for embedding generation
|
|
89
|
+
*/
|
|
90
|
+
protected createSearchText(resource: ClusterResource): string;
|
|
91
|
+
/**
|
|
92
|
+
* Extract unique ID from resource data
|
|
93
|
+
* Always constructs from components and hashes to UUID for Qdrant
|
|
94
|
+
*/
|
|
95
|
+
protected extractId(resource: ClusterResource): string;
|
|
96
|
+
/**
|
|
97
|
+
* Convert resource to storage payload format
|
|
98
|
+
*/
|
|
99
|
+
protected createPayload(resource: ClusterResource): Record<string, any>;
|
|
100
|
+
/**
|
|
101
|
+
* Convert storage payload back to resource object
|
|
102
|
+
*/
|
|
103
|
+
protected payloadToData(payload: Record<string, any>): ClusterResource;
|
|
104
|
+
/**
|
|
105
|
+
* Store a resource in the vector database
|
|
106
|
+
*/
|
|
107
|
+
storeResource(resource: ClusterResource): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Upsert a resource (alias for storeResource for API consistency)
|
|
110
|
+
*/
|
|
111
|
+
upsertResource(resource: ClusterResource): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Get a resource by ID
|
|
114
|
+
* Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
|
|
115
|
+
*/
|
|
116
|
+
getResource(id: string): Promise<ClusterResource | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Delete a resource by ID (idempotent - ignores not found)
|
|
119
|
+
* Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
|
|
120
|
+
*/
|
|
121
|
+
deleteResource(id: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Delete all resources (for testing/reset)
|
|
124
|
+
*/
|
|
125
|
+
deleteAllResources(): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* List all resources
|
|
128
|
+
*/
|
|
129
|
+
listResources(): Promise<ClusterResource[]>;
|
|
130
|
+
/**
|
|
131
|
+
* Diff incoming resources against Qdrant and sync changes
|
|
132
|
+
* Used for periodic resync operations
|
|
133
|
+
*/
|
|
134
|
+
diffAndSync(incoming: ClusterResource[]): Promise<DiffSyncResult>;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=resource-vector-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-vector-service.d.ts","sourceRoot":"","sources":["../../src/core/resource-vector-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CA+CpE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAK/D;AAeD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAiBhG;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,iBAAiB,CAAC,eAAe,CAAC;gBAGzE,cAAc,GAAE,MAAoB,EACpC,QAAQ,CAAC,EAAE,eAAe,EAC1B,gBAAgB,CAAC,EAAE,gBAAgB;IAKrC;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM;IAI7D;;;OAGG;IACH,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM;IAWtD;;OAEG;IACH,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAevE;;OAEG;IACH,SAAS,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,eAAe;IActE;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D;;OAEG;IACG,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D;;;OAGG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAK9D;;;OAGG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/C;;OAEG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIzC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAIjD;;;OAGG;IACG,WAAW,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;CA+CxE"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Resource Vector Service
|
|
4
|
+
*
|
|
5
|
+
* Vector-based storage and retrieval for Kubernetes cluster resources.
|
|
6
|
+
* Extends BaseVectorService to provide resource-specific operations.
|
|
7
|
+
*
|
|
8
|
+
* This service receives resource data from the dot-ai-controller and stores
|
|
9
|
+
* it in Qdrant for semantic search capabilities.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ResourceVectorService = void 0;
|
|
13
|
+
exports.extractApiGroup = extractApiGroup;
|
|
14
|
+
exports.buildEmbeddingText = buildEmbeddingText;
|
|
15
|
+
exports.generateResourceId = generateResourceId;
|
|
16
|
+
exports.generateResourceUuid = generateResourceUuid;
|
|
17
|
+
exports.hasResourceChanged = hasResourceChanged;
|
|
18
|
+
const crypto_1 = require("crypto");
|
|
19
|
+
const base_vector_service_1 = require("./base-vector-service");
|
|
20
|
+
/**
|
|
21
|
+
* Extract API group from apiVersion
|
|
22
|
+
* e.g., 'apps/v1' -> 'apps', 'v1' -> ''
|
|
23
|
+
*/
|
|
24
|
+
function extractApiGroup(apiVersion) {
|
|
25
|
+
const parts = apiVersion.split('/');
|
|
26
|
+
return parts.length > 1 ? parts[0] : '';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build embedding text from resource data
|
|
30
|
+
* Creates a semantic representation for vector search
|
|
31
|
+
*/
|
|
32
|
+
function buildEmbeddingText(resource) {
|
|
33
|
+
const parts = [
|
|
34
|
+
`${resource.kind} ${resource.name}`,
|
|
35
|
+
`namespace: ${resource.namespace}`,
|
|
36
|
+
`apiVersion: ${resource.apiVersion}`,
|
|
37
|
+
];
|
|
38
|
+
// Add API group if present
|
|
39
|
+
const apiGroup = resource.apiGroup || extractApiGroup(resource.apiVersion);
|
|
40
|
+
if (apiGroup) {
|
|
41
|
+
parts.push(`group: ${apiGroup}`);
|
|
42
|
+
}
|
|
43
|
+
// Add meaningful labels (skip standard Kubernetes labels)
|
|
44
|
+
if (resource.labels && Object.keys(resource.labels).length > 0) {
|
|
45
|
+
const meaningfulLabels = Object.entries(resource.labels)
|
|
46
|
+
.filter(([k]) => {
|
|
47
|
+
// Skip standard Kubernetes labels that don't add semantic value
|
|
48
|
+
const skipPrefixes = [
|
|
49
|
+
'app.kubernetes.io/',
|
|
50
|
+
'helm.sh/',
|
|
51
|
+
'kubernetes.io/',
|
|
52
|
+
'k8s.io/',
|
|
53
|
+
];
|
|
54
|
+
return !skipPrefixes.some(prefix => k.startsWith(prefix));
|
|
55
|
+
})
|
|
56
|
+
.map(([k, v]) => `${k}=${v}`);
|
|
57
|
+
if (meaningfulLabels.length > 0) {
|
|
58
|
+
parts.push(`labels: ${meaningfulLabels.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
// Also include app name from standard labels if present
|
|
61
|
+
const appName = resource.labels['app.kubernetes.io/name'] ||
|
|
62
|
+
resource.labels['app'] ||
|
|
63
|
+
resource.labels['name'];
|
|
64
|
+
if (appName) {
|
|
65
|
+
parts.push(`app: ${appName}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Add description from annotations if present
|
|
69
|
+
if (resource.annotations?.description) {
|
|
70
|
+
parts.push(`description: ${resource.annotations.description}`);
|
|
71
|
+
}
|
|
72
|
+
return parts.join(' | ');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Generate resource ID from components
|
|
76
|
+
* Format: namespace:apiVersion:kind:name
|
|
77
|
+
*/
|
|
78
|
+
function generateResourceId(namespace, apiVersion, kind, name) {
|
|
79
|
+
return `${namespace}:${apiVersion}:${kind}:${name}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate a deterministic UUID from resource ID for Qdrant storage
|
|
83
|
+
* Qdrant requires UUIDs or positive integers as point IDs
|
|
84
|
+
* The hash is deterministic so the same resource ID always maps to the same UUID
|
|
85
|
+
*/
|
|
86
|
+
function generateResourceUuid(resourceId) {
|
|
87
|
+
const hash = (0, crypto_1.createHash)('sha256').update(`resource-${resourceId}`).digest('hex');
|
|
88
|
+
// Convert to UUID format: 8-4-4-4-12
|
|
89
|
+
return `${hash.substring(0, 8)}-${hash.substring(8, 12)}-${hash.substring(12, 16)}-${hash.substring(16, 20)}-${hash.substring(20, 32)}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Stringify an object with sorted keys for reliable comparison
|
|
93
|
+
* Ensures consistent ordering regardless of object creation order
|
|
94
|
+
*/
|
|
95
|
+
function sortedStringify(obj) {
|
|
96
|
+
if (!obj)
|
|
97
|
+
return '{}';
|
|
98
|
+
const sorted = Object.keys(obj).sort().reduce((acc, key) => {
|
|
99
|
+
acc[key] = obj[key];
|
|
100
|
+
return acc;
|
|
101
|
+
}, {});
|
|
102
|
+
return JSON.stringify(sorted);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if two resources have meaningful differences
|
|
106
|
+
* Used for resync diff logic
|
|
107
|
+
*/
|
|
108
|
+
function hasResourceChanged(existing, incoming) {
|
|
109
|
+
// Compare updatedAt timestamps
|
|
110
|
+
if (existing.updatedAt !== incoming.updatedAt) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// Compare labels (with sorted keys for reliable comparison)
|
|
114
|
+
if (sortedStringify(existing.labels) !== sortedStringify(incoming.labels)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
// Compare annotations (with sorted keys for reliable comparison)
|
|
118
|
+
if (sortedStringify(existing.annotations) !== sortedStringify(incoming.annotations)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Vector service for storing and searching Kubernetes cluster resources
|
|
125
|
+
*/
|
|
126
|
+
class ResourceVectorService extends base_vector_service_1.BaseVectorService {
|
|
127
|
+
constructor(collectionName = 'resources', vectorDB, embeddingService) {
|
|
128
|
+
super(collectionName, vectorDB, embeddingService);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create searchable text from resource data for embedding generation
|
|
132
|
+
*/
|
|
133
|
+
createSearchText(resource) {
|
|
134
|
+
return buildEmbeddingText(resource);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Extract unique ID from resource data
|
|
138
|
+
* Always constructs from components and hashes to UUID for Qdrant
|
|
139
|
+
*/
|
|
140
|
+
extractId(resource) {
|
|
141
|
+
// Always construct ID from components (ignore any provided id)
|
|
142
|
+
const resourceId = generateResourceId(resource.namespace, resource.apiVersion, resource.kind, resource.name);
|
|
143
|
+
return generateResourceUuid(resourceId);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Convert resource to storage payload format
|
|
147
|
+
*/
|
|
148
|
+
createPayload(resource) {
|
|
149
|
+
return {
|
|
150
|
+
id: generateResourceId(resource.namespace, resource.apiVersion, resource.kind, resource.name),
|
|
151
|
+
namespace: resource.namespace,
|
|
152
|
+
name: resource.name,
|
|
153
|
+
kind: resource.kind,
|
|
154
|
+
apiVersion: resource.apiVersion,
|
|
155
|
+
apiGroup: resource.apiGroup || extractApiGroup(resource.apiVersion),
|
|
156
|
+
labels: resource.labels || {},
|
|
157
|
+
annotations: resource.annotations || {},
|
|
158
|
+
createdAt: resource.createdAt,
|
|
159
|
+
updatedAt: resource.updatedAt
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Convert storage payload back to resource object
|
|
164
|
+
*/
|
|
165
|
+
payloadToData(payload) {
|
|
166
|
+
return {
|
|
167
|
+
namespace: payload.namespace || '',
|
|
168
|
+
name: payload.name || '',
|
|
169
|
+
kind: payload.kind || '',
|
|
170
|
+
apiVersion: payload.apiVersion || '',
|
|
171
|
+
apiGroup: payload.apiGroup || '',
|
|
172
|
+
labels: payload.labels || {},
|
|
173
|
+
annotations: payload.annotations || {},
|
|
174
|
+
createdAt: payload.createdAt || new Date().toISOString(),
|
|
175
|
+
updatedAt: payload.updatedAt || new Date().toISOString()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Store a resource in the vector database
|
|
180
|
+
*/
|
|
181
|
+
async storeResource(resource) {
|
|
182
|
+
await this.storeData(resource);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Upsert a resource (alias for storeResource for API consistency)
|
|
186
|
+
*/
|
|
187
|
+
async upsertResource(resource) {
|
|
188
|
+
await this.storeResource(resource);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get a resource by ID
|
|
192
|
+
* Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
|
|
193
|
+
*/
|
|
194
|
+
async getResource(id) {
|
|
195
|
+
const uuid = generateResourceUuid(id);
|
|
196
|
+
return await this.getData(uuid);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Delete a resource by ID (idempotent - ignores not found)
|
|
200
|
+
* Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
|
|
201
|
+
*/
|
|
202
|
+
async deleteResource(id) {
|
|
203
|
+
try {
|
|
204
|
+
// Convert human-readable ID to UUID for Qdrant
|
|
205
|
+
const uuid = generateResourceUuid(id);
|
|
206
|
+
await this.deleteData(uuid);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// Idempotent delete - ignore "not found" errors
|
|
210
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
211
|
+
if (!errorMessage.toLowerCase().includes('not found')) {
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
// Resource already deleted or never existed - this is fine
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Delete all resources (for testing/reset)
|
|
219
|
+
*/
|
|
220
|
+
async deleteAllResources() {
|
|
221
|
+
await this.deleteAllData();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* List all resources
|
|
225
|
+
*/
|
|
226
|
+
async listResources() {
|
|
227
|
+
return await this.getAllData();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Diff incoming resources against Qdrant and sync changes
|
|
231
|
+
* Used for periodic resync operations
|
|
232
|
+
*/
|
|
233
|
+
async diffAndSync(incoming) {
|
|
234
|
+
// Helper to get human-readable ID from resource
|
|
235
|
+
const getResourceKey = (r) => generateResourceId(r.namespace, r.apiVersion, r.kind, r.name);
|
|
236
|
+
// Get all existing resources from Qdrant
|
|
237
|
+
const existing = await this.listResources();
|
|
238
|
+
const existingMap = new Map(existing.map(r => [getResourceKey(r), r]));
|
|
239
|
+
const incomingMap = new Map(incoming.map(r => [getResourceKey(r), r]));
|
|
240
|
+
const toInsert = [];
|
|
241
|
+
const toUpdate = [];
|
|
242
|
+
const toDelete = [];
|
|
243
|
+
// Find new and changed resources
|
|
244
|
+
for (const resource of incoming) {
|
|
245
|
+
const resourceId = getResourceKey(resource);
|
|
246
|
+
const existingResource = existingMap.get(resourceId);
|
|
247
|
+
if (!existingResource) {
|
|
248
|
+
toInsert.push(resource);
|
|
249
|
+
}
|
|
250
|
+
else if (hasResourceChanged(existingResource, resource)) {
|
|
251
|
+
toUpdate.push(resource);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Find deleted resources (in Qdrant but not in incoming)
|
|
255
|
+
for (const id of existingMap.keys()) {
|
|
256
|
+
if (!incomingMap.has(id)) {
|
|
257
|
+
toDelete.push(id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Apply changes
|
|
261
|
+
for (const resource of [...toInsert, ...toUpdate]) {
|
|
262
|
+
await this.storeResource(resource);
|
|
263
|
+
}
|
|
264
|
+
for (const id of toDelete) {
|
|
265
|
+
await this.deleteResource(id);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
inserted: toInsert.length,
|
|
269
|
+
updated: toUpdate.length,
|
|
270
|
+
deleted: toDelete.length
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
exports.ResourceVectorService = ResourceVectorService;
|
|
@@ -35,6 +35,7 @@ export declare class VectorDBService {
|
|
|
35
35
|
initializeCollection(vectorSize?: number): Promise<void>;
|
|
36
36
|
/**
|
|
37
37
|
* Create collection with specified vector size
|
|
38
|
+
* Handles conflict errors gracefully (collection already exists from race condition or restart)
|
|
38
39
|
*/
|
|
39
40
|
private createCollection;
|
|
40
41
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-db-service.d.ts","sourceRoot":"","sources":["../../src/core/vector-db-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,GAAE,cAAmB;IAsBvC,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,sBAAsB;IAM9B;;OAEG;IACG,oBAAoB,CAAC,UAAU,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDnE
|
|
1
|
+
{"version":3,"file":"vector-db-service.d.ts","sourceRoot":"","sources":["../../src/core/vector-db-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,GAAE,cAAmB;IAsBvC,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,sBAAsB;IAM9B;;OAEG;IACG,oBAAoB,CAAC,UAAU,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDnE;;;OAGG;YACW,gBAAgB;IAgC9B;;OAEG;IACG,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7D;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAsC1B;;OAEG;IACG,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAuD1B;;OAEG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAqC7D;;OAEG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B/C;;;;OAIG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCzC;;;OAGG;IACG,eAAe,CAAC,KAAK,GAAE,MAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyCvE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC;IAqBvC;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAsBrC;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,SAAS,IAAI,cAAc;CAG5B"}
|
|
@@ -92,22 +92,38 @@ class VectorDBService {
|
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* Create collection with specified vector size
|
|
95
|
+
* Handles conflict errors gracefully (collection already exists from race condition or restart)
|
|
95
96
|
*/
|
|
96
97
|
async createCollection(vectorSize) {
|
|
97
98
|
if (!this.client) {
|
|
98
99
|
throw new Error('Vector DB client not initialized');
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
try {
|
|
102
|
+
await this.client.createCollection(this.collectionName, {
|
|
103
|
+
vectors: {
|
|
104
|
+
size: vectorSize,
|
|
105
|
+
distance: 'Cosine',
|
|
106
|
+
on_disk: true // Enable on-disk storage for better performance with large collections
|
|
107
|
+
},
|
|
108
|
+
// Enable payload indexing for better keyword search performance
|
|
109
|
+
optimizers_config: {
|
|
110
|
+
default_segment_number: 2
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// Handle race condition where collection was created between check and create
|
|
116
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
117
|
+
if (errorMessage.toLowerCase().includes('conflict') ||
|
|
118
|
+
errorMessage.toLowerCase().includes('already exists')) {
|
|
119
|
+
// Collection exists - this is fine (race condition or restart)
|
|
120
|
+
if (process.env.DEBUG_DOT_AI) {
|
|
121
|
+
console.debug(`Collection ${this.collectionName} already exists, skipping creation`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
109
124
|
}
|
|
110
|
-
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
111
127
|
}
|
|
112
128
|
/**
|
|
113
129
|
* Store a document with optional vector
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Sync Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles POST /api/v1/resources/sync requests from dot-ai-controller.
|
|
5
|
+
* Receives cluster resource data, generates embeddings, and stores in Qdrant.
|
|
6
|
+
*/
|
|
7
|
+
import { Logger } from '../core/error-handling';
|
|
8
|
+
import { RestApiResponse } from './rest-api';
|
|
9
|
+
/**
|
|
10
|
+
* Reset initialization state (for testing or recovery)
|
|
11
|
+
* Call this to force re-initialization on the next request
|
|
12
|
+
*/
|
|
13
|
+
export declare function resetResourcesCollectionState(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Response data for successful sync operations
|
|
16
|
+
*/
|
|
17
|
+
export interface ResourceSyncResponseData {
|
|
18
|
+
upserted: number;
|
|
19
|
+
deleted: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Response data for partial failures
|
|
23
|
+
*/
|
|
24
|
+
export interface ResourceSyncFailureDetails {
|
|
25
|
+
upserted: number;
|
|
26
|
+
deleted: number;
|
|
27
|
+
failures: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
error: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle resource sync requests from the controller
|
|
34
|
+
*
|
|
35
|
+
* @param body - The request body containing upserts, deletes, and isResync flag
|
|
36
|
+
* @param logger - Logger instance for request tracking
|
|
37
|
+
* @param requestId - Unique request identifier for correlation
|
|
38
|
+
* @returns RestApiResponse with sync results
|
|
39
|
+
*/
|
|
40
|
+
export declare function handleResourceSync(body: unknown, logger: Logger, requestId: string): Promise<RestApiResponse>;
|
|
41
|
+
//# sourceMappingURL=resource-sync-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-sync-handler.d.ts","sourceRoot":"","sources":["../../src/interfaces/resource-sync-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAK7C;;;GAGG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAoID;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA0R1B"}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Resource Sync Handler
|
|
4
|
+
*
|
|
5
|
+
* Handles POST /api/v1/resources/sync requests from dot-ai-controller.
|
|
6
|
+
* Receives cluster resource data, generates embeddings, and stores in Qdrant.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.resetResourcesCollectionState = resetResourcesCollectionState;
|
|
10
|
+
exports.handleResourceSync = handleResourceSync;
|
|
11
|
+
const resource_vector_service_1 = require("../core/resource-vector-service");
|
|
12
|
+
// Global flag to track if resources collection has been initialized
|
|
13
|
+
let resourcesCollectionInitialized = false;
|
|
14
|
+
/**
|
|
15
|
+
* Reset initialization state (for testing or recovery)
|
|
16
|
+
* Call this to force re-initialization on the next request
|
|
17
|
+
*/
|
|
18
|
+
function resetResourcesCollectionState() {
|
|
19
|
+
resourcesCollectionInitialized = false;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate a single cluster resource object
|
|
23
|
+
*/
|
|
24
|
+
function validateClusterResource(resource, index) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
if (!resource || typeof resource !== 'object') {
|
|
27
|
+
return { valid: false, errors: [`upserts[${index}]: must be an object`] };
|
|
28
|
+
}
|
|
29
|
+
const r = resource;
|
|
30
|
+
if (!r.namespace || typeof r.namespace !== 'string' || r.namespace.length === 0) {
|
|
31
|
+
errors.push(`upserts[${index}].namespace: required string field`);
|
|
32
|
+
}
|
|
33
|
+
if (!r.name || typeof r.name !== 'string' || r.name.length === 0) {
|
|
34
|
+
errors.push(`upserts[${index}].name: required string field`);
|
|
35
|
+
}
|
|
36
|
+
if (!r.kind || typeof r.kind !== 'string' || r.kind.length === 0) {
|
|
37
|
+
errors.push(`upserts[${index}].kind: required string field`);
|
|
38
|
+
}
|
|
39
|
+
if (!r.apiVersion || typeof r.apiVersion !== 'string' || r.apiVersion.length === 0) {
|
|
40
|
+
errors.push(`upserts[${index}].apiVersion: required string field`);
|
|
41
|
+
}
|
|
42
|
+
if (!r.createdAt || typeof r.createdAt !== 'string') {
|
|
43
|
+
errors.push(`upserts[${index}].createdAt: required string field`);
|
|
44
|
+
}
|
|
45
|
+
if (!r.updatedAt || typeof r.updatedAt !== 'string') {
|
|
46
|
+
errors.push(`upserts[${index}].updatedAt: required string field`);
|
|
47
|
+
}
|
|
48
|
+
return { valid: errors.length === 0, errors };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate the sync request body
|
|
52
|
+
*/
|
|
53
|
+
function validateSyncRequest(body) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (!body || typeof body !== 'object') {
|
|
56
|
+
return { valid: false, errors: ['Request body must be an object'] };
|
|
57
|
+
}
|
|
58
|
+
const b = body;
|
|
59
|
+
// Default values
|
|
60
|
+
const upserts = [];
|
|
61
|
+
const deletes = [];
|
|
62
|
+
const isResync = b.isResync === true;
|
|
63
|
+
// Validate upserts array
|
|
64
|
+
if (b.upserts !== undefined) {
|
|
65
|
+
if (!Array.isArray(b.upserts)) {
|
|
66
|
+
errors.push('upserts: must be an array');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (let i = 0; i < b.upserts.length; i++) {
|
|
70
|
+
const resourceValidation = validateClusterResource(b.upserts[i], i);
|
|
71
|
+
if (!resourceValidation.valid) {
|
|
72
|
+
errors.push(...resourceValidation.errors);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const r = b.upserts[i];
|
|
76
|
+
upserts.push({
|
|
77
|
+
namespace: r.namespace,
|
|
78
|
+
name: r.name,
|
|
79
|
+
kind: r.kind,
|
|
80
|
+
apiVersion: r.apiVersion,
|
|
81
|
+
apiGroup: r.apiGroup,
|
|
82
|
+
labels: r.labels || {},
|
|
83
|
+
annotations: r.annotations,
|
|
84
|
+
createdAt: r.createdAt,
|
|
85
|
+
updatedAt: r.updatedAt
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Validate deletes array (objects with namespace, apiVersion, kind, name)
|
|
92
|
+
if (b.deletes !== undefined) {
|
|
93
|
+
if (!Array.isArray(b.deletes)) {
|
|
94
|
+
errors.push('deletes: must be an array');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (let i = 0; i < b.deletes.length; i++) {
|
|
98
|
+
const del = b.deletes[i];
|
|
99
|
+
if (!del || typeof del !== 'object') {
|
|
100
|
+
errors.push(`deletes[${i}]: must be an object`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const d = del;
|
|
104
|
+
const delErrors = [];
|
|
105
|
+
if (!d.namespace || typeof d.namespace !== 'string') {
|
|
106
|
+
delErrors.push(`deletes[${i}].namespace: required string field`);
|
|
107
|
+
}
|
|
108
|
+
if (!d.name || typeof d.name !== 'string') {
|
|
109
|
+
delErrors.push(`deletes[${i}].name: required string field`);
|
|
110
|
+
}
|
|
111
|
+
if (!d.kind || typeof d.kind !== 'string') {
|
|
112
|
+
delErrors.push(`deletes[${i}].kind: required string field`);
|
|
113
|
+
}
|
|
114
|
+
if (!d.apiVersion || typeof d.apiVersion !== 'string') {
|
|
115
|
+
delErrors.push(`deletes[${i}].apiVersion: required string field`);
|
|
116
|
+
}
|
|
117
|
+
if (delErrors.length > 0) {
|
|
118
|
+
errors.push(...delErrors);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Construct ID from components
|
|
122
|
+
const deleteId = (0, resource_vector_service_1.generateResourceId)(d.namespace, d.apiVersion, d.kind, d.name);
|
|
123
|
+
deletes.push(deleteId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (errors.length > 0) {
|
|
129
|
+
return { valid: false, errors };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
valid: true,
|
|
133
|
+
errors: [],
|
|
134
|
+
data: { upserts, deletes, isResync }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handle resource sync requests from the controller
|
|
139
|
+
*
|
|
140
|
+
* @param body - The request body containing upserts, deletes, and isResync flag
|
|
141
|
+
* @param logger - Logger instance for request tracking
|
|
142
|
+
* @param requestId - Unique request identifier for correlation
|
|
143
|
+
* @returns RestApiResponse with sync results
|
|
144
|
+
*/
|
|
145
|
+
async function handleResourceSync(body, logger, requestId) {
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
// Validate request body using manual validation
|
|
148
|
+
const validation = validateSyncRequest(body);
|
|
149
|
+
if (!validation.valid) {
|
|
150
|
+
logger.warn('Resource sync request validation failed', {
|
|
151
|
+
requestId,
|
|
152
|
+
errors: validation.errors
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: {
|
|
157
|
+
code: 'VALIDATION_ERROR',
|
|
158
|
+
message: 'Invalid request body',
|
|
159
|
+
details: validation.errors.map(e => ({
|
|
160
|
+
path: e.split(':')[0] || 'unknown',
|
|
161
|
+
message: e.split(':').slice(1).join(':').trim() || e
|
|
162
|
+
}))
|
|
163
|
+
},
|
|
164
|
+
meta: {
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
requestId,
|
|
167
|
+
version: 'v1'
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const syncRequest = validation.data;
|
|
172
|
+
const { upserts = [], deletes = [], isResync = false } = syncRequest;
|
|
173
|
+
logger.info('Processing resource sync request', {
|
|
174
|
+
requestId,
|
|
175
|
+
upsertCount: upserts.length,
|
|
176
|
+
deleteCount: deletes.length,
|
|
177
|
+
isResync
|
|
178
|
+
});
|
|
179
|
+
// Initialize the resource vector service
|
|
180
|
+
let resourceService;
|
|
181
|
+
try {
|
|
182
|
+
resourceService = new resource_vector_service_1.ResourceVectorService();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
logger.error('Failed to create ResourceVectorService', error, { requestId });
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
error: {
|
|
189
|
+
code: 'SERVICE_INIT_FAILED',
|
|
190
|
+
message: 'Failed to create resource vector service',
|
|
191
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
192
|
+
},
|
|
193
|
+
meta: {
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
requestId,
|
|
196
|
+
version: 'v1'
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Check Vector DB health
|
|
201
|
+
let isHealthy;
|
|
202
|
+
try {
|
|
203
|
+
isHealthy = await resourceService.healthCheck();
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
logger.error('Health check threw exception', error, { requestId });
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: {
|
|
210
|
+
code: 'HEALTH_CHECK_FAILED',
|
|
211
|
+
message: 'Vector DB health check threw exception',
|
|
212
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
213
|
+
},
|
|
214
|
+
meta: {
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
requestId,
|
|
217
|
+
version: 'v1'
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (!isHealthy) {
|
|
222
|
+
logger.error('Vector DB health check failed', new Error('Qdrant unavailable'), { requestId });
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
error: {
|
|
226
|
+
code: 'VECTOR_DB_UNAVAILABLE',
|
|
227
|
+
message: 'Vector database is not available',
|
|
228
|
+
details: {
|
|
229
|
+
recommendation: 'Ensure Qdrant is running and accessible'
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
meta: {
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
requestId,
|
|
235
|
+
version: 'v1'
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// Initialize the collection (only on first request, skip thereafter for performance)
|
|
240
|
+
if (!resourcesCollectionInitialized) {
|
|
241
|
+
try {
|
|
242
|
+
await resourceService.initialize();
|
|
243
|
+
resourcesCollectionInitialized = true;
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
logger.error('Failed to initialize resources collection', error, { requestId });
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: {
|
|
250
|
+
code: 'COLLECTION_INIT_FAILED',
|
|
251
|
+
message: 'Failed to initialize resources collection',
|
|
252
|
+
details: {
|
|
253
|
+
error: error instanceof Error ? error.message : String(error)
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
meta: {
|
|
257
|
+
timestamp: new Date().toISOString(),
|
|
258
|
+
requestId,
|
|
259
|
+
version: 'v1'
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let upserted = 0;
|
|
265
|
+
let deleted = 0;
|
|
266
|
+
const failures = [];
|
|
267
|
+
// Handle resync mode - use diffAndSync for full reconciliation
|
|
268
|
+
if (isResync && upserts.length > 0) {
|
|
269
|
+
logger.info('Processing resync with diff', {
|
|
270
|
+
requestId,
|
|
271
|
+
incomingResourceCount: upserts.length
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
const diffResult = await resourceService.diffAndSync(upserts);
|
|
275
|
+
logger.info('Resync diff completed', {
|
|
276
|
+
requestId,
|
|
277
|
+
inserted: diffResult.inserted,
|
|
278
|
+
updated: diffResult.updated,
|
|
279
|
+
deleted: diffResult.deleted
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
data: {
|
|
284
|
+
upserted: diffResult.inserted + diffResult.updated,
|
|
285
|
+
deleted: diffResult.deleted,
|
|
286
|
+
resync: {
|
|
287
|
+
inserted: diffResult.inserted,
|
|
288
|
+
updated: diffResult.updated,
|
|
289
|
+
deleted: diffResult.deleted
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
meta: {
|
|
293
|
+
timestamp: new Date().toISOString(),
|
|
294
|
+
requestId,
|
|
295
|
+
version: 'v1'
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
logger.error('Resync diff failed', error, { requestId });
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: {
|
|
304
|
+
code: 'RESYNC_FAILED',
|
|
305
|
+
message: 'Failed to perform resync diff',
|
|
306
|
+
details: {
|
|
307
|
+
error: error instanceof Error ? error.message : String(error)
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
meta: {
|
|
311
|
+
timestamp: new Date().toISOString(),
|
|
312
|
+
requestId,
|
|
313
|
+
version: 'v1'
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Handle upserts - process each resource
|
|
319
|
+
for (const resource of upserts) {
|
|
320
|
+
const resourceId = (0, resource_vector_service_1.generateResourceId)(resource.namespace, resource.apiVersion, resource.kind, resource.name);
|
|
321
|
+
try {
|
|
322
|
+
await resourceService.upsertResource(resource);
|
|
323
|
+
upserted++;
|
|
324
|
+
logger.debug('Resource upserted', {
|
|
325
|
+
requestId,
|
|
326
|
+
resourceId,
|
|
327
|
+
kind: resource.kind,
|
|
328
|
+
namespace: resource.namespace
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
333
|
+
failures.push({ id: resourceId, error: errorMessage });
|
|
334
|
+
logger.warn('Failed to upsert resource', {
|
|
335
|
+
requestId,
|
|
336
|
+
resourceId,
|
|
337
|
+
error: errorMessage
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Handle deletes - idempotent (ignore not found)
|
|
342
|
+
for (const id of deletes) {
|
|
343
|
+
try {
|
|
344
|
+
await resourceService.deleteResource(id);
|
|
345
|
+
deleted++;
|
|
346
|
+
logger.debug('Resource deleted', {
|
|
347
|
+
requestId,
|
|
348
|
+
resourceId: id
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
// deleteResource is already idempotent, but handle any other errors
|
|
353
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
354
|
+
failures.push({ id, error: errorMessage });
|
|
355
|
+
logger.warn('Failed to delete resource', {
|
|
356
|
+
requestId,
|
|
357
|
+
resourceId: id,
|
|
358
|
+
error: errorMessage
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const executionTime = Date.now() - startTime;
|
|
363
|
+
// Return appropriate response based on failures
|
|
364
|
+
if (failures.length > 0) {
|
|
365
|
+
logger.warn('Resource sync completed with failures', {
|
|
366
|
+
requestId,
|
|
367
|
+
upserted,
|
|
368
|
+
deleted,
|
|
369
|
+
failureCount: failures.length,
|
|
370
|
+
executionTime
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
error: {
|
|
375
|
+
code: 'SYNC_PARTIAL_FAILURE',
|
|
376
|
+
message: `Failed to process ${failures.length} resource(s)`,
|
|
377
|
+
details: {
|
|
378
|
+
upserted,
|
|
379
|
+
deleted,
|
|
380
|
+
failures
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
meta: {
|
|
384
|
+
timestamp: new Date().toISOString(),
|
|
385
|
+
requestId,
|
|
386
|
+
version: 'v1'
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
logger.info('Resource sync completed successfully', {
|
|
391
|
+
requestId,
|
|
392
|
+
upserted,
|
|
393
|
+
deleted,
|
|
394
|
+
executionTime
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
success: true,
|
|
398
|
+
data: {
|
|
399
|
+
upserted,
|
|
400
|
+
deleted
|
|
401
|
+
},
|
|
402
|
+
meta: {
|
|
403
|
+
timestamp: new Date().toISOString(),
|
|
404
|
+
requestId,
|
|
405
|
+
version: 'v1'
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
@@ -16,7 +16,8 @@ export declare enum HttpStatus {
|
|
|
16
16
|
BAD_REQUEST = 400,
|
|
17
17
|
NOT_FOUND = 404,
|
|
18
18
|
METHOD_NOT_ALLOWED = 405,
|
|
19
|
-
INTERNAL_SERVER_ERROR = 500
|
|
19
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
20
|
+
SERVICE_UNAVAILABLE = 503
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Standard REST API response format
|
|
@@ -96,6 +97,10 @@ export declare class RestApiRouter {
|
|
|
96
97
|
* Handle OpenAPI specification requests
|
|
97
98
|
*/
|
|
98
99
|
private handleOpenApiSpec;
|
|
100
|
+
/**
|
|
101
|
+
* Handle resource sync requests from controller
|
|
102
|
+
*/
|
|
103
|
+
private handleResourceSyncRequest;
|
|
99
104
|
/**
|
|
100
105
|
* Set CORS headers
|
|
101
106
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC;;GAEG;AACH,oBAAY,UAAU;IACpB,EAAE,MAAM;IACR,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,kBAAkB,MAAM;IACxB,qBAAqB,MAAM;IAC3B,mBAAmB,MAAM;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,GAAG,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAa;gBAGjC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAoBrC;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAyFzF;;OAEG;IACH,OAAO,CAAC,YAAY;IAyCpB;;OAEG;YACW,mBAAmB;IA2CjC;;OAEG;YACW,mBAAmB;IA+FjC;;OAEG;YACW,iBAAiB;IA8B/B;;OAEG;YACW,yBAAyB;IAgEvC;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;YACW,gBAAgB;IAK9B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIvC;;OAEG;IACH,SAAS,IAAI,aAAa;CAG3B"}
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.RestApiRouter = exports.HttpStatus = void 0;
|
|
10
10
|
const node_url_1 = require("node:url");
|
|
11
11
|
const openapi_generator_1 = require("./openapi-generator");
|
|
12
|
+
const resource_sync_handler_1 = require("./resource-sync-handler");
|
|
12
13
|
/**
|
|
13
14
|
* HTTP status codes for REST responses
|
|
14
15
|
*/
|
|
@@ -19,6 +20,7 @@ var HttpStatus;
|
|
|
19
20
|
HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
20
21
|
HttpStatus[HttpStatus["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
|
|
21
22
|
HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
|
23
|
+
HttpStatus[HttpStatus["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
|
|
22
24
|
})(HttpStatus || (exports.HttpStatus = HttpStatus = {}));
|
|
23
25
|
/**
|
|
24
26
|
* REST API Router for MCP tools
|
|
@@ -105,6 +107,17 @@ class RestApiRouter {
|
|
|
105
107
|
await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for OpenAPI specification');
|
|
106
108
|
}
|
|
107
109
|
break;
|
|
110
|
+
case 'resources':
|
|
111
|
+
if (req.method === 'POST' && pathMatch.action === 'sync') {
|
|
112
|
+
await this.handleResourceSyncRequest(req, res, requestId, body);
|
|
113
|
+
}
|
|
114
|
+
else if (req.method !== 'POST') {
|
|
115
|
+
await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for resource sync');
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown resources endpoint');
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
108
121
|
default:
|
|
109
122
|
await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown API endpoint');
|
|
110
123
|
}
|
|
@@ -125,6 +138,7 @@ class RestApiRouter {
|
|
|
125
138
|
// /api/v1/tools -> tools discovery
|
|
126
139
|
// /api/v1/tools/{toolName} -> tool execution
|
|
127
140
|
// /api/v1/openapi -> OpenAPI spec
|
|
141
|
+
// /api/v1/resources/sync -> resource sync from controller
|
|
128
142
|
const basePath = `${this.config.basePath}/${this.config.version}`;
|
|
129
143
|
if (!pathname.startsWith(basePath)) {
|
|
130
144
|
return null;
|
|
@@ -144,6 +158,10 @@ class RestApiRouter {
|
|
|
144
158
|
return { endpoint: 'tool', toolName };
|
|
145
159
|
}
|
|
146
160
|
}
|
|
161
|
+
// Handle resources/sync endpoint
|
|
162
|
+
if (cleanPath === 'resources/sync') {
|
|
163
|
+
return { endpoint: 'resources', action: 'sync' };
|
|
164
|
+
}
|
|
147
165
|
return null;
|
|
148
166
|
}
|
|
149
167
|
/**
|
|
@@ -284,6 +302,50 @@ class RestApiRouter {
|
|
|
284
302
|
await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'OPENAPI_ERROR', 'Failed to generate OpenAPI specification');
|
|
285
303
|
}
|
|
286
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Handle resource sync requests from controller
|
|
307
|
+
*/
|
|
308
|
+
async handleResourceSyncRequest(req, res, requestId, body) {
|
|
309
|
+
try {
|
|
310
|
+
this.logger.info('Processing resource sync request', { requestId });
|
|
311
|
+
// Validate request body exists
|
|
312
|
+
if (!body || typeof body !== 'object') {
|
|
313
|
+
await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_REQUEST', 'Request body must be a JSON object');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Delegate to the resource sync handler
|
|
317
|
+
const response = await (0, resource_sync_handler_1.handleResourceSync)(body, this.logger, requestId);
|
|
318
|
+
// Determine HTTP status based on response and error type
|
|
319
|
+
let httpStatus = HttpStatus.OK;
|
|
320
|
+
if (!response.success) {
|
|
321
|
+
const errorCode = response.error?.code;
|
|
322
|
+
if (errorCode === 'VECTOR_DB_UNAVAILABLE' || errorCode === 'HEALTH_CHECK_FAILED') {
|
|
323
|
+
httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
|
|
324
|
+
}
|
|
325
|
+
else if (errorCode === 'SERVICE_INIT_FAILED' || errorCode === 'COLLECTION_INIT_FAILED' || errorCode === 'RESYNC_FAILED') {
|
|
326
|
+
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
httpStatus = HttpStatus.BAD_REQUEST;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await this.sendJsonResponse(res, httpStatus, response);
|
|
333
|
+
this.logger.info('Resource sync request completed', {
|
|
334
|
+
requestId,
|
|
335
|
+
success: response.success,
|
|
336
|
+
upserted: response.data?.upserted,
|
|
337
|
+
deleted: response.data?.deleted
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
342
|
+
this.logger.error('Resource sync request failed', error instanceof Error ? error : new Error(String(error)), {
|
|
343
|
+
requestId,
|
|
344
|
+
errorMessage
|
|
345
|
+
});
|
|
346
|
+
await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SYNC_ERROR', 'Resource sync failed', { error: errorMessage });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
287
349
|
/**
|
|
288
350
|
* Set CORS headers
|
|
289
351
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vfarcic/dot-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.167.0",
|
|
4
4
|
"description": "AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance",
|
|
5
5
|
"mcpName": "io.github.vfarcic/dot-ai",
|
|
6
6
|
"main": "dist/index.js",
|