@influxdata/influxdb3-mcp-server 1.3.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/CHANGELOG.md +194 -0
- package/Dockerfile +22 -0
- package/LICENSE +6 -0
- package/LICENSE-APACHE.txt +201 -0
- package/LICENSE-MIT.txt +25 -0
- package/README.md +318 -0
- package/build/config.js +85 -0
- package/build/helpers/enums/influx-product-types.enum.js +8 -0
- package/build/index.js +33 -0
- package/build/prompts/index.js +98 -0
- package/build/resources/index.js +223 -0
- package/build/server/index.js +104 -0
- package/build/services/base-connection.service.js +318 -0
- package/build/services/cloud-token-management.service.js +179 -0
- package/build/services/context-file.service.js +97 -0
- package/build/services/database-management.service.js +549 -0
- package/build/services/help.service.js +241 -0
- package/build/services/http-client.service.js +109 -0
- package/build/services/influxdb-master.service.js +117 -0
- package/build/services/query.service.js +499 -0
- package/build/services/serverless-schema-management.service.js +266 -0
- package/build/services/token-management.service.js +215 -0
- package/build/services/write.service.js +153 -0
- package/build/tools/categories/cloud-token.tools.js +321 -0
- package/build/tools/categories/database.tools.js +299 -0
- package/build/tools/categories/health.tools.js +75 -0
- package/build/tools/categories/help.tools.js +104 -0
- package/build/tools/categories/query.tools.js +180 -0
- package/build/tools/categories/schema.tools.js +308 -0
- package/build/tools/categories/token.tools.js +378 -0
- package/build/tools/categories/write.tools.js +104 -0
- package/build/tools/index.js +27 -0
- package/env.example +17 -0
- package/example-cloud-dedicated.mcp.json +15 -0
- package/example-cloud-serverless.mcp.json +13 -0
- package/example-clustered.mcp.json +14 -0
- package/example-docker.mcp.json +25 -0
- package/example-local.mcp.json +13 -0
- package/example-npx.mcp.json +13 -0
- package/package.json +78 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InfluxDB Cloud Serverless Schema Management Service
|
|
3
|
+
*
|
|
4
|
+
* NOTE: As of Ocotber 2025, explicit schemas are not supported in InfluxDB v3 (Cloud Serverless).
|
|
5
|
+
* This service and tools are preserved for potential future compatibility when
|
|
6
|
+
* explicit schema support may be added to Cloud Serverless.
|
|
7
|
+
*
|
|
8
|
+
* For current schema exploration in Cloud Serverless, use the query-based tools:
|
|
9
|
+
* - get_measurements: Lists all measurements using information_schema
|
|
10
|
+
* - get_measurement_schema: Shows column details using information_schema
|
|
11
|
+
*
|
|
12
|
+
* This service provides schema management capabilities specific to InfluxDB Cloud Serverless,
|
|
13
|
+
* including listing, creating, updating, and deleting measurement schemas within buckets.
|
|
14
|
+
*/
|
|
15
|
+
import { InfluxProductType } from "../helpers/enums/influx-product-types.enum.js";
|
|
16
|
+
export class SchemaManagementService {
|
|
17
|
+
baseService;
|
|
18
|
+
constructor(baseService) {
|
|
19
|
+
this.baseService = baseService;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate that schema operations are supported (Cloud Serverless only)
|
|
23
|
+
*/
|
|
24
|
+
validateSchemaOperationSupport() {
|
|
25
|
+
const connectionInfo = this.baseService.getConnectionInfo();
|
|
26
|
+
if (connectionInfo.type !== InfluxProductType.CloudServerless) {
|
|
27
|
+
throw new Error(`Schema management is only supported for Cloud Serverless. Current type: ${connectionInfo.type}`);
|
|
28
|
+
}
|
|
29
|
+
this.baseService.validateManagementCapabilities();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* List all measurement schemas in a bucket
|
|
33
|
+
* GET /api/v2/buckets/{bucketID}/schema/measurements
|
|
34
|
+
*/
|
|
35
|
+
async listSchemas(bucketName) {
|
|
36
|
+
this.validateSchemaOperationSupport();
|
|
37
|
+
try {
|
|
38
|
+
const httpClient = this.baseService.getInfluxHttpClient(true);
|
|
39
|
+
// First, get the bucket ID by name
|
|
40
|
+
const bucketsResponse = await httpClient.get("/api/v2/buckets");
|
|
41
|
+
let bucketId;
|
|
42
|
+
if (bucketsResponse?.buckets) {
|
|
43
|
+
const bucket = bucketsResponse.buckets.find((b) => b.name === bucketName && b.type !== "system");
|
|
44
|
+
if (bucket) {
|
|
45
|
+
bucketId = bucket.id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!bucketId) {
|
|
49
|
+
throw new Error(`Bucket '${bucketName}' not found`);
|
|
50
|
+
}
|
|
51
|
+
const endpoint = `/api/v2/buckets/${bucketId}/schema/measurements`;
|
|
52
|
+
const response = await httpClient.get(endpoint);
|
|
53
|
+
let schemas = [];
|
|
54
|
+
if (response && Array.isArray(response.measurementSchemas)) {
|
|
55
|
+
schemas = response.measurementSchemas;
|
|
56
|
+
}
|
|
57
|
+
else if (Array.isArray(response)) {
|
|
58
|
+
schemas = response;
|
|
59
|
+
}
|
|
60
|
+
return schemas.map((schema) => {
|
|
61
|
+
const schemaInfo = {
|
|
62
|
+
name: schema.name,
|
|
63
|
+
bucketId: schema.bucketID || bucketId,
|
|
64
|
+
bucketName: bucketName,
|
|
65
|
+
};
|
|
66
|
+
if (Array.isArray(schema.columns)) {
|
|
67
|
+
schemaInfo.columns = schema.columns.map((col) => ({
|
|
68
|
+
name: col.name,
|
|
69
|
+
type: col.type,
|
|
70
|
+
dataType: col.dataType,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
if (schema.createdAt) {
|
|
74
|
+
schemaInfo.createdAt = schema.createdAt;
|
|
75
|
+
}
|
|
76
|
+
if (schema.updatedAt) {
|
|
77
|
+
schemaInfo.updatedAt = schema.updatedAt;
|
|
78
|
+
}
|
|
79
|
+
return schemaInfo;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.handleSchemaError(error, `list schemas for bucket '${bucketName}'`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get detailed information about a specific measurement schema
|
|
88
|
+
* GET /api/v2/buckets/{bucketID}/schema/measurements/{measurementID}
|
|
89
|
+
*/
|
|
90
|
+
async getSchema(bucketName, schemaName) {
|
|
91
|
+
this.validateSchemaOperationSupport();
|
|
92
|
+
try {
|
|
93
|
+
const httpClient = this.baseService.getInfluxHttpClient(true);
|
|
94
|
+
const bucketsResponse = await httpClient.get("/api/v2/buckets");
|
|
95
|
+
let bucketId;
|
|
96
|
+
if (bucketsResponse?.buckets) {
|
|
97
|
+
const bucket = bucketsResponse.buckets.find((b) => b.name === bucketName && b.type !== "system");
|
|
98
|
+
if (bucket) {
|
|
99
|
+
bucketId = bucket.id;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!bucketId) {
|
|
103
|
+
throw new Error(`Bucket '${bucketName}' not found`);
|
|
104
|
+
}
|
|
105
|
+
const schemasResponse = await httpClient.get(`/api/v2/buckets/${bucketId}/schema/measurements`);
|
|
106
|
+
let measurementId;
|
|
107
|
+
if (schemasResponse &&
|
|
108
|
+
Array.isArray(schemasResponse.measurementSchemas)) {
|
|
109
|
+
const schema = schemasResponse.measurementSchemas.find((s) => s.name === schemaName);
|
|
110
|
+
if (schema) {
|
|
111
|
+
measurementId = schema.id;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!measurementId) {
|
|
115
|
+
throw new Error(`Schema '${schemaName}' not found in bucket '${bucketName}'`);
|
|
116
|
+
}
|
|
117
|
+
const endpoint = `/api/v2/buckets/${bucketId}/schema/measurements/${measurementId}`;
|
|
118
|
+
const response = await httpClient.get(endpoint);
|
|
119
|
+
const schemaInfo = {
|
|
120
|
+
name: response.name,
|
|
121
|
+
bucketId: response.bucketID || bucketId,
|
|
122
|
+
bucketName: bucketName,
|
|
123
|
+
};
|
|
124
|
+
if (Array.isArray(response.columns)) {
|
|
125
|
+
schemaInfo.columns = response.columns.map((col) => ({
|
|
126
|
+
name: col.name,
|
|
127
|
+
type: col.type,
|
|
128
|
+
dataType: col.dataType,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
if (response.createdAt) {
|
|
132
|
+
schemaInfo.createdAt = response.createdAt;
|
|
133
|
+
}
|
|
134
|
+
if (response.updatedAt) {
|
|
135
|
+
schemaInfo.updatedAt = response.updatedAt;
|
|
136
|
+
}
|
|
137
|
+
return schemaInfo;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.handleSchemaError(error, `get schema '${schemaName}' from bucket '${bucketName}'`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Create a new measurement schema in a bucket
|
|
145
|
+
* POST /api/v2/buckets/{bucketID}/schema/measurements
|
|
146
|
+
*/
|
|
147
|
+
async createSchema(config) {
|
|
148
|
+
this.validateSchemaOperationSupport();
|
|
149
|
+
try {
|
|
150
|
+
const httpClient = this.baseService.getInfluxHttpClient(true);
|
|
151
|
+
const bucketsResponse = await httpClient.get("/api/v2/buckets");
|
|
152
|
+
let bucketId;
|
|
153
|
+
if (bucketsResponse?.buckets) {
|
|
154
|
+
const bucket = bucketsResponse.buckets.find((b) => b.name === config.bucketName && b.type !== "system");
|
|
155
|
+
if (bucket) {
|
|
156
|
+
bucketId = bucket.id;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!bucketId) {
|
|
160
|
+
throw new Error(`Bucket '${config.bucketName}' not found`);
|
|
161
|
+
}
|
|
162
|
+
const payload = {
|
|
163
|
+
name: config.name,
|
|
164
|
+
columns: config.columns.map((col) => {
|
|
165
|
+
const column = {
|
|
166
|
+
type: col.type,
|
|
167
|
+
name: col.name,
|
|
168
|
+
};
|
|
169
|
+
if (col.type === "field" && col.dataType) {
|
|
170
|
+
column.dataType = col.dataType;
|
|
171
|
+
}
|
|
172
|
+
return column;
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
const endpoint = `/api/v2/buckets/${bucketId}/schema/measurements`;
|
|
176
|
+
await httpClient.post(endpoint, payload);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
this.handleSchemaError(error, `create schema '${config.name}' in bucket '${config.bucketName}'`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Add new columns to an existing measurement schema
|
|
185
|
+
* IMPORTANT: This endpoint only allows ADDING new columns, not modifying existing ones.
|
|
186
|
+
* The request must include ALL columns (existing + new ones to add).
|
|
187
|
+
* Get the current schema first to retrieve existing columns, then include them with new columns.
|
|
188
|
+
* PATCH /api/v2/buckets/{bucketID}/schema/measurements/{measurementName}
|
|
189
|
+
*/
|
|
190
|
+
async updateSchema(bucketName, schemaName, config) {
|
|
191
|
+
this.validateSchemaOperationSupport();
|
|
192
|
+
try {
|
|
193
|
+
const httpClient = this.baseService.getInfluxHttpClient(true);
|
|
194
|
+
const bucketsResponse = await httpClient.get("/api/v2/buckets");
|
|
195
|
+
let bucketId;
|
|
196
|
+
if (bucketsResponse?.buckets) {
|
|
197
|
+
const bucket = bucketsResponse.buckets.find((b) => b.name === bucketName && b.type !== "system");
|
|
198
|
+
if (bucket) {
|
|
199
|
+
bucketId = bucket.id;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!bucketId) {
|
|
203
|
+
throw new Error(`Bucket '${bucketName}' not found`);
|
|
204
|
+
}
|
|
205
|
+
const schemasResponse = await httpClient.get(`/api/v2/buckets/${bucketId}/schema/measurements`);
|
|
206
|
+
if (!schemasResponse.measurementSchemas ||
|
|
207
|
+
!Array.isArray(schemasResponse.measurementSchemas)) {
|
|
208
|
+
throw new Error(`No schemas found in bucket '${bucketName}'`);
|
|
209
|
+
}
|
|
210
|
+
const existingSchema = schemasResponse.measurementSchemas.find((schema) => schema.name === schemaName);
|
|
211
|
+
if (!existingSchema) {
|
|
212
|
+
throw new Error(`Schema '${schemaName}' not found in bucket '${bucketName}'`);
|
|
213
|
+
}
|
|
214
|
+
const measurementId = existingSchema.id;
|
|
215
|
+
const updatePayload = {
|
|
216
|
+
columns: config.columns?.map((col) => ({
|
|
217
|
+
name: col.name,
|
|
218
|
+
type: col.type,
|
|
219
|
+
...(col.type === "field" &&
|
|
220
|
+
col.dataType && { dataType: col.dataType }),
|
|
221
|
+
})) || [],
|
|
222
|
+
};
|
|
223
|
+
await httpClient.patch(`/api/v2/buckets/${bucketId}/schema/measurements/${measurementId}`, updatePayload);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (error.response?.data) {
|
|
228
|
+
throw new Error(`Failed to update schema: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw new Error(`Failed to update schema '${schemaName}': ${error.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Common error handling for schema operations
|
|
237
|
+
*/
|
|
238
|
+
handleSchemaError(error, operation) {
|
|
239
|
+
const status = error.response?.status;
|
|
240
|
+
const originalMessage = error.response?.data?.message ||
|
|
241
|
+
error.response?.data?.error ||
|
|
242
|
+
(typeof error.response?.data === "string" ? error.response.data : null) ||
|
|
243
|
+
error.response?.statusText;
|
|
244
|
+
const formatError = (userMessage) => {
|
|
245
|
+
const parts = [`HTTP ${status}`, userMessage];
|
|
246
|
+
if (originalMessage) {
|
|
247
|
+
parts.push(`Server message: ${originalMessage}`);
|
|
248
|
+
}
|
|
249
|
+
return parts.join(" - ");
|
|
250
|
+
};
|
|
251
|
+
switch (status) {
|
|
252
|
+
case 400:
|
|
253
|
+
throw new Error(formatError("Bad Request: Invalid schema definition or parameters"));
|
|
254
|
+
case 401:
|
|
255
|
+
throw new Error(formatError("Unauthorized: Check your InfluxDB token permissions"));
|
|
256
|
+
case 403:
|
|
257
|
+
throw new Error(formatError("Forbidden: Token lacks permissions for schema operations"));
|
|
258
|
+
case 404:
|
|
259
|
+
throw new Error(formatError("Not Found: Schema or bucket does not exist"));
|
|
260
|
+
case 409:
|
|
261
|
+
throw new Error(formatError("Conflict: Schema already exists or conflicts with bucket settings"));
|
|
262
|
+
default:
|
|
263
|
+
throw new Error(`Failed to ${operation}: ${error.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InfluxDB Token Management Service
|
|
3
|
+
*
|
|
4
|
+
* Simplified service for essential token operations based on InfluxDB 3 Enterprise API
|
|
5
|
+
*/
|
|
6
|
+
import { InfluxProductType } from "../helpers/enums/influx-product-types.enum.js";
|
|
7
|
+
export class TokenManagementService {
|
|
8
|
+
baseService;
|
|
9
|
+
httpClient;
|
|
10
|
+
constructor(baseService) {
|
|
11
|
+
this.baseService = baseService;
|
|
12
|
+
this.httpClient = baseService.getInfluxHttpClient();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create a new admin token with full permissions
|
|
16
|
+
*/
|
|
17
|
+
async createAdminToken(token_name) {
|
|
18
|
+
this.baseService.validateOperationSupport("create_admin_token", [
|
|
19
|
+
InfluxProductType.Core,
|
|
20
|
+
InfluxProductType.Enterprise,
|
|
21
|
+
]);
|
|
22
|
+
this.baseService.validateManagementCapabilities();
|
|
23
|
+
try {
|
|
24
|
+
const name = token_name ||
|
|
25
|
+
`admin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
26
|
+
const requestBody = { token_name: name };
|
|
27
|
+
const response = await this.httpClient.post("/api/v3/configure/token/named_admin", requestBody);
|
|
28
|
+
if (response && response.token) {
|
|
29
|
+
return {
|
|
30
|
+
token: response.token,
|
|
31
|
+
id: response.id || response.token_id || "unknown",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new Error("Admin token creation failed: No token returned in response");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const errorMessage = error.response?.data || error.message;
|
|
40
|
+
const statusCode = error.response?.status;
|
|
41
|
+
throw new Error(`Failed to create admin token: ${errorMessage} (Status: ${statusCode})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* List admin tokens with optional filtering
|
|
46
|
+
*/
|
|
47
|
+
async listAdminTokens(filters) {
|
|
48
|
+
this.baseService.validateOperationSupport("list_admin_tokens", [
|
|
49
|
+
InfluxProductType.Core,
|
|
50
|
+
InfluxProductType.Enterprise,
|
|
51
|
+
]);
|
|
52
|
+
this.baseService.validateManagementCapabilities();
|
|
53
|
+
try {
|
|
54
|
+
let query = "SELECT * FROM system.tokens WHERE permissions LIKE '%*:*:*%'";
|
|
55
|
+
if (filters?.tokenName) {
|
|
56
|
+
query += ` AND name LIKE '%${filters.tokenName}%'`;
|
|
57
|
+
}
|
|
58
|
+
if (filters?.order) {
|
|
59
|
+
query += ` ORDER BY ${filters.order.field} ${filters.order.direction}`;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
query += " ORDER BY token_id ASC";
|
|
63
|
+
}
|
|
64
|
+
const payload = {
|
|
65
|
+
db: "_internal",
|
|
66
|
+
q: query,
|
|
67
|
+
format: "json",
|
|
68
|
+
params: {},
|
|
69
|
+
};
|
|
70
|
+
const response = await this.httpClient.post("/api/v3/query_sql", payload, {
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
Accept: "application/json",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return response;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const errorMessage = error.response?.data;
|
|
80
|
+
const _statusCode = error.response?.status;
|
|
81
|
+
throw new Error(`Failed to list admin tokens: ${errorMessage || error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* List resource tokens with optional filtering
|
|
86
|
+
*/
|
|
87
|
+
async listResourceTokens(filters) {
|
|
88
|
+
this.baseService.validateOperationSupport("list_resource_tokens", [
|
|
89
|
+
InfluxProductType.Core,
|
|
90
|
+
InfluxProductType.Enterprise,
|
|
91
|
+
]);
|
|
92
|
+
this.baseService.validateManagementCapabilities();
|
|
93
|
+
try {
|
|
94
|
+
let query = "SELECT * FROM system.tokens WHERE permissions NOT LIKE '%*:*:*%'";
|
|
95
|
+
if (filters?.databaseName) {
|
|
96
|
+
query += ` AND permissions LIKE '%${filters.databaseName}%'`;
|
|
97
|
+
}
|
|
98
|
+
if (filters?.tokenName) {
|
|
99
|
+
query += ` AND name LIKE '%${filters.tokenName}%'`;
|
|
100
|
+
}
|
|
101
|
+
if (filters?.order) {
|
|
102
|
+
query += ` ORDER BY ${filters.order.field} ${filters.order.direction}`;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
query += " ORDER BY token_id ASC";
|
|
106
|
+
}
|
|
107
|
+
const payload = {
|
|
108
|
+
db: "_internal",
|
|
109
|
+
q: query,
|
|
110
|
+
format: "json",
|
|
111
|
+
params: {},
|
|
112
|
+
};
|
|
113
|
+
const response = await this.httpClient.post("/api/v3/query_sql", payload, {
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
Accept: "application/json",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const errorMessage = error.response?.data || error.response?.statusText || error.message;
|
|
123
|
+
const statusCode = error.response?.status;
|
|
124
|
+
throw new Error(`Failed to list resource tokens: Status: ${statusCode}, Message: ${errorMessage}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Regenerate the operator token
|
|
129
|
+
*/
|
|
130
|
+
async regenerateOperatorToken() {
|
|
131
|
+
this.baseService.validateOperationSupport("regenerate_operator_token", [
|
|
132
|
+
InfluxProductType.Core,
|
|
133
|
+
InfluxProductType.Enterprise,
|
|
134
|
+
]);
|
|
135
|
+
this.baseService.validateManagementCapabilities();
|
|
136
|
+
try {
|
|
137
|
+
const response = await this.httpClient.post("/api/v3/configure/token/admin/regenerate");
|
|
138
|
+
if (response && response.token) {
|
|
139
|
+
return {
|
|
140
|
+
token: response.token,
|
|
141
|
+
id: response.id || response.token_id || "operator",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw new Error("Operator token regeneration failed: No token returned in response");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const errorMessage = error.response?.data || error.message;
|
|
150
|
+
const statusCode = error.response?.status;
|
|
151
|
+
throw new Error(`Failed to regenerate operator token: ${errorMessage} (Status: ${statusCode})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create a new resource token with specific database permissions
|
|
156
|
+
*/
|
|
157
|
+
async createResourceToken(description, permissions, expiry_secs) {
|
|
158
|
+
this.baseService.validateOperationSupport("create_resource_token", [
|
|
159
|
+
InfluxProductType.Core,
|
|
160
|
+
InfluxProductType.Enterprise,
|
|
161
|
+
]);
|
|
162
|
+
this.baseService.validateManagementCapabilities();
|
|
163
|
+
try {
|
|
164
|
+
const requestBody = {
|
|
165
|
+
token_name: description,
|
|
166
|
+
permissions: permissions,
|
|
167
|
+
};
|
|
168
|
+
if (expiry_secs) {
|
|
169
|
+
requestBody.expiry_secs = expiry_secs;
|
|
170
|
+
}
|
|
171
|
+
const influxType = this.baseService.getConnectionInfo().type;
|
|
172
|
+
const endpoint = influxType === "enterprise"
|
|
173
|
+
? "/api/v3/enterprise/configure/token"
|
|
174
|
+
: "/api/v3/configure/token";
|
|
175
|
+
const response = await this.httpClient.post(endpoint, requestBody);
|
|
176
|
+
if (response && response.token) {
|
|
177
|
+
return {
|
|
178
|
+
token: response.token,
|
|
179
|
+
id: response.id || response.token_id || "unknown",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
throw new Error("Resource token creation failed: No token returned in response");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const errorMessage = error.response?.data || error.message;
|
|
188
|
+
const statusCode = error.response?.status;
|
|
189
|
+
throw new Error(`Failed to create resource token: ${errorMessage} (Status: ${statusCode})`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Delete a token by name
|
|
194
|
+
*/
|
|
195
|
+
async deleteToken(token_name) {
|
|
196
|
+
if (!token_name)
|
|
197
|
+
throw new Error("token_name is required");
|
|
198
|
+
this.baseService.validateOperationSupport("delete_token", [
|
|
199
|
+
InfluxProductType.Core,
|
|
200
|
+
InfluxProductType.Enterprise,
|
|
201
|
+
]);
|
|
202
|
+
this.baseService.validateManagementCapabilities();
|
|
203
|
+
try {
|
|
204
|
+
const response = await this.httpClient.delete("/api/v3/configure/token", {
|
|
205
|
+
params: { token_name },
|
|
206
|
+
});
|
|
207
|
+
return response?.success !== false;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const errorMessage = error.response?.data || error.message;
|
|
211
|
+
const statusCode = error.response?.status;
|
|
212
|
+
throw new Error(`Failed to delete token: ${errorMessage} (Status: ${statusCode})`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InfluxDB Write Service
|
|
3
|
+
*
|
|
4
|
+
* Handles write operations using InfluxDB v3 line protocol API
|
|
5
|
+
* Note: InfluxDB v3 only supports writing via line protocol - no UPDATE/DELETE operations
|
|
6
|
+
*/
|
|
7
|
+
import { InfluxProductType } from "../helpers/enums/influx-product-types.enum.js";
|
|
8
|
+
export class WriteService {
|
|
9
|
+
baseService;
|
|
10
|
+
constructor(baseService) {
|
|
11
|
+
this.baseService = baseService;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Map full precision names to cloud-compatible short format
|
|
15
|
+
*/
|
|
16
|
+
mapPrecisionForCloud(precision) {
|
|
17
|
+
const precisionMap = {
|
|
18
|
+
nanosecond: "ns",
|
|
19
|
+
microsecond: "us",
|
|
20
|
+
millisecond: "ms",
|
|
21
|
+
second: "s",
|
|
22
|
+
};
|
|
23
|
+
return precisionMap[precision];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Write data (single entrypoint for all product types)
|
|
27
|
+
* For core/enterprise: HTTP API (/api/v3/write_lp)
|
|
28
|
+
* For cloud-dedicated: influxdb3 client
|
|
29
|
+
* For clustered: HTTP API (/api/v2/write)
|
|
30
|
+
* For cloud-serverless: influxdb3 client
|
|
31
|
+
*/
|
|
32
|
+
async writeLineProtocol(lineProtocolData, database, options) {
|
|
33
|
+
// Validate we have data capabilities for write operations
|
|
34
|
+
this.baseService.validateDataCapabilities();
|
|
35
|
+
const connectionInfo = this.baseService.getConnectionInfo();
|
|
36
|
+
switch (connectionInfo.type) {
|
|
37
|
+
case InfluxProductType.CloudDedicated:
|
|
38
|
+
return this.writeCloudDedicated(lineProtocolData, database, options);
|
|
39
|
+
case InfluxProductType.Clustered:
|
|
40
|
+
return this.writeClustered(lineProtocolData, database, options);
|
|
41
|
+
case InfluxProductType.CloudServerless:
|
|
42
|
+
return this.writeCloudServerless(lineProtocolData, database, options);
|
|
43
|
+
case InfluxProductType.Core:
|
|
44
|
+
case InfluxProductType.Enterprise:
|
|
45
|
+
return this.writeCoreEnterprise(lineProtocolData, database, options);
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unsupported InfluxDB product type: ${connectionInfo.type}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Write for core/enterprise (HTTP API)
|
|
52
|
+
*/
|
|
53
|
+
async writeCoreEnterprise(lineProtocolData, database, options) {
|
|
54
|
+
const { precision, acceptPartial = true, noSync = false } = options;
|
|
55
|
+
try {
|
|
56
|
+
const httpClient = this.baseService.getInfluxHttpClient();
|
|
57
|
+
const params = new URLSearchParams({
|
|
58
|
+
db: database,
|
|
59
|
+
precision,
|
|
60
|
+
accept_partial: acceptPartial.toString(),
|
|
61
|
+
no_sync: noSync.toString(),
|
|
62
|
+
});
|
|
63
|
+
await httpClient.post(`/api/v3/write_lp?${params.toString()}`, lineProtocolData, {
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.handleWriteError(error, database);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Write for cloud-dedicated/clustered (influxdb3 client)
|
|
76
|
+
*/
|
|
77
|
+
async writeCloudDedicated(lineProtocolData, database, options) {
|
|
78
|
+
try {
|
|
79
|
+
const client = this.baseService.getClient();
|
|
80
|
+
if (!client)
|
|
81
|
+
throw new Error("InfluxDB client not initialized");
|
|
82
|
+
const writeOptions = {};
|
|
83
|
+
if (options.precision) {
|
|
84
|
+
writeOptions.precision = options.precision;
|
|
85
|
+
}
|
|
86
|
+
await client.write(lineProtocolData, database, undefined, writeOptions);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
this.handleWriteError(error, database);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Write for clustered (HTTP API)
|
|
94
|
+
*/
|
|
95
|
+
async writeClustered(lineProtocolData, database, options) {
|
|
96
|
+
const { precision } = options;
|
|
97
|
+
try {
|
|
98
|
+
const httpClient = this.baseService.getInfluxHttpClient();
|
|
99
|
+
const params = new URLSearchParams({
|
|
100
|
+
bucket: database,
|
|
101
|
+
precision: this.mapPrecisionForCloud(precision),
|
|
102
|
+
});
|
|
103
|
+
await httpClient.post(`/api/v2/write?${params.toString()}`, lineProtocolData, {
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
106
|
+
Accept: "application/json",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.handleWriteError(error, database);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Write for cloud-serverless (influxdb3 client)
|
|
116
|
+
*/
|
|
117
|
+
async writeCloudServerless(lineProtocolData, database, options) {
|
|
118
|
+
try {
|
|
119
|
+
const client = this.baseService.getClient();
|
|
120
|
+
if (!client)
|
|
121
|
+
throw new Error("InfluxDB client not initialized");
|
|
122
|
+
const writeOptions = {};
|
|
123
|
+
if (options.precision) {
|
|
124
|
+
writeOptions.precision = this.mapPrecisionForCloud(options.precision);
|
|
125
|
+
}
|
|
126
|
+
await client.write(lineProtocolData, database, undefined, writeOptions);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.handleWriteError(error, database);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Centralized error handler for write methods
|
|
134
|
+
*/
|
|
135
|
+
handleWriteError(error, database) {
|
|
136
|
+
if (error.response?.status === 400) {
|
|
137
|
+
throw new Error(`Bad request: Invalid line protocol format or parameters`);
|
|
138
|
+
}
|
|
139
|
+
else if (error.response?.status === 401) {
|
|
140
|
+
throw new Error("Unauthorized: Check your InfluxDB token permissions");
|
|
141
|
+
}
|
|
142
|
+
else if (error.response?.status === 403) {
|
|
143
|
+
throw new Error("Access denied: Insufficient permissions for database operations");
|
|
144
|
+
}
|
|
145
|
+
else if (error.response?.status === 413) {
|
|
146
|
+
throw new Error("Request entity too large: Reduce the size of your line protocol data");
|
|
147
|
+
}
|
|
148
|
+
else if (error.response?.status === 422) {
|
|
149
|
+
throw new Error("Unprocessable entity: Invalid line protocol syntax");
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Failed to write data to database '${database}': ${error.response?.data || error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|