@harborclient/team-hub 0.4.1 → 0.4.2
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/dist/cli.js +611 -4
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -364,6 +364,7 @@ var REQUESTS_COLLECTION = "requests";
|
|
|
364
364
|
var AUDIT_LOG_COLLECTION = "auditLog";
|
|
365
365
|
var LLM_USAGE_COLLECTION = "llmUsage";
|
|
366
366
|
var LLM_USAGE_LOG_COLLECTION = "llmUsageLog";
|
|
367
|
+
var RUN_RESULTS_COLLECTION = "runResults";
|
|
367
368
|
var WRITE_BATCH_LIMIT = 500;
|
|
368
369
|
|
|
369
370
|
// src/db/systemUsers.ts
|
|
@@ -400,7 +401,7 @@ var firestoreConfigSchema = z2.object({
|
|
|
400
401
|
|
|
401
402
|
// src/db/firestore/utils.ts
|
|
402
403
|
function parseAuditEntityType(value) {
|
|
403
|
-
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
|
|
404
|
+
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
|
|
404
405
|
return value;
|
|
405
406
|
}
|
|
406
407
|
throw new Error(`Invalid audit entity type: ${value}`);
|
|
@@ -544,6 +545,19 @@ function mapFirestoreRequest(id, data) {
|
|
|
544
545
|
updatedByUserId: data.updatedByUserId ?? null
|
|
545
546
|
};
|
|
546
547
|
}
|
|
548
|
+
function mapFirestoreRunResult(id, data) {
|
|
549
|
+
return {
|
|
550
|
+
id,
|
|
551
|
+
kind: data.kind,
|
|
552
|
+
label: data.label,
|
|
553
|
+
collectionName: data.collectionName,
|
|
554
|
+
requestName: data.requestName,
|
|
555
|
+
summary: data.summary,
|
|
556
|
+
payload: data.payload,
|
|
557
|
+
createdAt: data.createdAt,
|
|
558
|
+
createdByUserId: data.createdByUserId ?? null
|
|
559
|
+
};
|
|
560
|
+
}
|
|
547
561
|
function mapFirestoreAuditLog(id, data) {
|
|
548
562
|
return {
|
|
549
563
|
id,
|
|
@@ -557,6 +571,53 @@ function mapFirestoreAuditLog(id, data) {
|
|
|
557
571
|
};
|
|
558
572
|
}
|
|
559
573
|
|
|
574
|
+
// src/db/runResultPayload.ts
|
|
575
|
+
function isRecord(value) {
|
|
576
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
577
|
+
}
|
|
578
|
+
function summarizeResultRows(results) {
|
|
579
|
+
let passed = 0;
|
|
580
|
+
let failed = 0;
|
|
581
|
+
let skipped = 0;
|
|
582
|
+
for (const row of results) {
|
|
583
|
+
if (!isRecord(row)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const status = row.status;
|
|
587
|
+
if (status === "passed") {
|
|
588
|
+
passed += 1;
|
|
589
|
+
} else if (status === "failed") {
|
|
590
|
+
failed += 1;
|
|
591
|
+
} else if (status === "skipped") {
|
|
592
|
+
skipped += 1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { passed, failed, skipped };
|
|
596
|
+
}
|
|
597
|
+
function buildDefaultRunResultLabel(metadata) {
|
|
598
|
+
const target = metadata.requestName ?? metadata.collectionName ?? "Run";
|
|
599
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
600
|
+
return `${target} \u2014 ${timestamp}`;
|
|
601
|
+
}
|
|
602
|
+
function parseRunResultPayload(payload) {
|
|
603
|
+
const kind = payload.harborclientExport;
|
|
604
|
+
if (kind !== "collection-run-results" && kind !== "request-run-results") {
|
|
605
|
+
throw new Error("Invalid run result payload: harborclientExport is required");
|
|
606
|
+
}
|
|
607
|
+
const results = payload.results;
|
|
608
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
609
|
+
throw new Error("Invalid run result payload: results are required");
|
|
610
|
+
}
|
|
611
|
+
const collection = isRecord(payload.collection) ? payload.collection : void 0;
|
|
612
|
+
const request = isRecord(payload.request) ? payload.request : void 0;
|
|
613
|
+
return {
|
|
614
|
+
kind,
|
|
615
|
+
collectionName: typeof collection?.name === "string" ? collection.name : null,
|
|
616
|
+
requestName: typeof request?.name === "string" ? request.name : null,
|
|
617
|
+
summary: summarizeResultRows(results)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
560
621
|
// src/db/trimRequiredName.ts
|
|
561
622
|
function trimRequiredName(name, label) {
|
|
562
623
|
const trimmed = name.trim();
|
|
@@ -1822,6 +1883,78 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1822
1883
|
}
|
|
1823
1884
|
return usage;
|
|
1824
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Lists run results saved by the given user, newest first.
|
|
1888
|
+
*
|
|
1889
|
+
* @param userId - Owning user identifier.
|
|
1890
|
+
*/
|
|
1891
|
+
async listRunResultsForUser(userId) {
|
|
1892
|
+
const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).where("createdByUserId", "==", userId).orderBy("createdAt", "desc").get();
|
|
1893
|
+
return snapshot.docs.map(
|
|
1894
|
+
(doc) => mapFirestoreRunResult(doc.id, doc.data())
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Lists all run results for admin inspection, newest first.
|
|
1899
|
+
*/
|
|
1900
|
+
async listAllRunResults() {
|
|
1901
|
+
const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).orderBy("createdAt", "desc").get();
|
|
1902
|
+
return snapshot.docs.map(
|
|
1903
|
+
(doc) => mapFirestoreRunResult(doc.id, doc.data())
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Creates a standalone run result snapshot.
|
|
1908
|
+
*
|
|
1909
|
+
* @param input - HarborClient export payload and optional label.
|
|
1910
|
+
* @param actingUserId - User performing the create action.
|
|
1911
|
+
*/
|
|
1912
|
+
async createRunResult(input, actingUserId) {
|
|
1913
|
+
const metadata = parseRunResultPayload(input.payload);
|
|
1914
|
+
const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
|
|
1915
|
+
const id = randomUUID2();
|
|
1916
|
+
const now = /* @__PURE__ */ new Date();
|
|
1917
|
+
const data = {
|
|
1918
|
+
kind: metadata.kind,
|
|
1919
|
+
label,
|
|
1920
|
+
collectionName: metadata.collectionName,
|
|
1921
|
+
requestName: metadata.requestName,
|
|
1922
|
+
summary: metadata.summary,
|
|
1923
|
+
payload: input.payload,
|
|
1924
|
+
createdAt: now,
|
|
1925
|
+
createdByUserId: actingUserId
|
|
1926
|
+
};
|
|
1927
|
+
await this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id).set(data);
|
|
1928
|
+
await this.recordAuditEntry(actingUserId, "create", "run_result", id);
|
|
1929
|
+
return mapFirestoreRunResult(id, data);
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Finds a run result by stable identifier.
|
|
1933
|
+
*
|
|
1934
|
+
* @param id - Run result ID to look up.
|
|
1935
|
+
*/
|
|
1936
|
+
async findRunResultById(id) {
|
|
1937
|
+
const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id).get();
|
|
1938
|
+
if (!snapshot.exists) {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
return mapFirestoreRunResult(id, snapshot.data());
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Deletes a run result.
|
|
1945
|
+
*
|
|
1946
|
+
* @param id - Run result ID to delete.
|
|
1947
|
+
* @param actingUserId - User performing the delete action.
|
|
1948
|
+
*/
|
|
1949
|
+
async deleteRunResult(id, actingUserId) {
|
|
1950
|
+
const docRef = this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id);
|
|
1951
|
+
const snapshot = await docRef.get();
|
|
1952
|
+
if (!snapshot.exists) {
|
|
1953
|
+
throw new Error("Run result not found");
|
|
1954
|
+
}
|
|
1955
|
+
await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
|
|
1956
|
+
await docRef.delete();
|
|
1957
|
+
}
|
|
1825
1958
|
/**
|
|
1826
1959
|
* Inserts a per-request LLM usage log entry.
|
|
1827
1960
|
*
|
|
@@ -1976,7 +2109,7 @@ function parseAuditAction(value) {
|
|
|
1976
2109
|
throw new Error(`Invalid audit action: ${value}`);
|
|
1977
2110
|
}
|
|
1978
2111
|
function parseAuditEntityType2(value) {
|
|
1979
|
-
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
|
|
2112
|
+
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
|
|
1980
2113
|
return value;
|
|
1981
2114
|
}
|
|
1982
2115
|
throw new Error(`Invalid audit entity type: ${value}`);
|
|
@@ -2073,6 +2206,23 @@ function mapSnippetSqlRow(row) {
|
|
|
2073
2206
|
deletionLocked: Boolean(row.deletion_locked)
|
|
2074
2207
|
};
|
|
2075
2208
|
}
|
|
2209
|
+
function mapRunResultSqlRow(row) {
|
|
2210
|
+
return {
|
|
2211
|
+
id: row.id,
|
|
2212
|
+
kind: row.kind,
|
|
2213
|
+
label: row.label,
|
|
2214
|
+
collectionName: row.collection_name,
|
|
2215
|
+
requestName: row.request_name,
|
|
2216
|
+
summary: {
|
|
2217
|
+
passed: row.summary_passed,
|
|
2218
|
+
failed: row.summary_failed,
|
|
2219
|
+
skipped: row.summary_skipped
|
|
2220
|
+
},
|
|
2221
|
+
payload: parseJson(row.payload, {}),
|
|
2222
|
+
createdAt: row.created_at,
|
|
2223
|
+
createdByUserId: row.created_by_user_id ?? null
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2076
2226
|
function mapFolderSqlRow(row) {
|
|
2077
2227
|
return {
|
|
2078
2228
|
id: row.id,
|
|
@@ -2349,6 +2499,22 @@ WHERE role = 'user'
|
|
|
2349
2499
|
AND snippet_access = '[]'
|
|
2350
2500
|
AND collection_access LIKE '%"*"%';
|
|
2351
2501
|
`.trim();
|
|
2502
|
+
var RUN_RESULTS_MIGRATION_SQL = `
|
|
2503
|
+
CREATE TABLE IF NOT EXISTS run_results (
|
|
2504
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
2505
|
+
kind VARCHAR(64) NOT NULL,
|
|
2506
|
+
label VARCHAR(512) NOT NULL,
|
|
2507
|
+
collection_name VARCHAR(512),
|
|
2508
|
+
request_name VARCHAR(512),
|
|
2509
|
+
summary_passed INT NOT NULL DEFAULT 0,
|
|
2510
|
+
summary_failed INT NOT NULL DEFAULT 0,
|
|
2511
|
+
summary_skipped INT NOT NULL DEFAULT 0,
|
|
2512
|
+
payload LONGTEXT NOT NULL,
|
|
2513
|
+
created_at DATETIME NOT NULL,
|
|
2514
|
+
created_by_user_id VARCHAR(36),
|
|
2515
|
+
INDEX run_results_created_idx (created_at)
|
|
2516
|
+
)
|
|
2517
|
+
`.trim();
|
|
2352
2518
|
var MYSQL_MIGRATIONS = [
|
|
2353
2519
|
USERS_MIGRATION_SQL,
|
|
2354
2520
|
API_TOKENS_MIGRATION_SQL,
|
|
@@ -2374,7 +2540,8 @@ var MYSQL_MIGRATIONS = [
|
|
|
2374
2540
|
COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
|
|
2375
2541
|
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
|
|
2376
2542
|
USERS_SNIPPET_ACCESS_MIGRATION_SQL,
|
|
2377
|
-
USERS_SNIPPET_ACCESS_BACKFILL_SQL
|
|
2543
|
+
USERS_SNIPPET_ACCESS_BACKFILL_SQL,
|
|
2544
|
+
RUN_RESULTS_MIGRATION_SQL
|
|
2378
2545
|
];
|
|
2379
2546
|
|
|
2380
2547
|
// src/db/mysql/schemas.ts
|
|
@@ -2478,6 +2645,8 @@ var LLM_USAGE_SELECT_COLUMNS = `id, user_id, period, prompt_tokens, completion_t
|
|
|
2478
2645
|
var COLLECTION_SELECT = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
|
|
2479
2646
|
var ENVIRONMENT_SELECT = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
|
|
2480
2647
|
var SNIPPET_SELECT = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
|
|
2648
|
+
var RUN_RESULT_SELECT_COLUMNS = "id, kind, label, collection_name, request_name, summary_passed, summary_failed, summary_skipped, payload, created_at, created_by_user_id";
|
|
2649
|
+
var RUN_RESULT_SELECT = `SELECT ${RUN_RESULT_SELECT_COLUMNS} FROM run_results`;
|
|
2481
2650
|
var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
|
|
2482
2651
|
var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
|
|
2483
2652
|
var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
|
|
@@ -3883,6 +4052,93 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3883
4052
|
);
|
|
3884
4053
|
return rows.map(mapLlmUsageLogSqlRow);
|
|
3885
4054
|
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Lists run results saved by the given user, newest first.
|
|
4057
|
+
*/
|
|
4058
|
+
async listRunResultsForUser(userId) {
|
|
4059
|
+
const rows = await this.queryRows(
|
|
4060
|
+
`${RUN_RESULT_SELECT} WHERE created_by_user_id = ? ORDER BY created_at DESC`,
|
|
4061
|
+
[userId]
|
|
4062
|
+
);
|
|
4063
|
+
return rows.map(mapRunResultSqlRow);
|
|
4064
|
+
}
|
|
4065
|
+
/**
|
|
4066
|
+
* Lists all run results for admin inspection, newest first.
|
|
4067
|
+
*/
|
|
4068
|
+
async listAllRunResults() {
|
|
4069
|
+
const rows = await this.queryRows(
|
|
4070
|
+
`${RUN_RESULT_SELECT} ORDER BY created_at DESC`
|
|
4071
|
+
);
|
|
4072
|
+
return rows.map(mapRunResultSqlRow);
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* Creates a standalone run result snapshot.
|
|
4076
|
+
*/
|
|
4077
|
+
async createRunResult(input, actingUserId) {
|
|
4078
|
+
const metadata = parseRunResultPayload(input.payload);
|
|
4079
|
+
const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
|
|
4080
|
+
const id = randomUUID3();
|
|
4081
|
+
const now = /* @__PURE__ */ new Date();
|
|
4082
|
+
await this.executeStatement(
|
|
4083
|
+
`INSERT INTO run_results (
|
|
4084
|
+
id,
|
|
4085
|
+
kind,
|
|
4086
|
+
label,
|
|
4087
|
+
collection_name,
|
|
4088
|
+
request_name,
|
|
4089
|
+
summary_passed,
|
|
4090
|
+
summary_failed,
|
|
4091
|
+
summary_skipped,
|
|
4092
|
+
payload,
|
|
4093
|
+
created_at,
|
|
4094
|
+
created_by_user_id
|
|
4095
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4096
|
+
[
|
|
4097
|
+
id,
|
|
4098
|
+
metadata.kind,
|
|
4099
|
+
label,
|
|
4100
|
+
metadata.collectionName,
|
|
4101
|
+
metadata.requestName,
|
|
4102
|
+
metadata.summary.passed,
|
|
4103
|
+
metadata.summary.failed,
|
|
4104
|
+
metadata.summary.skipped,
|
|
4105
|
+
JSON.stringify(input.payload),
|
|
4106
|
+
now,
|
|
4107
|
+
actingUserId
|
|
4108
|
+
]
|
|
4109
|
+
);
|
|
4110
|
+
const rows = await this.queryRows(
|
|
4111
|
+
`${RUN_RESULT_SELECT} WHERE id = ?`,
|
|
4112
|
+
[id]
|
|
4113
|
+
);
|
|
4114
|
+
const row = rows[0];
|
|
4115
|
+
if (!row) {
|
|
4116
|
+
throw new Error("Run result not found after insert");
|
|
4117
|
+
}
|
|
4118
|
+
await this.recordAuditEntry(actingUserId, "create", "run_result", id);
|
|
4119
|
+
return mapRunResultSqlRow(row);
|
|
4120
|
+
}
|
|
4121
|
+
/**
|
|
4122
|
+
* Finds a run result by id.
|
|
4123
|
+
*/
|
|
4124
|
+
async findRunResultById(id) {
|
|
4125
|
+
const rows = await this.queryRows(
|
|
4126
|
+
`${RUN_RESULT_SELECT} WHERE id = ?`,
|
|
4127
|
+
[id]
|
|
4128
|
+
);
|
|
4129
|
+
const row = rows[0];
|
|
4130
|
+
return row ? mapRunResultSqlRow(row) : null;
|
|
4131
|
+
}
|
|
4132
|
+
/**
|
|
4133
|
+
* Deletes a run result by id.
|
|
4134
|
+
*/
|
|
4135
|
+
async deleteRunResult(id, actingUserId) {
|
|
4136
|
+
const result = await this.executeStatement("DELETE FROM run_results WHERE id = ?", [id]);
|
|
4137
|
+
if (result.affectedRows === 0) {
|
|
4138
|
+
throw new Error("Run result not found");
|
|
4139
|
+
}
|
|
4140
|
+
await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
|
|
4141
|
+
}
|
|
3886
4142
|
/**
|
|
3887
4143
|
* Ensures the internal system user exists and caches its identifier.
|
|
3888
4144
|
*/
|
|
@@ -4229,6 +4485,22 @@ WHERE role = 'user'
|
|
|
4229
4485
|
AND snippet_access = '[]'
|
|
4230
4486
|
AND collection_access LIKE '%"*"%';
|
|
4231
4487
|
`.trim();
|
|
4488
|
+
var RUN_RESULTS_MIGRATION_SQL2 = `
|
|
4489
|
+
CREATE TABLE IF NOT EXISTS run_results (
|
|
4490
|
+
id TEXT PRIMARY KEY,
|
|
4491
|
+
kind TEXT NOT NULL CHECK (kind IN ('collection-run-results', 'request-run-results')),
|
|
4492
|
+
label TEXT NOT NULL,
|
|
4493
|
+
collection_name TEXT,
|
|
4494
|
+
request_name TEXT,
|
|
4495
|
+
summary_passed INT NOT NULL DEFAULT 0,
|
|
4496
|
+
summary_failed INT NOT NULL DEFAULT 0,
|
|
4497
|
+
summary_skipped INT NOT NULL DEFAULT 0,
|
|
4498
|
+
payload TEXT NOT NULL,
|
|
4499
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
4500
|
+
created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
|
|
4501
|
+
);
|
|
4502
|
+
CREATE INDEX IF NOT EXISTS run_results_created_idx ON run_results (created_at DESC);
|
|
4503
|
+
`.trim();
|
|
4232
4504
|
var POSTGRES_MIGRATIONS = [
|
|
4233
4505
|
USERS_MIGRATION_SQL2,
|
|
4234
4506
|
API_TOKENS_MIGRATION_SQL2,
|
|
@@ -4254,7 +4526,8 @@ var POSTGRES_MIGRATIONS = [
|
|
|
4254
4526
|
COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
|
|
4255
4527
|
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
|
|
4256
4528
|
USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
|
|
4257
|
-
USERS_SNIPPET_ACCESS_BACKFILL_SQL2
|
|
4529
|
+
USERS_SNIPPET_ACCESS_BACKFILL_SQL2,
|
|
4530
|
+
RUN_RESULTS_MIGRATION_SQL2
|
|
4258
4531
|
];
|
|
4259
4532
|
|
|
4260
4533
|
// src/db/postgres/schemas.ts
|
|
@@ -4279,6 +4552,8 @@ var { Pool } = pg;
|
|
|
4279
4552
|
var COLLECTION_SELECT2 = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
|
|
4280
4553
|
var ENVIRONMENT_SELECT2 = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
|
|
4281
4554
|
var SNIPPET_SELECT2 = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
|
|
4555
|
+
var RUN_RESULT_SELECT_COLUMNS2 = "id, kind, label, collection_name, request_name, summary_passed, summary_failed, summary_skipped, payload, created_at, created_by_user_id";
|
|
4556
|
+
var RUN_RESULT_SELECT2 = `SELECT ${RUN_RESULT_SELECT_COLUMNS2} FROM run_results`;
|
|
4282
4557
|
var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
|
|
4283
4558
|
var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
|
|
4284
4559
|
var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
|
|
@@ -5629,6 +5904,87 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
5629
5904
|
);
|
|
5630
5905
|
return result.rows.map(mapLlmUsageLogSqlRow);
|
|
5631
5906
|
}
|
|
5907
|
+
/**
|
|
5908
|
+
* Lists run results saved by the given user, newest first.
|
|
5909
|
+
*/
|
|
5910
|
+
async listRunResultsForUser(userId) {
|
|
5911
|
+
const result = await this.query(
|
|
5912
|
+
`${RUN_RESULT_SELECT2} WHERE created_by_user_id = $1 ORDER BY created_at DESC`,
|
|
5913
|
+
[userId]
|
|
5914
|
+
);
|
|
5915
|
+
return result.rows.map(mapRunResultSqlRow);
|
|
5916
|
+
}
|
|
5917
|
+
/**
|
|
5918
|
+
* Lists all run results for admin inspection, newest first.
|
|
5919
|
+
*/
|
|
5920
|
+
async listAllRunResults() {
|
|
5921
|
+
const result = await this.query(
|
|
5922
|
+
`${RUN_RESULT_SELECT2} ORDER BY created_at DESC`
|
|
5923
|
+
);
|
|
5924
|
+
return result.rows.map(mapRunResultSqlRow);
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* Creates a standalone run result snapshot.
|
|
5928
|
+
*/
|
|
5929
|
+
async createRunResult(input, actingUserId) {
|
|
5930
|
+
const metadata = parseRunResultPayload(input.payload);
|
|
5931
|
+
const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
|
|
5932
|
+
const id = randomUUID4();
|
|
5933
|
+
const now = /* @__PURE__ */ new Date();
|
|
5934
|
+
const result = await this.query(
|
|
5935
|
+
`INSERT INTO run_results (
|
|
5936
|
+
id,
|
|
5937
|
+
kind,
|
|
5938
|
+
label,
|
|
5939
|
+
collection_name,
|
|
5940
|
+
request_name,
|
|
5941
|
+
summary_passed,
|
|
5942
|
+
summary_failed,
|
|
5943
|
+
summary_skipped,
|
|
5944
|
+
payload,
|
|
5945
|
+
created_at,
|
|
5946
|
+
created_by_user_id
|
|
5947
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
5948
|
+
RETURNING ${RUN_RESULT_SELECT_COLUMNS2}`,
|
|
5949
|
+
[
|
|
5950
|
+
id,
|
|
5951
|
+
metadata.kind,
|
|
5952
|
+
label,
|
|
5953
|
+
metadata.collectionName,
|
|
5954
|
+
metadata.requestName,
|
|
5955
|
+
metadata.summary.passed,
|
|
5956
|
+
metadata.summary.failed,
|
|
5957
|
+
metadata.summary.skipped,
|
|
5958
|
+
JSON.stringify(input.payload),
|
|
5959
|
+
now,
|
|
5960
|
+
actingUserId
|
|
5961
|
+
]
|
|
5962
|
+
);
|
|
5963
|
+
const row = result.rows[0];
|
|
5964
|
+
if (!row) {
|
|
5965
|
+
throw new Error("Run result not found after insert");
|
|
5966
|
+
}
|
|
5967
|
+
await this.recordAuditEntry(actingUserId, "create", "run_result", id);
|
|
5968
|
+
return mapRunResultSqlRow(row);
|
|
5969
|
+
}
|
|
5970
|
+
/**
|
|
5971
|
+
* Finds a run result by id.
|
|
5972
|
+
*/
|
|
5973
|
+
async findRunResultById(id) {
|
|
5974
|
+
const result = await this.query(`${RUN_RESULT_SELECT2} WHERE id = $1`, [id]);
|
|
5975
|
+
const row = result.rows[0];
|
|
5976
|
+
return row ? mapRunResultSqlRow(row) : null;
|
|
5977
|
+
}
|
|
5978
|
+
/**
|
|
5979
|
+
* Deletes a run result by id.
|
|
5980
|
+
*/
|
|
5981
|
+
async deleteRunResult(id, actingUserId) {
|
|
5982
|
+
const result = await this.query("DELETE FROM run_results WHERE id = $1", [id]);
|
|
5983
|
+
if ((result.rowCount ?? 0) === 0) {
|
|
5984
|
+
throw new Error("Run result not found");
|
|
5985
|
+
}
|
|
5986
|
+
await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
|
|
5987
|
+
}
|
|
5632
5988
|
/**
|
|
5633
5989
|
* Returns the active pool or throws when connect has not been called.
|
|
5634
5990
|
*
|
|
@@ -6655,6 +7011,9 @@ function canListEnvironments(user) {
|
|
|
6655
7011
|
function canListSnippets(user) {
|
|
6656
7012
|
return canUseDataApi(user) || canUseManagementApi(user);
|
|
6657
7013
|
}
|
|
7014
|
+
function canListRunResults(user) {
|
|
7015
|
+
return canUseDataApi(user) || canUseManagementApi(user);
|
|
7016
|
+
}
|
|
6658
7017
|
function hasWildcardAccess(access) {
|
|
6659
7018
|
return access.includes("*");
|
|
6660
7019
|
}
|
|
@@ -6691,6 +7050,9 @@ function canDeleteCollection(user, collection) {
|
|
|
6691
7050
|
function canDeleteRequest(user, request) {
|
|
6692
7051
|
return canUseDataApi(user) && canAccessCollection(user, request.collectionId) && request.createdByUserId === user.id;
|
|
6693
7052
|
}
|
|
7053
|
+
function canDeleteRunResult(user, runResult) {
|
|
7054
|
+
return canUseDataApi(user) && runResult.createdByUserId === user.id;
|
|
7055
|
+
}
|
|
6694
7056
|
function canCreateCollection(user) {
|
|
6695
7057
|
return user.role === "user" && hasWildcardAccess(user.collectionAccess);
|
|
6696
7058
|
}
|
|
@@ -7115,6 +7477,48 @@ function serializeSavedRequest(record) {
|
|
|
7115
7477
|
updatedAt: record.updatedAt.toISOString()
|
|
7116
7478
|
};
|
|
7117
7479
|
}
|
|
7480
|
+
var runResultRecordSchema = z8.object({
|
|
7481
|
+
id: z8.string(),
|
|
7482
|
+
kind: z8.enum(["collection-run-results", "request-run-results"]),
|
|
7483
|
+
label: z8.string(),
|
|
7484
|
+
collectionName: z8.string().nullable(),
|
|
7485
|
+
requestName: z8.string().nullable(),
|
|
7486
|
+
summary: z8.object({
|
|
7487
|
+
passed: z8.number().int().nonnegative(),
|
|
7488
|
+
failed: z8.number().int().nonnegative(),
|
|
7489
|
+
skipped: z8.number().int().nonnegative()
|
|
7490
|
+
}),
|
|
7491
|
+
createdAt: timestampSchema,
|
|
7492
|
+
createdByUserId: z8.string().nullable()
|
|
7493
|
+
});
|
|
7494
|
+
var runResultDetailSchema = runResultRecordSchema.extend({
|
|
7495
|
+
payload: z8.record(z8.string(), z8.unknown())
|
|
7496
|
+
});
|
|
7497
|
+
var createRunResultBodySchema = z8.object({
|
|
7498
|
+
label: z8.string().trim().min(1).optional(),
|
|
7499
|
+
payload: z8.record(z8.string(), z8.unknown())
|
|
7500
|
+
});
|
|
7501
|
+
var listRunResultsResponseSchema = z8.object({
|
|
7502
|
+
runResults: z8.array(runResultRecordSchema)
|
|
7503
|
+
});
|
|
7504
|
+
function serializeRunResult(record) {
|
|
7505
|
+
return {
|
|
7506
|
+
id: record.id,
|
|
7507
|
+
kind: record.kind,
|
|
7508
|
+
label: record.label,
|
|
7509
|
+
collectionName: record.collectionName,
|
|
7510
|
+
requestName: record.requestName,
|
|
7511
|
+
summary: record.summary,
|
|
7512
|
+
createdAt: record.createdAt.toISOString(),
|
|
7513
|
+
createdByUserId: record.createdByUserId
|
|
7514
|
+
};
|
|
7515
|
+
}
|
|
7516
|
+
function serializeRunResultDetail(record) {
|
|
7517
|
+
return {
|
|
7518
|
+
...serializeRunResult(record),
|
|
7519
|
+
payload: record.payload
|
|
7520
|
+
};
|
|
7521
|
+
}
|
|
7118
7522
|
|
|
7119
7523
|
// src/server/routes/schemas/admin.ts
|
|
7120
7524
|
var adminResourceOptionSchema = z9.object({
|
|
@@ -7164,6 +7568,9 @@ var listAdminEnvironmentsResponseSchema = z9.object({
|
|
|
7164
7568
|
var listAdminSnippetsResponseSchema = z9.object({
|
|
7165
7569
|
snippets: z9.array(snippetRecordSchema)
|
|
7166
7570
|
});
|
|
7571
|
+
var listAdminRunResultsResponseSchema = z9.object({
|
|
7572
|
+
runResults: z9.array(runResultRecordSchema)
|
|
7573
|
+
});
|
|
7167
7574
|
var adminSnippetRecordSchema = snippetRecordSchema;
|
|
7168
7575
|
var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
|
|
7169
7576
|
var hubUserRecordSchema = z9.object({
|
|
@@ -7722,6 +8129,71 @@ async function registerAdminRoutes(app, options) {
|
|
|
7722
8129
|
}
|
|
7723
8130
|
}
|
|
7724
8131
|
});
|
|
8132
|
+
routes.route({
|
|
8133
|
+
method: "GET",
|
|
8134
|
+
url: "/admin/run-results",
|
|
8135
|
+
schema: {
|
|
8136
|
+
response: {
|
|
8137
|
+
200: listAdminRunResultsResponseSchema,
|
|
8138
|
+
403: errorResponseSchema
|
|
8139
|
+
}
|
|
8140
|
+
},
|
|
8141
|
+
/**
|
|
8142
|
+
* Lists all run results for operator management.
|
|
8143
|
+
*/
|
|
8144
|
+
handler: async (request, reply) => {
|
|
8145
|
+
try {
|
|
8146
|
+
const user = requireAuthenticatedUser(request);
|
|
8147
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
8148
|
+
return;
|
|
8149
|
+
}
|
|
8150
|
+
const runResults = await db.listAllRunResults();
|
|
8151
|
+
return reply.send({
|
|
8152
|
+
runResults: runResults.map((record) => serializeRunResult(record))
|
|
8153
|
+
});
|
|
8154
|
+
} catch (error) {
|
|
8155
|
+
if (handleDbError(reply, error)) {
|
|
8156
|
+
return;
|
|
8157
|
+
}
|
|
8158
|
+
throw error;
|
|
8159
|
+
}
|
|
8160
|
+
}
|
|
8161
|
+
});
|
|
8162
|
+
routes.route({
|
|
8163
|
+
method: "DELETE",
|
|
8164
|
+
url: "/admin/run-results/:id",
|
|
8165
|
+
schema: {
|
|
8166
|
+
params: idParamSchema,
|
|
8167
|
+
response: {
|
|
8168
|
+
204: emptyResponseSchema,
|
|
8169
|
+
403: errorResponseSchema,
|
|
8170
|
+
404: errorResponseSchema
|
|
8171
|
+
}
|
|
8172
|
+
},
|
|
8173
|
+
/**
|
|
8174
|
+
* Deletes a run result regardless of creator ownership.
|
|
8175
|
+
*/
|
|
8176
|
+
handler: async (request, reply) => {
|
|
8177
|
+
try {
|
|
8178
|
+
const user = requireAuthenticatedUser(request);
|
|
8179
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
8180
|
+
return;
|
|
8181
|
+
}
|
|
8182
|
+
const record = await db.findRunResultById(request.params.id);
|
|
8183
|
+
if (!record) {
|
|
8184
|
+
void reply.code(404).send({ error: "Run result not found" });
|
|
8185
|
+
return;
|
|
8186
|
+
}
|
|
8187
|
+
await db.deleteRunResult(request.params.id, user.id);
|
|
8188
|
+
return reply.code(204).send(null);
|
|
8189
|
+
} catch (error) {
|
|
8190
|
+
if (handleDbError(reply, error)) {
|
|
8191
|
+
return;
|
|
8192
|
+
}
|
|
8193
|
+
throw error;
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
});
|
|
7725
8197
|
routes.route({
|
|
7726
8198
|
method: "PUT",
|
|
7727
8199
|
url: "/admin/snippets/:id",
|
|
@@ -9104,6 +9576,140 @@ async function registerRequestRoutes(app, db) {
|
|
|
9104
9576
|
});
|
|
9105
9577
|
}
|
|
9106
9578
|
|
|
9579
|
+
// src/server/routes/runResults.ts
|
|
9580
|
+
async function registerRunResultRoutes(app, db) {
|
|
9581
|
+
const routes = app.withTypeProvider();
|
|
9582
|
+
routes.route({
|
|
9583
|
+
method: "GET",
|
|
9584
|
+
url: "/run-results",
|
|
9585
|
+
schema: {
|
|
9586
|
+
response: {
|
|
9587
|
+
200: listRunResultsResponseSchema
|
|
9588
|
+
}
|
|
9589
|
+
},
|
|
9590
|
+
/**
|
|
9591
|
+
* Lists run results saved by the authenticated user token.
|
|
9592
|
+
*/
|
|
9593
|
+
handler: async (request, reply) => {
|
|
9594
|
+
try {
|
|
9595
|
+
const user = requireAuthenticatedUser(request);
|
|
9596
|
+
if (denyUnlessAllowed(reply, canListRunResults(user))) {
|
|
9597
|
+
return;
|
|
9598
|
+
}
|
|
9599
|
+
const runResults = await db.listRunResultsForUser(user.id);
|
|
9600
|
+
return reply.send({
|
|
9601
|
+
runResults: runResults.map((record) => serializeRunResult(record))
|
|
9602
|
+
});
|
|
9603
|
+
} catch (error) {
|
|
9604
|
+
if (handleDbError(reply, error)) {
|
|
9605
|
+
return;
|
|
9606
|
+
}
|
|
9607
|
+
throw error;
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
9610
|
+
});
|
|
9611
|
+
routes.route({
|
|
9612
|
+
method: "POST",
|
|
9613
|
+
url: "/run-results",
|
|
9614
|
+
schema: {
|
|
9615
|
+
body: createRunResultBodySchema,
|
|
9616
|
+
response: {
|
|
9617
|
+
200: runResultDetailSchema,
|
|
9618
|
+
400: errorResponseSchema
|
|
9619
|
+
}
|
|
9620
|
+
},
|
|
9621
|
+
/**
|
|
9622
|
+
* Saves a standalone run result snapshot.
|
|
9623
|
+
*/
|
|
9624
|
+
handler: async (request, reply) => {
|
|
9625
|
+
try {
|
|
9626
|
+
const user = requireAuthenticatedUser(request);
|
|
9627
|
+
if (denyUnlessAllowed(reply, canUseDataApi(user))) {
|
|
9628
|
+
return;
|
|
9629
|
+
}
|
|
9630
|
+
const record = await db.createRunResult(request.body, user.id);
|
|
9631
|
+
return reply.send(serializeRunResultDetail(record));
|
|
9632
|
+
} catch (error) {
|
|
9633
|
+
if (handleDbError(reply, error)) {
|
|
9634
|
+
return;
|
|
9635
|
+
}
|
|
9636
|
+
throw error;
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
});
|
|
9640
|
+
routes.route({
|
|
9641
|
+
method: "GET",
|
|
9642
|
+
url: "/run-results/:id",
|
|
9643
|
+
schema: {
|
|
9644
|
+
params: idParamSchema,
|
|
9645
|
+
response: {
|
|
9646
|
+
200: runResultDetailSchema,
|
|
9647
|
+
404: errorResponseSchema
|
|
9648
|
+
}
|
|
9649
|
+
},
|
|
9650
|
+
/**
|
|
9651
|
+
* Loads a run result snapshot by id for shared deep links.
|
|
9652
|
+
*/
|
|
9653
|
+
handler: async (request, reply) => {
|
|
9654
|
+
try {
|
|
9655
|
+
const user = requireAuthenticatedUser(request);
|
|
9656
|
+
if (denyUnlessAllowed(reply, canUseDataApi(user))) {
|
|
9657
|
+
return;
|
|
9658
|
+
}
|
|
9659
|
+
const record = await db.findRunResultById(request.params.id);
|
|
9660
|
+
if (!record) {
|
|
9661
|
+
void reply.code(404).send({ error: "Run result not found" });
|
|
9662
|
+
return;
|
|
9663
|
+
}
|
|
9664
|
+
return reply.send(serializeRunResultDetail(record));
|
|
9665
|
+
} catch (error) {
|
|
9666
|
+
if (handleDbError(reply, error)) {
|
|
9667
|
+
return;
|
|
9668
|
+
}
|
|
9669
|
+
throw error;
|
|
9670
|
+
}
|
|
9671
|
+
}
|
|
9672
|
+
});
|
|
9673
|
+
routes.route({
|
|
9674
|
+
method: "DELETE",
|
|
9675
|
+
url: "/run-results/:id",
|
|
9676
|
+
schema: {
|
|
9677
|
+
params: idParamSchema,
|
|
9678
|
+
response: {
|
|
9679
|
+
204: emptyResponseSchema,
|
|
9680
|
+
403: errorResponseSchema,
|
|
9681
|
+
404: errorResponseSchema
|
|
9682
|
+
}
|
|
9683
|
+
},
|
|
9684
|
+
/**
|
|
9685
|
+
* Deletes a run result saved by the authenticated user when permitted.
|
|
9686
|
+
*/
|
|
9687
|
+
handler: async (request, reply) => {
|
|
9688
|
+
try {
|
|
9689
|
+
const user = requireAuthenticatedUser(request);
|
|
9690
|
+
if (denyUnlessAllowed(reply, canUseDataApi(user))) {
|
|
9691
|
+
return;
|
|
9692
|
+
}
|
|
9693
|
+
const record = await db.findRunResultById(request.params.id);
|
|
9694
|
+
if (!record) {
|
|
9695
|
+
void reply.code(404).send({ error: "Run result not found" });
|
|
9696
|
+
return;
|
|
9697
|
+
}
|
|
9698
|
+
if (denyUnlessAllowed(reply, canDeleteRunResult(user, record))) {
|
|
9699
|
+
return;
|
|
9700
|
+
}
|
|
9701
|
+
await db.deleteRunResult(request.params.id, user.id);
|
|
9702
|
+
return reply.code(204).send(null);
|
|
9703
|
+
} catch (error) {
|
|
9704
|
+
if (handleDbError(reply, error)) {
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
throw error;
|
|
9708
|
+
}
|
|
9709
|
+
}
|
|
9710
|
+
});
|
|
9711
|
+
}
|
|
9712
|
+
|
|
9107
9713
|
// src/server/llm/client.ts
|
|
9108
9714
|
var OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
9109
9715
|
var CLAUDE_BASE_URL = "https://api.anthropic.com/v1";
|
|
@@ -9746,6 +10352,7 @@ async function registerProtectedRoutes(app, options) {
|
|
|
9746
10352
|
await registerSnippetRoutes(app, options.db);
|
|
9747
10353
|
await registerFolderRoutes(app, options.db);
|
|
9748
10354
|
await registerRequestRoutes(app, options.db);
|
|
10355
|
+
await registerRunResultRoutes(app, options.db);
|
|
9749
10356
|
await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
|
|
9750
10357
|
await registerPluginsRoutes(app, { getPlugins: options.getPlugins });
|
|
9751
10358
|
}
|