@forgehive/hive-sdk 0.1.3 → 0.1.5
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 +158 -19
- package/dist/index.d.ts +48 -1
- package/dist/index.js +312 -7
- package/dist/test/sendLogByUuid.test.d.ts +1 -0
- package/dist/test/sendLogByUuid.test.js +222 -0
- package/package.json +5 -3
- package/src/index.ts +396 -6
- package/src/test/sendLogByUuid.test.ts +272 -0
package/dist/index.js
CHANGED
|
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createHiveClient = exports.HiveClient = exports.createHiveLogClient = exports.HiveLogClient = void 0;
|
|
6
|
+
exports.createClientFromForgeConf = exports.createHiveClient = exports.HiveClient = exports.createHiveLogClient = exports.HiveLogClient = void 0;
|
|
7
7
|
exports.isApiError = isApiError;
|
|
8
8
|
exports.isInvokeError = isInvokeError;
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
10
|
const debug_1 = __importDefault(require("debug"));
|
|
11
|
+
const uuid_1 = require("uuid");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
13
|
const log = (0, debug_1.default)('hive-sdk');
|
|
12
14
|
// Type guard to check if response is an error
|
|
13
15
|
function isApiError(response) {
|
|
@@ -15,10 +17,12 @@ function isApiError(response) {
|
|
|
15
17
|
}
|
|
16
18
|
class HiveLogClient {
|
|
17
19
|
constructor(config) {
|
|
20
|
+
this.forgeConfig = null;
|
|
18
21
|
const apiKey = config.apiKey || process.env.HIVE_API_KEY;
|
|
19
22
|
const apiSecret = config.apiSecret || process.env.HIVE_API_SECRET;
|
|
20
23
|
const host = config.host || process.env.HIVE_HOST || 'https://www.forgehive.cloud';
|
|
21
24
|
this.projectName = config.projectName;
|
|
25
|
+
this.projectUuid = config.projectUuid || null;
|
|
22
26
|
this.baseMetadata = config.metadata || {};
|
|
23
27
|
if (!apiKey || !apiSecret) {
|
|
24
28
|
this.apiKey = null;
|
|
@@ -34,10 +38,117 @@ class HiveLogClient {
|
|
|
34
38
|
this.isInitialized = true;
|
|
35
39
|
log('HiveLogClient initialized for project "%s" with host "%s"', config.projectName, host);
|
|
36
40
|
}
|
|
41
|
+
// Load forge.json - use provided path or default to ./forge.json
|
|
42
|
+
const configPath = config.forgeConfigPath || './forge.json';
|
|
43
|
+
this.loadForgeConfig(configPath);
|
|
37
44
|
}
|
|
38
45
|
isActive() {
|
|
39
46
|
return this.isInitialized;
|
|
40
47
|
}
|
|
48
|
+
maskSecret(secret) {
|
|
49
|
+
if (!secret || secret.length <= 8) {
|
|
50
|
+
return secret ? '****' : 'null';
|
|
51
|
+
}
|
|
52
|
+
const first4 = secret.slice(0, 4);
|
|
53
|
+
const last4 = secret.slice(-4);
|
|
54
|
+
const middle = '*'.repeat(secret.length - 8);
|
|
55
|
+
return `${first4}${middle}${last4}`;
|
|
56
|
+
}
|
|
57
|
+
getConf() {
|
|
58
|
+
return {
|
|
59
|
+
projectName: this.projectName,
|
|
60
|
+
projectUuid: this.projectUuid,
|
|
61
|
+
host: this.host,
|
|
62
|
+
apiKey: this.maskSecret(this.apiKey),
|
|
63
|
+
apiSecret: this.maskSecret(this.apiSecret),
|
|
64
|
+
isInitialized: this.isInitialized,
|
|
65
|
+
baseMetadata: this.baseMetadata,
|
|
66
|
+
forgeConfig: this.forgeConfig
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async testConfig() {
|
|
70
|
+
if (!this.isInitialized) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: 'Client not initialized - missing API credentials'
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
// First verify credentials with /api/me
|
|
78
|
+
const meResponse = await axios_1.default.get(`${this.host}/api/me`, {
|
|
79
|
+
headers: {
|
|
80
|
+
'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
|
|
81
|
+
'Content-Type': 'application/json'
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (meResponse.status !== 200) {
|
|
85
|
+
const error = `Credential verification failed: HTTP ${meResponse.status}`;
|
|
86
|
+
log('Failed to verify credentials: %s', error);
|
|
87
|
+
return { success: false, error };
|
|
88
|
+
}
|
|
89
|
+
const meData = meResponse.data;
|
|
90
|
+
log('Successfully verified credentials for user "%s" in team "%s"', meData.user?.name, meData.team?.name);
|
|
91
|
+
// Then verify project exists if we have a projectUuid
|
|
92
|
+
let projectExists = false;
|
|
93
|
+
let projectName;
|
|
94
|
+
let tasksVerified;
|
|
95
|
+
if (this.projectUuid) {
|
|
96
|
+
try {
|
|
97
|
+
const projectResponse = await axios_1.default.get(`${this.host}/api/projects/${this.projectUuid}`, {
|
|
98
|
+
headers: {
|
|
99
|
+
'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
|
|
100
|
+
'Content-Type': 'application/json'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (projectResponse.status === 200) {
|
|
104
|
+
projectExists = true;
|
|
105
|
+
projectName = projectResponse.data.project?.projectName;
|
|
106
|
+
log('Successfully verified project "%s" exists with UUID "%s"', projectName, this.projectUuid);
|
|
107
|
+
// Verify tasks if we have forge config
|
|
108
|
+
if (this.forgeConfig && this.forgeConfig.tasks) {
|
|
109
|
+
const localTasks = Object.keys(this.forgeConfig.tasks);
|
|
110
|
+
const remoteTasks = projectResponse.data.project?.tasks || [];
|
|
111
|
+
const remoteTaskUuids = new Set(remoteTasks.map((task) => task.uuid));
|
|
112
|
+
const missing = [];
|
|
113
|
+
let found = 0;
|
|
114
|
+
for (const taskName of localTasks) {
|
|
115
|
+
const taskUuid = this.forgeConfig.tasks[taskName].uuid;
|
|
116
|
+
if (remoteTaskUuids.has(taskUuid)) {
|
|
117
|
+
found++;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
missing.push(taskName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
tasksVerified = {
|
|
124
|
+
total: localTasks.length,
|
|
125
|
+
found,
|
|
126
|
+
missing
|
|
127
|
+
};
|
|
128
|
+
log('Task verification: %d/%d tasks found, missing: %s', found, localTasks.length, missing.join(', '));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (projectError) {
|
|
133
|
+
log('Project verification failed for UUID "%s": %s', this.projectUuid, projectError instanceof Error ? projectError.message : String(projectError));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
teamName: meData.team?.name,
|
|
139
|
+
teamUuid: meData.team?.uuid,
|
|
140
|
+
userName: meData.user?.name,
|
|
141
|
+
projectName,
|
|
142
|
+
projectExists: this.projectUuid ? projectExists : undefined,
|
|
143
|
+
tasksVerified
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
const error = e instanceof Error ? e.message : 'Network error';
|
|
148
|
+
log('Error during config test: %s', error);
|
|
149
|
+
return { success: false, error };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
41
152
|
mergeMetadata(record, sendLogMetadata) {
|
|
42
153
|
// Start with base metadata from client
|
|
43
154
|
let finalMetadata = { ...this.baseMetadata };
|
|
@@ -58,13 +169,15 @@ class HiveLogClient {
|
|
|
58
169
|
log('Silent mode: Skipping sendLog for task "%s" - client not initialized', taskName);
|
|
59
170
|
return 'silent';
|
|
60
171
|
}
|
|
172
|
+
// Deprecation warning for legacy endpoint
|
|
173
|
+
log('DEPRECATION WARNING: sendLog() is deprecated. Use sendLogByUuid() with project and task UUIDs for enhanced features and better performance.');
|
|
61
174
|
try {
|
|
62
175
|
const logsUrl = `${this.host}/api/tasks/log-ingest`;
|
|
63
176
|
log('Sending log for task "%s" to %s', taskName, logsUrl);
|
|
64
177
|
const authToken = `${this.apiKey}:${this.apiSecret}`;
|
|
65
178
|
// Merge metadata with priority: sendLog > record.metadata > client
|
|
66
179
|
const finalMetadata = this.mergeMetadata(record, metadata);
|
|
67
|
-
// Create logItem with merged metadata
|
|
180
|
+
// Create logItem with merged metadata (no UUID generation for legacy method)
|
|
68
181
|
const logItem = {
|
|
69
182
|
...record,
|
|
70
183
|
taskName,
|
|
@@ -93,6 +206,52 @@ class HiveLogClient {
|
|
|
93
206
|
return 'error';
|
|
94
207
|
}
|
|
95
208
|
}
|
|
209
|
+
async sendLogByUuid(record, taskUuid, metadata) {
|
|
210
|
+
if (!this.isInitialized) {
|
|
211
|
+
log('Silent mode: Skipping sendLogByUuid for task UUID "%s" - client not initialized', taskUuid);
|
|
212
|
+
return 'silent';
|
|
213
|
+
}
|
|
214
|
+
if (!this.projectUuid) {
|
|
215
|
+
log('Error: sendLogByUuid requires projectUuid to be set in client config');
|
|
216
|
+
return 'error';
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const logsUrl = `${this.host}/api/log-ingest`;
|
|
220
|
+
log('Sending log for task UUID "%s" to %s', taskUuid, logsUrl);
|
|
221
|
+
const authToken = `${this.apiKey}:${this.apiSecret}`;
|
|
222
|
+
// Merge metadata with priority: sendLog > record.metadata > client
|
|
223
|
+
const finalMetadata = this.mergeMetadata(record, metadata);
|
|
224
|
+
// Ensure execution record has a UUID - generate one if missing
|
|
225
|
+
const recordWithUuid = {
|
|
226
|
+
...record,
|
|
227
|
+
uuid: record.uuid || (0, uuid_1.v7)(),
|
|
228
|
+
metadata: finalMetadata
|
|
229
|
+
};
|
|
230
|
+
// Create logItem with merged metadata and UUID
|
|
231
|
+
const logItem = recordWithUuid;
|
|
232
|
+
const response = await axios_1.default.post(logsUrl, {
|
|
233
|
+
projectUuid: this.projectUuid,
|
|
234
|
+
taskUuid,
|
|
235
|
+
logItem: JSON.stringify(logItem)
|
|
236
|
+
}, {
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Bearer ${authToken}`,
|
|
239
|
+
'Content-Type': 'application/json'
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
log('Success: Sent log for task UUID "%s"', taskUuid);
|
|
243
|
+
// Return the full response data if available
|
|
244
|
+
if (response.data && typeof response.data === 'object' && 'uuid' in response.data) {
|
|
245
|
+
return response.data;
|
|
246
|
+
}
|
|
247
|
+
return 'success';
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
const error = e;
|
|
251
|
+
log('Error: Failed to send log for task UUID "%s": %s', taskUuid, error.message);
|
|
252
|
+
return 'error';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
96
255
|
getListener() {
|
|
97
256
|
return async (record) => {
|
|
98
257
|
await this.sendLog(record);
|
|
@@ -148,6 +307,52 @@ class HiveLogClient {
|
|
|
148
307
|
return false;
|
|
149
308
|
}
|
|
150
309
|
}
|
|
310
|
+
loadForgeConfig(configPath) {
|
|
311
|
+
try {
|
|
312
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
313
|
+
const configContent = fs_1.default.readFileSync(configPath, 'utf8');
|
|
314
|
+
this.forgeConfig = JSON.parse(configContent);
|
|
315
|
+
log('Found forge.json configuration at %s', configPath);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
log('No forge.json configuration found at %s', configPath);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
log('Error loading forge.json: %s', error instanceof Error ? error.message : String(error));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
getTaskUUID(taskName) {
|
|
326
|
+
if (!this.forgeConfig) {
|
|
327
|
+
log('No forge.json configuration loaded, cannot get UUID for task "%s"', taskName);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const task = this.forgeConfig.tasks[taskName];
|
|
331
|
+
if (!task) {
|
|
332
|
+
log('Task "%s" not found in forge.json configuration', taskName);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
log('Found UUID "%s" for task "%s"', task.uuid, taskName);
|
|
336
|
+
return task.uuid;
|
|
337
|
+
}
|
|
338
|
+
async sendLogByName(taskName, record, metadata) {
|
|
339
|
+
if (!this.isInitialized) {
|
|
340
|
+
log('Silent mode: Skipping sendLogByName for task "%s" - client not initialized', taskName);
|
|
341
|
+
return 'silent';
|
|
342
|
+
}
|
|
343
|
+
if (!this.projectUuid) {
|
|
344
|
+
log('Error: sendLogByName requires projectUuid to be set in client config');
|
|
345
|
+
return 'error';
|
|
346
|
+
}
|
|
347
|
+
const taskUuid = this.getTaskUUID(taskName);
|
|
348
|
+
if (!taskUuid) {
|
|
349
|
+
log('Error: Cannot find UUID for task "%s" in forge.json', taskName);
|
|
350
|
+
return 'error';
|
|
351
|
+
}
|
|
352
|
+
// Use the existing sendLogByUuid method
|
|
353
|
+
log('Sending log for task "%s" with uuid "%s"', taskName, taskUuid);
|
|
354
|
+
return await this.sendLogByUuid(record, taskUuid, metadata);
|
|
355
|
+
}
|
|
151
356
|
}
|
|
152
357
|
exports.HiveLogClient = HiveLogClient;
|
|
153
358
|
const createHiveLogClient = (config) => {
|
|
@@ -173,10 +378,77 @@ class HiveClient {
|
|
|
173
378
|
this.apiSecret = apiSecret;
|
|
174
379
|
log('HiveClient initialized for project "%s" with host "%s"', config.projectUuid, host);
|
|
175
380
|
}
|
|
176
|
-
|
|
381
|
+
maskSecret(secret) {
|
|
382
|
+
if (secret.length <= 8) {
|
|
383
|
+
return '****';
|
|
384
|
+
}
|
|
385
|
+
const first4 = secret.slice(0, 4);
|
|
386
|
+
const last4 = secret.slice(-4);
|
|
387
|
+
const middle = '*'.repeat(secret.length - 8);
|
|
388
|
+
return `${first4}${middle}${last4}`;
|
|
389
|
+
}
|
|
390
|
+
getConf() {
|
|
391
|
+
return {
|
|
392
|
+
projectUuid: this.projectUuid,
|
|
393
|
+
host: this.host,
|
|
394
|
+
apiKey: this.maskSecret(this.apiKey),
|
|
395
|
+
apiSecret: this.maskSecret(this.apiSecret)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async testConfig() {
|
|
399
|
+
try {
|
|
400
|
+
// First verify credentials with /api/me
|
|
401
|
+
const meResponse = await axios_1.default.get(`${this.host}/api/me`, {
|
|
402
|
+
headers: {
|
|
403
|
+
'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
|
|
404
|
+
'Content-Type': 'application/json'
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
if (meResponse.status !== 200) {
|
|
408
|
+
const error = `Credential verification failed: HTTP ${meResponse.status}`;
|
|
409
|
+
log('Failed to verify credentials: %s', error);
|
|
410
|
+
return { success: false, error };
|
|
411
|
+
}
|
|
412
|
+
const meData = meResponse.data;
|
|
413
|
+
log('Successfully verified credentials for user "%s" in team "%s"', meData.user?.name, meData.team?.name);
|
|
414
|
+
// Then verify project exists
|
|
415
|
+
let projectExists = false;
|
|
416
|
+
let projectName;
|
|
417
|
+
try {
|
|
418
|
+
const projectResponse = await axios_1.default.get(`${this.host}/api/projects/${this.projectUuid}`, {
|
|
419
|
+
headers: {
|
|
420
|
+
'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
|
|
421
|
+
'Content-Type': 'application/json'
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
if (projectResponse.status === 200) {
|
|
425
|
+
projectExists = true;
|
|
426
|
+
projectName = projectResponse.data.project?.projectName;
|
|
427
|
+
log('Successfully verified project "%s" exists with UUID "%s"', projectName, this.projectUuid);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (projectError) {
|
|
431
|
+
log('Project verification failed for UUID "%s": %s', this.projectUuid, projectError instanceof Error ? projectError.message : String(projectError));
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
success: true,
|
|
435
|
+
teamName: meData.team?.name,
|
|
436
|
+
teamUuid: meData.team?.uuid,
|
|
437
|
+
userName: meData.user?.name,
|
|
438
|
+
projectName,
|
|
439
|
+
projectExists
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
const error = e instanceof Error ? e.message : 'Network error';
|
|
444
|
+
log('Error during config test: %s', error);
|
|
445
|
+
return { success: false, error };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async invoke(taskUuid, payload) {
|
|
177
449
|
try {
|
|
178
|
-
const invokeUrl = `${this.host}/api/
|
|
179
|
-
log('Invoking task "%s" at %s',
|
|
450
|
+
const invokeUrl = `${this.host}/api/projects/${this.projectUuid}/tasks/${taskUuid}/invoke`;
|
|
451
|
+
log('Invoking task UUID "%s" at %s', taskUuid, invokeUrl);
|
|
180
452
|
const authToken = `${this.apiKey}:${this.apiSecret}`;
|
|
181
453
|
const response = await axios_1.default.post(invokeUrl, {
|
|
182
454
|
payload
|
|
@@ -186,12 +458,12 @@ class HiveClient {
|
|
|
186
458
|
'Content-Type': 'application/json'
|
|
187
459
|
}
|
|
188
460
|
});
|
|
189
|
-
log('Success: Invoked task "%s"',
|
|
461
|
+
log('Success: Invoked task UUID "%s"', taskUuid);
|
|
190
462
|
return response.data;
|
|
191
463
|
}
|
|
192
464
|
catch (e) {
|
|
193
465
|
const error = e;
|
|
194
|
-
log('Error: Failed to invoke task "%s": %s',
|
|
466
|
+
log('Error: Failed to invoke task UUID "%s": %s', taskUuid, error.message);
|
|
195
467
|
// Check if it's an axios error with response data
|
|
196
468
|
if (axios_1.default.isAxiosError(error) && error.response?.data) {
|
|
197
469
|
return error.response.data;
|
|
@@ -206,3 +478,36 @@ const createHiveClient = (config) => {
|
|
|
206
478
|
return new HiveClient(config);
|
|
207
479
|
};
|
|
208
480
|
exports.createHiveClient = createHiveClient;
|
|
481
|
+
/**
|
|
482
|
+
* Create a HiveLogClient from forge.json configuration
|
|
483
|
+
* @param forgeConfigPath Path to forge.json file (defaults to './forge.json')
|
|
484
|
+
* @param additionalConfig Additional config options to override forge.json values
|
|
485
|
+
* @returns HiveLogClient configured from forge.json
|
|
486
|
+
*/
|
|
487
|
+
const createClientFromForgeConf = (forgeConfigPath = './forge.json', additionalConfig = {}) => {
|
|
488
|
+
log('Creating HiveLogClient from forge.json at "%s"', forgeConfigPath);
|
|
489
|
+
let forgeConfig = null;
|
|
490
|
+
try {
|
|
491
|
+
if (fs_1.default.existsSync(forgeConfigPath)) {
|
|
492
|
+
const configContent = fs_1.default.readFileSync(forgeConfigPath, 'utf8');
|
|
493
|
+
forgeConfig = JSON.parse(configContent);
|
|
494
|
+
log('Loaded forge.json configuration from %s', forgeConfigPath);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
log('No forge.json found at %s', forgeConfigPath);
|
|
498
|
+
throw new Error(`forge.json not found at ${forgeConfigPath}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
log('Error loading forge.json: %s', error instanceof Error ? error.message : String(error));
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
const config = {
|
|
506
|
+
projectName: forgeConfig.project.name,
|
|
507
|
+
projectUuid: forgeConfig.project.uuid,
|
|
508
|
+
forgeConfigPath,
|
|
509
|
+
...additionalConfig // Allow overriding any config values
|
|
510
|
+
};
|
|
511
|
+
return new HiveLogClient(config);
|
|
512
|
+
};
|
|
513
|
+
exports.createClientFromForgeConf = createClientFromForgeConf;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const axios_1 = __importDefault(require("axios"));
|
|
7
|
+
const index_1 = require("../index");
|
|
8
|
+
// Mock axios
|
|
9
|
+
jest.mock('axios');
|
|
10
|
+
const mockedAxios = axios_1.default;
|
|
11
|
+
describe('HiveLogClient sendLogByUuid', () => {
|
|
12
|
+
const testConfig = {
|
|
13
|
+
projectName: 'test-project',
|
|
14
|
+
projectUuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
15
|
+
apiKey: 'test-key',
|
|
16
|
+
apiSecret: 'test-secret',
|
|
17
|
+
host: 'https://test.example.com'
|
|
18
|
+
};
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('UUID Generation', () => {
|
|
23
|
+
it('should generate UUID v7 when execution record has no UUID', async () => {
|
|
24
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
25
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
26
|
+
const executionRecord = {
|
|
27
|
+
input: { value: 'test-input' },
|
|
28
|
+
output: { result: 'test-output' },
|
|
29
|
+
taskName: 'test-task',
|
|
30
|
+
type: 'success',
|
|
31
|
+
boundaries: {},
|
|
32
|
+
metadata: {}
|
|
33
|
+
};
|
|
34
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
35
|
+
expect(result).toBe('success');
|
|
36
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
37
|
+
// Check that the request was made with a UUID
|
|
38
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
39
|
+
const requestBody = callArgs[1];
|
|
40
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
41
|
+
expect(logItem.uuid).toBeDefined();
|
|
42
|
+
expect(typeof logItem.uuid).toBe('string');
|
|
43
|
+
expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); // UUID v7 pattern
|
|
44
|
+
});
|
|
45
|
+
it('should preserve existing UUID when execution record already has one', async () => {
|
|
46
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
47
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
48
|
+
const existingUuid = '01234567-89ab-7def-8123-456789abcdef';
|
|
49
|
+
const executionRecord = {
|
|
50
|
+
uuid: existingUuid,
|
|
51
|
+
input: { value: 'test-input' },
|
|
52
|
+
output: { result: 'test-output' },
|
|
53
|
+
taskName: 'test-task',
|
|
54
|
+
type: 'success',
|
|
55
|
+
boundaries: {},
|
|
56
|
+
metadata: {}
|
|
57
|
+
};
|
|
58
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
59
|
+
expect(result).toBe('success');
|
|
60
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
61
|
+
// Check that the existing UUID was preserved
|
|
62
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
63
|
+
const requestBody = callArgs[1];
|
|
64
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
65
|
+
expect(logItem.uuid).toBe(existingUuid);
|
|
66
|
+
});
|
|
67
|
+
it('should generate UUID v7 when execution record has empty UUID', async () => {
|
|
68
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
69
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
70
|
+
const executionRecord = {
|
|
71
|
+
uuid: '', // Empty string should trigger UUID generation
|
|
72
|
+
input: { value: 'test-input' },
|
|
73
|
+
output: { result: 'test-output' },
|
|
74
|
+
taskName: 'test-task',
|
|
75
|
+
type: 'success',
|
|
76
|
+
boundaries: {},
|
|
77
|
+
metadata: {}
|
|
78
|
+
};
|
|
79
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
80
|
+
expect(result).toBe('success');
|
|
81
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
82
|
+
// Check that a new UUID was generated (not empty string)
|
|
83
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
84
|
+
const requestBody = callArgs[1];
|
|
85
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
86
|
+
expect(logItem.uuid).toBeDefined();
|
|
87
|
+
expect(logItem.uuid).not.toBe('');
|
|
88
|
+
expect(typeof logItem.uuid).toBe('string');
|
|
89
|
+
expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); // UUID v7 pattern
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('API Integration', () => {
|
|
93
|
+
it('should send correct request to log-ingest endpoint', async () => {
|
|
94
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
95
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
96
|
+
const taskUuid = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
|
|
97
|
+
const executionRecord = {
|
|
98
|
+
input: { userId: 123 },
|
|
99
|
+
output: { result: 'processed' },
|
|
100
|
+
taskName: 'process-user',
|
|
101
|
+
type: 'success',
|
|
102
|
+
boundaries: {},
|
|
103
|
+
metadata: { session: 'abc123' }
|
|
104
|
+
};
|
|
105
|
+
await client.sendLogByUuid(executionRecord, taskUuid);
|
|
106
|
+
expect(mockedAxios.post).toHaveBeenCalledWith('https://test.example.com/api/log-ingest', {
|
|
107
|
+
projectUuid: testConfig.projectUuid,
|
|
108
|
+
taskUuid: taskUuid,
|
|
109
|
+
logItem: expect.any(String)
|
|
110
|
+
}, {
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': 'Bearer test-key:test-secret',
|
|
113
|
+
'Content-Type': 'application/json'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// Verify the logItem contains the execution record with UUID
|
|
117
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
118
|
+
const requestBody = callArgs[1];
|
|
119
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
120
|
+
expect(logItem.uuid).toBeDefined();
|
|
121
|
+
expect(logItem.input).toEqual({ userId: 123 });
|
|
122
|
+
expect(logItem.output).toEqual({ result: 'processed' });
|
|
123
|
+
expect(logItem.taskName).toBe('process-user');
|
|
124
|
+
expect(logItem.type).toBe('success');
|
|
125
|
+
expect(logItem.metadata).toEqual({ session: 'abc123' });
|
|
126
|
+
});
|
|
127
|
+
it('should return "silent" when client is not initialized', async () => {
|
|
128
|
+
const client = new index_1.HiveLogClient({
|
|
129
|
+
projectName: 'test-project',
|
|
130
|
+
projectUuid: testConfig.projectUuid
|
|
131
|
+
// Missing apiKey and apiSecret
|
|
132
|
+
});
|
|
133
|
+
const executionRecord = {
|
|
134
|
+
input: { value: 'test' },
|
|
135
|
+
taskName: 'test-task',
|
|
136
|
+
type: 'success',
|
|
137
|
+
boundaries: {},
|
|
138
|
+
metadata: {}
|
|
139
|
+
};
|
|
140
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
141
|
+
expect(result).toBe('silent');
|
|
142
|
+
expect(mockedAxios.post).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
it('should return "error" when projectUuid is missing', async () => {
|
|
145
|
+
const client = new index_1.HiveLogClient({
|
|
146
|
+
projectName: 'test-project',
|
|
147
|
+
apiKey: 'test-key',
|
|
148
|
+
apiSecret: 'test-secret',
|
|
149
|
+
host: 'https://test.example.com'
|
|
150
|
+
// Missing projectUuid
|
|
151
|
+
});
|
|
152
|
+
const executionRecord = {
|
|
153
|
+
input: { value: 'test' },
|
|
154
|
+
taskName: 'test-task',
|
|
155
|
+
type: 'success',
|
|
156
|
+
boundaries: {},
|
|
157
|
+
metadata: {}
|
|
158
|
+
};
|
|
159
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
160
|
+
expect(result).toBe('error');
|
|
161
|
+
expect(mockedAxios.post).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
it('should handle API error responses', async () => {
|
|
164
|
+
mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
|
|
165
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
166
|
+
const executionRecord = {
|
|
167
|
+
input: { value: 'test' },
|
|
168
|
+
taskName: 'test-task',
|
|
169
|
+
type: 'success',
|
|
170
|
+
boundaries: {},
|
|
171
|
+
metadata: {}
|
|
172
|
+
};
|
|
173
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
174
|
+
expect(result).toBe('error');
|
|
175
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
it('should return response data when API returns object with uuid', async () => {
|
|
178
|
+
const responseData = { uuid: 'log-uuid-123', success: true };
|
|
179
|
+
mockedAxios.post.mockResolvedValueOnce({ data: responseData });
|
|
180
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
181
|
+
const executionRecord = {
|
|
182
|
+
input: { value: 'test' },
|
|
183
|
+
taskName: 'test-task',
|
|
184
|
+
type: 'success',
|
|
185
|
+
boundaries: {},
|
|
186
|
+
metadata: {}
|
|
187
|
+
};
|
|
188
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
189
|
+
expect(result).toEqual(responseData);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('Metadata Handling', () => {
|
|
193
|
+
it('should merge metadata correctly with UUID generation', async () => {
|
|
194
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
195
|
+
const baseMetadata = { environment: 'test', version: '1.0' };
|
|
196
|
+
const client = new index_1.HiveLogClient({ ...testConfig, metadata: baseMetadata });
|
|
197
|
+
const recordMetadata = { session: 'abc123' };
|
|
198
|
+
const sendLogMetadata = { priority: 'high' };
|
|
199
|
+
const executionRecord = {
|
|
200
|
+
input: { value: 'test' },
|
|
201
|
+
taskName: 'test-task',
|
|
202
|
+
type: 'success',
|
|
203
|
+
boundaries: {},
|
|
204
|
+
metadata: recordMetadata
|
|
205
|
+
};
|
|
206
|
+
await client.sendLogByUuid(executionRecord, 'task-uuid-123', sendLogMetadata);
|
|
207
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
208
|
+
const requestBody = callArgs[1];
|
|
209
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
210
|
+
// Should have UUID generated
|
|
211
|
+
expect(logItem.uuid).toBeDefined();
|
|
212
|
+
expect(typeof logItem.uuid).toBe('string');
|
|
213
|
+
// Should have merged metadata (sendLog > record > client)
|
|
214
|
+
expect(logItem.metadata).toEqual({
|
|
215
|
+
environment: 'test', // from client
|
|
216
|
+
version: '1.0', // from client
|
|
217
|
+
session: 'abc123', // from record
|
|
218
|
+
priority: 'high' // from sendLog (highest priority)
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgehive/hive-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@forgehive/task": "^0.2.
|
|
10
|
+
"@forgehive/task": "^0.2.6"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/jest": "^29.5.14",
|
|
15
15
|
"@types/node": "^24.0.3",
|
|
16
|
+
"@types/uuid": "^10.0.0",
|
|
16
17
|
"jest": "^29.7.0",
|
|
17
18
|
"ts-jest": "^29.1.2"
|
|
18
19
|
},
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"axios": "^1.8.4",
|
|
24
25
|
"debug": "^4.4.1",
|
|
25
|
-
"
|
|
26
|
+
"uuid": "^11.1.0",
|
|
27
|
+
"@forgehive/task": "0.2.6"
|
|
26
28
|
},
|
|
27
29
|
"scripts": {
|
|
28
30
|
"build": "tsc",
|