@open-skills-hub/core 1.0.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/package.json +75 -0
- package/src/config/config.ts +420 -0
- package/src/config/index.ts +5 -0
- package/src/index.ts +18 -0
- package/src/models/index.ts +5 -0
- package/src/models/types.ts +503 -0
- package/src/scanner/index.ts +19 -0
- package/src/scanner/scanner.ts +708 -0
- package/src/scanner/skills-guard-adapter.ts +172 -0
- package/src/storage/index.ts +58 -0
- package/src/storage/interface.ts +391 -0
- package/src/storage/sqlite.ts +1838 -0
- package/src/utils/helpers.ts +414 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +69 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,1838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open Skills Hub - SQLite Storage Implementation (using sql.js)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import initSqlJs, { Database as SqlJsDatabase, SqlValue } from 'sql.js';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
IStorage,
|
|
10
|
+
PaginatedResult,
|
|
11
|
+
SkillQueryOptions,
|
|
12
|
+
VersionQueryOptions,
|
|
13
|
+
AuditQueryOptions,
|
|
14
|
+
FeedbackQueryOptions,
|
|
15
|
+
QueueQueryOptions,
|
|
16
|
+
PaginationOptions,
|
|
17
|
+
} from './interface.js';
|
|
18
|
+
import type {
|
|
19
|
+
Skill,
|
|
20
|
+
Version,
|
|
21
|
+
ScanRecord,
|
|
22
|
+
ScanIssue,
|
|
23
|
+
Feedback,
|
|
24
|
+
FeedbackQueueItem,
|
|
25
|
+
AuditLog,
|
|
26
|
+
CacheMetadata,
|
|
27
|
+
UseRecord,
|
|
28
|
+
} from '../models/types.js';
|
|
29
|
+
import { now } from '../utils/helpers.js';
|
|
30
|
+
import { logger } from '../utils/logger.js';
|
|
31
|
+
|
|
32
|
+
// Type alias for SQL parameters
|
|
33
|
+
type SqlParams = SqlValue[];
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// SQLite Schema
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
const SCHEMA = `
|
|
40
|
+
-- Skills table
|
|
41
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
name TEXT NOT NULL,
|
|
44
|
+
scope TEXT,
|
|
45
|
+
full_name TEXT UNIQUE NOT NULL,
|
|
46
|
+
|
|
47
|
+
owner_id TEXT,
|
|
48
|
+
owner_type TEXT DEFAULT 'user',
|
|
49
|
+
|
|
50
|
+
display_name TEXT,
|
|
51
|
+
description TEXT NOT NULL,
|
|
52
|
+
category TEXT,
|
|
53
|
+
keywords TEXT DEFAULT '[]',
|
|
54
|
+
license TEXT,
|
|
55
|
+
|
|
56
|
+
repository TEXT,
|
|
57
|
+
homepage TEXT,
|
|
58
|
+
|
|
59
|
+
derived_from TEXT,
|
|
60
|
+
|
|
61
|
+
latest_version TEXT,
|
|
62
|
+
latest_version_id TEXT,
|
|
63
|
+
|
|
64
|
+
visibility TEXT DEFAULT 'public',
|
|
65
|
+
status TEXT DEFAULT 'active',
|
|
66
|
+
deprecated_message TEXT,
|
|
67
|
+
|
|
68
|
+
security_score INTEGER,
|
|
69
|
+
security_level TEXT,
|
|
70
|
+
last_scan_at TEXT,
|
|
71
|
+
|
|
72
|
+
stats TEXT DEFAULT '{"totalUses":0,"weeklyUses":0,"monthlyUses":0,"versionCount":0,"derivationCount":0}',
|
|
73
|
+
rating TEXT DEFAULT '{"average":0,"count":0}',
|
|
74
|
+
|
|
75
|
+
searchable_text TEXT,
|
|
76
|
+
|
|
77
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
78
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
79
|
+
published_at TEXT
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_skills_full_name ON skills(full_name);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(visibility);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(owner_id);
|
|
87
|
+
|
|
88
|
+
-- Versions table
|
|
89
|
+
CREATE TABLE IF NOT EXISTS versions (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
|
|
92
|
+
|
|
93
|
+
version TEXT NOT NULL,
|
|
94
|
+
tag TEXT,
|
|
95
|
+
|
|
96
|
+
content TEXT NOT NULL,
|
|
97
|
+
|
|
98
|
+
package_url TEXT NOT NULL,
|
|
99
|
+
package_size INTEGER NOT NULL,
|
|
100
|
+
package_hash TEXT NOT NULL,
|
|
101
|
+
|
|
102
|
+
changelog TEXT,
|
|
103
|
+
|
|
104
|
+
scan_record_id TEXT,
|
|
105
|
+
security_score INTEGER,
|
|
106
|
+
security_level TEXT,
|
|
107
|
+
|
|
108
|
+
status TEXT DEFAULT 'published',
|
|
109
|
+
deprecated_message TEXT,
|
|
110
|
+
|
|
111
|
+
published_by TEXT,
|
|
112
|
+
uses INTEGER DEFAULT 0,
|
|
113
|
+
|
|
114
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
115
|
+
published_at TEXT DEFAULT (datetime('now')),
|
|
116
|
+
deprecated_at TEXT,
|
|
117
|
+
|
|
118
|
+
UNIQUE(skill_id, version)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_versions_skill ON versions(skill_id);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_versions_tag ON versions(skill_id, tag);
|
|
123
|
+
|
|
124
|
+
-- Scan records table
|
|
125
|
+
CREATE TABLE IF NOT EXISTS scan_records (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
|
|
128
|
+
skill_id TEXT REFERENCES skills(id) ON DELETE SET NULL,
|
|
129
|
+
version_id TEXT REFERENCES versions(id) ON DELETE SET NULL,
|
|
130
|
+
user_id TEXT,
|
|
131
|
+
|
|
132
|
+
input_type TEXT NOT NULL,
|
|
133
|
+
content_hash TEXT NOT NULL,
|
|
134
|
+
content_size INTEGER NOT NULL,
|
|
135
|
+
|
|
136
|
+
score INTEGER,
|
|
137
|
+
level TEXT,
|
|
138
|
+
issue_count TEXT DEFAULT '{"high":0,"medium":0,"low":0}',
|
|
139
|
+
|
|
140
|
+
scanner_version TEXT,
|
|
141
|
+
rules_version TEXT,
|
|
142
|
+
rules_applied INTEGER,
|
|
143
|
+
duration INTEGER,
|
|
144
|
+
|
|
145
|
+
status TEXT DEFAULT 'pending',
|
|
146
|
+
error_message TEXT,
|
|
147
|
+
|
|
148
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
149
|
+
completed_at TEXT
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_scan_records_skill ON scan_records(skill_id);
|
|
153
|
+
|
|
154
|
+
-- Scan issues table
|
|
155
|
+
CREATE TABLE IF NOT EXISTS scan_issues (
|
|
156
|
+
id TEXT PRIMARY KEY,
|
|
157
|
+
scan_record_id TEXT NOT NULL REFERENCES scan_records(id) ON DELETE CASCADE,
|
|
158
|
+
|
|
159
|
+
rule_id TEXT NOT NULL,
|
|
160
|
+
rule_name TEXT NOT NULL,
|
|
161
|
+
rule_category TEXT NOT NULL,
|
|
162
|
+
|
|
163
|
+
severity TEXT NOT NULL,
|
|
164
|
+
|
|
165
|
+
file TEXT,
|
|
166
|
+
line INTEGER,
|
|
167
|
+
column_num INTEGER,
|
|
168
|
+
end_line INTEGER,
|
|
169
|
+
end_column INTEGER,
|
|
170
|
+
|
|
171
|
+
content TEXT NOT NULL,
|
|
172
|
+
context TEXT,
|
|
173
|
+
message TEXT NOT NULL,
|
|
174
|
+
suggestion TEXT,
|
|
175
|
+
|
|
176
|
+
acknowledged INTEGER DEFAULT 0,
|
|
177
|
+
acknowledged_at TEXT,
|
|
178
|
+
acknowledged_by TEXT,
|
|
179
|
+
acknowledged_reason TEXT,
|
|
180
|
+
|
|
181
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_scan_issues_record ON scan_issues(scan_record_id);
|
|
185
|
+
|
|
186
|
+
-- Feedbacks table
|
|
187
|
+
CREATE TABLE IF NOT EXISTS feedbacks (
|
|
188
|
+
id TEXT PRIMARY KEY,
|
|
189
|
+
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
|
|
190
|
+
skill_version TEXT NOT NULL,
|
|
191
|
+
|
|
192
|
+
feedback_type TEXT NOT NULL,
|
|
193
|
+
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
|
194
|
+
comment TEXT,
|
|
195
|
+
|
|
196
|
+
context TEXT DEFAULT '{}',
|
|
197
|
+
|
|
198
|
+
status TEXT DEFAULT 'pending',
|
|
199
|
+
reviewer_notes TEXT,
|
|
200
|
+
reviewed_at TEXT,
|
|
201
|
+
reviewed_by TEXT,
|
|
202
|
+
|
|
203
|
+
source_ip_hash TEXT,
|
|
204
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_feedbacks_skill ON feedbacks(skill_id);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_feedbacks_type ON feedbacks(feedback_type);
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_feedbacks_status ON feedbacks(status);
|
|
210
|
+
|
|
211
|
+
-- Feedback queue table
|
|
212
|
+
CREATE TABLE IF NOT EXISTS feedback_queue (
|
|
213
|
+
id TEXT PRIMARY KEY,
|
|
214
|
+
skill_name TEXT NOT NULL,
|
|
215
|
+
skill_version TEXT NOT NULL,
|
|
216
|
+
feedback TEXT NOT NULL,
|
|
217
|
+
|
|
218
|
+
status TEXT DEFAULT 'pending',
|
|
219
|
+
|
|
220
|
+
attempts INTEGER DEFAULT 0,
|
|
221
|
+
max_attempts INTEGER DEFAULT 5,
|
|
222
|
+
last_attempt_at TEXT,
|
|
223
|
+
next_retry_at TEXT,
|
|
224
|
+
last_error TEXT,
|
|
225
|
+
|
|
226
|
+
base_delay INTEGER DEFAULT 1000,
|
|
227
|
+
current_delay INTEGER DEFAULT 1000,
|
|
228
|
+
max_delay INTEGER DEFAULT 3600000,
|
|
229
|
+
|
|
230
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
231
|
+
processed_at TEXT
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_queue_status ON feedback_queue(status);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_queue_next_retry ON feedback_queue(next_retry_at);
|
|
236
|
+
|
|
237
|
+
-- Audit logs table
|
|
238
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
239
|
+
id TEXT PRIMARY KEY,
|
|
240
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
241
|
+
|
|
242
|
+
event_type TEXT NOT NULL,
|
|
243
|
+
|
|
244
|
+
actor TEXT NOT NULL,
|
|
245
|
+
resource TEXT NOT NULL,
|
|
246
|
+
|
|
247
|
+
action TEXT NOT NULL,
|
|
248
|
+
result TEXT NOT NULL,
|
|
249
|
+
|
|
250
|
+
details TEXT,
|
|
251
|
+
changes TEXT,
|
|
252
|
+
|
|
253
|
+
error_code TEXT,
|
|
254
|
+
error_message TEXT
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
|
|
258
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_event_type ON audit_logs(event_type);
|
|
259
|
+
|
|
260
|
+
-- Cache metadata table
|
|
261
|
+
CREATE TABLE IF NOT EXISTS cache_metadata (
|
|
262
|
+
id TEXT PRIMARY KEY,
|
|
263
|
+
skill_name TEXT NOT NULL,
|
|
264
|
+
version TEXT NOT NULL,
|
|
265
|
+
|
|
266
|
+
cached_at TEXT DEFAULT (datetime('now')),
|
|
267
|
+
expires_at TEXT,
|
|
268
|
+
|
|
269
|
+
size INTEGER,
|
|
270
|
+
hit_count INTEGER DEFAULT 0,
|
|
271
|
+
last_hit_at TEXT,
|
|
272
|
+
|
|
273
|
+
source TEXT,
|
|
274
|
+
integrity_hash TEXT,
|
|
275
|
+
|
|
276
|
+
UNIQUE(skill_name, version)
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_cache_metadata_expires ON cache_metadata(expires_at);
|
|
280
|
+
|
|
281
|
+
-- Use records table
|
|
282
|
+
CREATE TABLE IF NOT EXISTS use_records (
|
|
283
|
+
id TEXT PRIMARY KEY,
|
|
284
|
+
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
|
|
285
|
+
version_id TEXT REFERENCES versions(id),
|
|
286
|
+
user_id TEXT,
|
|
287
|
+
|
|
288
|
+
source TEXT NOT NULL,
|
|
289
|
+
cache_hit INTEGER DEFAULT 0,
|
|
290
|
+
client_version TEXT,
|
|
291
|
+
|
|
292
|
+
platform TEXT,
|
|
293
|
+
arch TEXT,
|
|
294
|
+
|
|
295
|
+
ip TEXT NOT NULL,
|
|
296
|
+
user_agent TEXT,
|
|
297
|
+
country TEXT,
|
|
298
|
+
region TEXT,
|
|
299
|
+
|
|
300
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_use_records_skill ON use_records(skill_id, created_at);
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// SQLite Storage Implementation
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
export class SQLiteStorage implements IStorage {
|
|
311
|
+
private db: SqlJsDatabase | null = null;
|
|
312
|
+
private dbPath: string;
|
|
313
|
+
private saveInterval: ReturnType<typeof setInterval> | null = null;
|
|
314
|
+
private isDirty = false;
|
|
315
|
+
|
|
316
|
+
constructor(dbPath: string) {
|
|
317
|
+
this.dbPath = dbPath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
// Lifecycle
|
|
322
|
+
// -------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
async initialize(): Promise<void> {
|
|
325
|
+
try {
|
|
326
|
+
const SQL = await initSqlJs();
|
|
327
|
+
|
|
328
|
+
// Load existing database or create new one
|
|
329
|
+
if (fs.existsSync(this.dbPath)) {
|
|
330
|
+
const buffer = fs.readFileSync(this.dbPath);
|
|
331
|
+
this.db = new SQL.Database(buffer);
|
|
332
|
+
} else {
|
|
333
|
+
// Ensure directory exists
|
|
334
|
+
const dir = path.dirname(this.dbPath);
|
|
335
|
+
if (!fs.existsSync(dir)) {
|
|
336
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
337
|
+
}
|
|
338
|
+
this.db = new SQL.Database();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Enable foreign keys and run schema
|
|
342
|
+
this.db.run('PRAGMA foreign_keys = ON;');
|
|
343
|
+
this.db.run(SCHEMA);
|
|
344
|
+
|
|
345
|
+
// Save periodically (every 5 seconds if dirty)
|
|
346
|
+
this.saveInterval = setInterval(() => {
|
|
347
|
+
if (this.isDirty) {
|
|
348
|
+
this.persist();
|
|
349
|
+
}
|
|
350
|
+
}, 5000);
|
|
351
|
+
|
|
352
|
+
logger.info('SQLite storage initialized', { path: this.dbPath });
|
|
353
|
+
} catch (error) {
|
|
354
|
+
logger.error('Failed to initialize SQLite storage', { error });
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private persist(): void {
|
|
360
|
+
if (this.db && this.isDirty) {
|
|
361
|
+
const data = this.db.export();
|
|
362
|
+
const buffer = Buffer.from(data);
|
|
363
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
364
|
+
this.isDirty = false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async close(): Promise<void> {
|
|
369
|
+
if (this.saveInterval) {
|
|
370
|
+
clearInterval(this.saveInterval);
|
|
371
|
+
this.saveInterval = null;
|
|
372
|
+
}
|
|
373
|
+
if (this.db) {
|
|
374
|
+
this.persist();
|
|
375
|
+
this.db.close();
|
|
376
|
+
this.db = null;
|
|
377
|
+
logger.info('SQLite storage closed');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async healthCheck(): Promise<boolean> {
|
|
382
|
+
try {
|
|
383
|
+
this.getDb().exec('SELECT 1');
|
|
384
|
+
return true;
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async sync(): Promise<void> {
|
|
391
|
+
this.persist();
|
|
392
|
+
logger.debug('SQLite storage synced to disk');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Reload the database from disk file
|
|
397
|
+
* This is useful when external processes (like CLI) have updated the database
|
|
398
|
+
*/
|
|
399
|
+
async reload(): Promise<void> {
|
|
400
|
+
try {
|
|
401
|
+
// First persist any pending changes
|
|
402
|
+
this.persist();
|
|
403
|
+
|
|
404
|
+
// Check if database file exists
|
|
405
|
+
if (!fs.existsSync(this.dbPath)) {
|
|
406
|
+
logger.warn('Database file not found during reload', { path: this.dbPath });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Read the updated file
|
|
411
|
+
const buffer = fs.readFileSync(this.dbPath);
|
|
412
|
+
|
|
413
|
+
// Get SQL.js constructor
|
|
414
|
+
const SQL = await initSqlJs();
|
|
415
|
+
|
|
416
|
+
// Create new database from file
|
|
417
|
+
const newDb = new SQL.Database(buffer);
|
|
418
|
+
newDb.run('PRAGMA foreign_keys = ON;');
|
|
419
|
+
|
|
420
|
+
// Close old database
|
|
421
|
+
if (this.db) {
|
|
422
|
+
this.db.close();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Replace with new database
|
|
426
|
+
this.db = newDb;
|
|
427
|
+
this.isDirty = false;
|
|
428
|
+
|
|
429
|
+
logger.info('Database reloaded from disk', { path: this.dbPath });
|
|
430
|
+
} catch (error) {
|
|
431
|
+
logger.error('Failed to reload database', { error });
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private getDb(): SqlJsDatabase {
|
|
437
|
+
if (!this.db) {
|
|
438
|
+
throw new Error('Database not initialized');
|
|
439
|
+
}
|
|
440
|
+
return this.db;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private run(sql: string, params?: Record<string, unknown>): void {
|
|
444
|
+
const db = this.getDb();
|
|
445
|
+
if (params) {
|
|
446
|
+
db.run(sql, params as Record<string, string | number | null | Uint8Array>);
|
|
447
|
+
} else {
|
|
448
|
+
db.run(sql);
|
|
449
|
+
}
|
|
450
|
+
this.isDirty = true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private get<T>(sql: string, params?: SqlParams): T | undefined {
|
|
454
|
+
const db = this.getDb();
|
|
455
|
+
const stmt = db.prepare(sql);
|
|
456
|
+
if (params) {
|
|
457
|
+
stmt.bind(params);
|
|
458
|
+
}
|
|
459
|
+
if (stmt.step()) {
|
|
460
|
+
const columns = stmt.getColumnNames();
|
|
461
|
+
const values = stmt.get();
|
|
462
|
+
stmt.free();
|
|
463
|
+
const row: Record<string, unknown> = {};
|
|
464
|
+
columns.forEach((col, i) => {
|
|
465
|
+
row[col] = values[i];
|
|
466
|
+
});
|
|
467
|
+
return row as T;
|
|
468
|
+
}
|
|
469
|
+
stmt.free();
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private all<T>(sql: string, params?: SqlParams): T[] {
|
|
474
|
+
const db = this.getDb();
|
|
475
|
+
const stmt = db.prepare(sql);
|
|
476
|
+
if (params) {
|
|
477
|
+
stmt.bind(params);
|
|
478
|
+
}
|
|
479
|
+
const results: T[] = [];
|
|
480
|
+
const columns = stmt.getColumnNames();
|
|
481
|
+
while (stmt.step()) {
|
|
482
|
+
const values = stmt.get();
|
|
483
|
+
const row: Record<string, unknown> = {};
|
|
484
|
+
columns.forEach((col, i) => {
|
|
485
|
+
row[col] = values[i];
|
|
486
|
+
});
|
|
487
|
+
results.push(row as T);
|
|
488
|
+
}
|
|
489
|
+
stmt.free();
|
|
490
|
+
return results;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// -------------------------------------------------------------------------
|
|
494
|
+
// Skills
|
|
495
|
+
// -------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
async createSkill(skill: Skill): Promise<Skill> {
|
|
498
|
+
const searchableText = [
|
|
499
|
+
skill.name,
|
|
500
|
+
skill.displayName,
|
|
501
|
+
skill.description,
|
|
502
|
+
skill.keywords.join(' '),
|
|
503
|
+
].filter(Boolean).join(' ');
|
|
504
|
+
|
|
505
|
+
this.run(`
|
|
506
|
+
INSERT INTO skills (
|
|
507
|
+
id, name, scope, full_name, owner_id, owner_type,
|
|
508
|
+
display_name, description, category, keywords, license,
|
|
509
|
+
repository, homepage, derived_from, latest_version, latest_version_id,
|
|
510
|
+
visibility, status, deprecated_message, security_score, security_level,
|
|
511
|
+
last_scan_at, stats, rating, searchable_text, created_at, updated_at, published_at
|
|
512
|
+
) VALUES (
|
|
513
|
+
$id, $name, $scope, $fullName, $ownerId, $ownerType,
|
|
514
|
+
$displayName, $description, $category, $keywords, $license,
|
|
515
|
+
$repository, $homepage, $derivedFrom, $latestVersion, $latestVersionId,
|
|
516
|
+
$visibility, $status, $deprecatedMessage, $securityScore, $securityLevel,
|
|
517
|
+
$lastScanAt, $stats, $rating, $searchableText, $createdAt, $updatedAt, $publishedAt
|
|
518
|
+
)
|
|
519
|
+
`, {
|
|
520
|
+
$id: skill.id,
|
|
521
|
+
$name: skill.name,
|
|
522
|
+
$scope: skill.scope ?? null,
|
|
523
|
+
$fullName: skill.fullName,
|
|
524
|
+
$ownerId: skill.ownerId ?? null,
|
|
525
|
+
$ownerType: skill.ownerType,
|
|
526
|
+
$displayName: skill.displayName ?? null,
|
|
527
|
+
$description: skill.description,
|
|
528
|
+
$category: skill.category ?? null,
|
|
529
|
+
$keywords: JSON.stringify(skill.keywords),
|
|
530
|
+
$license: skill.license ?? null,
|
|
531
|
+
$repository: skill.repository ?? null,
|
|
532
|
+
$homepage: skill.homepage ?? null,
|
|
533
|
+
$derivedFrom: skill.derivedFrom ? JSON.stringify(skill.derivedFrom) : null,
|
|
534
|
+
$latestVersion: skill.latestVersion,
|
|
535
|
+
$latestVersionId: skill.latestVersionId ?? null,
|
|
536
|
+
$visibility: skill.visibility,
|
|
537
|
+
$status: skill.status,
|
|
538
|
+
$deprecatedMessage: skill.deprecatedMessage ?? null,
|
|
539
|
+
$securityScore: skill.securityScore ?? null,
|
|
540
|
+
$securityLevel: skill.securityLevel ?? null,
|
|
541
|
+
$lastScanAt: skill.lastScanAt ?? null,
|
|
542
|
+
$stats: JSON.stringify(skill.stats),
|
|
543
|
+
$rating: JSON.stringify(skill.rating),
|
|
544
|
+
$searchableText: searchableText,
|
|
545
|
+
$createdAt: skill.createdAt,
|
|
546
|
+
$updatedAt: skill.updatedAt,
|
|
547
|
+
$publishedAt: skill.publishedAt ?? null,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
return skill;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async getSkillById(id: string): Promise<Skill | null> {
|
|
554
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM skills WHERE id = ?', [id]);
|
|
555
|
+
return row ? this.rowToSkill(row) : null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async getSkillByName(fullName: string): Promise<Skill | null> {
|
|
559
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM skills WHERE full_name = ?', [fullName]);
|
|
560
|
+
return row ? this.rowToSkill(row) : null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async updateSkill(id: string, updates: Partial<Skill>): Promise<Skill | null> {
|
|
564
|
+
const existing = await this.getSkillById(id);
|
|
565
|
+
if (!existing) return null;
|
|
566
|
+
|
|
567
|
+
const updated = { ...existing, ...updates, updatedAt: now() };
|
|
568
|
+
|
|
569
|
+
const searchableText = [
|
|
570
|
+
updated.name,
|
|
571
|
+
updated.displayName,
|
|
572
|
+
updated.description,
|
|
573
|
+
updated.keywords.join(' '),
|
|
574
|
+
].filter(Boolean).join(' ');
|
|
575
|
+
|
|
576
|
+
this.run(`
|
|
577
|
+
UPDATE skills SET
|
|
578
|
+
name = $name,
|
|
579
|
+
scope = $scope,
|
|
580
|
+
full_name = $fullName,
|
|
581
|
+
owner_id = $ownerId,
|
|
582
|
+
owner_type = $ownerType,
|
|
583
|
+
display_name = $displayName,
|
|
584
|
+
description = $description,
|
|
585
|
+
category = $category,
|
|
586
|
+
keywords = $keywords,
|
|
587
|
+
license = $license,
|
|
588
|
+
repository = $repository,
|
|
589
|
+
homepage = $homepage,
|
|
590
|
+
derived_from = $derivedFrom,
|
|
591
|
+
latest_version = $latestVersion,
|
|
592
|
+
latest_version_id = $latestVersionId,
|
|
593
|
+
visibility = $visibility,
|
|
594
|
+
status = $status,
|
|
595
|
+
deprecated_message = $deprecatedMessage,
|
|
596
|
+
security_score = $securityScore,
|
|
597
|
+
security_level = $securityLevel,
|
|
598
|
+
last_scan_at = $lastScanAt,
|
|
599
|
+
stats = $stats,
|
|
600
|
+
rating = $rating,
|
|
601
|
+
searchable_text = $searchableText,
|
|
602
|
+
updated_at = $updatedAt,
|
|
603
|
+
published_at = $publishedAt
|
|
604
|
+
WHERE id = $id
|
|
605
|
+
`, {
|
|
606
|
+
$id: updated.id,
|
|
607
|
+
$name: updated.name,
|
|
608
|
+
$scope: updated.scope ?? null,
|
|
609
|
+
$fullName: updated.fullName,
|
|
610
|
+
$ownerId: updated.ownerId ?? null,
|
|
611
|
+
$ownerType: updated.ownerType,
|
|
612
|
+
$displayName: updated.displayName ?? null,
|
|
613
|
+
$description: updated.description,
|
|
614
|
+
$category: updated.category ?? null,
|
|
615
|
+
$keywords: JSON.stringify(updated.keywords),
|
|
616
|
+
$license: updated.license ?? null,
|
|
617
|
+
$repository: updated.repository ?? null,
|
|
618
|
+
$homepage: updated.homepage ?? null,
|
|
619
|
+
$derivedFrom: updated.derivedFrom ? JSON.stringify(updated.derivedFrom) : null,
|
|
620
|
+
$latestVersion: updated.latestVersion,
|
|
621
|
+
$latestVersionId: updated.latestVersionId ?? null,
|
|
622
|
+
$visibility: updated.visibility,
|
|
623
|
+
$status: updated.status,
|
|
624
|
+
$deprecatedMessage: updated.deprecatedMessage ?? null,
|
|
625
|
+
$securityScore: updated.securityScore ?? null,
|
|
626
|
+
$securityLevel: updated.securityLevel ?? null,
|
|
627
|
+
$lastScanAt: updated.lastScanAt ?? null,
|
|
628
|
+
$stats: JSON.stringify(updated.stats),
|
|
629
|
+
$rating: JSON.stringify(updated.rating),
|
|
630
|
+
$searchableText: searchableText,
|
|
631
|
+
$updatedAt: updated.updatedAt,
|
|
632
|
+
$publishedAt: updated.publishedAt ?? null,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return updated;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async deleteSkill(id: string): Promise<boolean> {
|
|
639
|
+
const before = this.get<{ count: number }>('SELECT COUNT(*) as count FROM skills WHERE id = ?', [id]);
|
|
640
|
+
this.run('DELETE FROM skills WHERE id = ?', { $id: id });
|
|
641
|
+
const after = this.get<{ count: number }>('SELECT COUNT(*) as count FROM skills WHERE id = ?', [id]);
|
|
642
|
+
return (before?.count ?? 0) > (after?.count ?? 0);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async searchSkills(options: SkillQueryOptions): Promise<PaginatedResult<Skill>> {
|
|
646
|
+
const limit = Math.min(options.limit ?? 20, 100);
|
|
647
|
+
const conditions: string[] = ["visibility = 'public'", "status = 'active'"];
|
|
648
|
+
const params: SqlParams = [];
|
|
649
|
+
|
|
650
|
+
// Basic search using LIKE (sql.js doesn't support FTS5)
|
|
651
|
+
if (options.query) {
|
|
652
|
+
conditions.push('(name LIKE ? OR display_name LIKE ? OR description LIKE ? OR searchable_text LIKE ?)');
|
|
653
|
+
const searchTerm = `%${options.query}%`;
|
|
654
|
+
params.push(searchTerm, searchTerm, searchTerm, searchTerm);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (options.category) {
|
|
658
|
+
conditions.push('category = ?');
|
|
659
|
+
params.push(options.category);
|
|
660
|
+
}
|
|
661
|
+
if (options.author) {
|
|
662
|
+
conditions.push('owner_id = ?');
|
|
663
|
+
params.push(options.author);
|
|
664
|
+
}
|
|
665
|
+
if (options.visibility) {
|
|
666
|
+
// Override the default public visibility
|
|
667
|
+
conditions[0] = 'visibility = ?';
|
|
668
|
+
params.unshift(options.visibility);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Sorting
|
|
672
|
+
const sortField = options.sort ?? 'updated';
|
|
673
|
+
const sortOrder = options.order ?? 'desc';
|
|
674
|
+
const sortMap: Record<string, string> = {
|
|
675
|
+
relevance: 'updated_at',
|
|
676
|
+
uses: "json_extract(stats, '$.totalUses')",
|
|
677
|
+
updated: 'updated_at',
|
|
678
|
+
created: 'created_at',
|
|
679
|
+
name: 'name',
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
let query = `SELECT * FROM skills WHERE ${conditions.join(' AND ')}`;
|
|
683
|
+
let countQuery = `SELECT COUNT(*) as count FROM skills WHERE ${conditions.join(' AND ')}`;
|
|
684
|
+
|
|
685
|
+
if (options.cursor) {
|
|
686
|
+
query += ' AND id > ?';
|
|
687
|
+
params.push(options.cursor);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
query += ` ORDER BY ${sortMap[sortField]} ${sortOrder.toUpperCase()} LIMIT ?`;
|
|
691
|
+
params.push(limit + 1);
|
|
692
|
+
|
|
693
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
694
|
+
const countParams = options.cursor ? params.slice(0, -2) : params.slice(0, -1);
|
|
695
|
+
const countResult = this.get<{ count: number }>(countQuery, countParams);
|
|
696
|
+
|
|
697
|
+
const hasMore = rows.length > limit;
|
|
698
|
+
const items = rows.slice(0, limit).map(row => this.rowToSkill(row));
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
items,
|
|
702
|
+
pagination: {
|
|
703
|
+
total: countResult?.count ?? 0,
|
|
704
|
+
limit,
|
|
705
|
+
hasMore,
|
|
706
|
+
cursor: options.cursor,
|
|
707
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async getSkillsByOwner(ownerId: string, options?: PaginationOptions): Promise<PaginatedResult<Skill>> {
|
|
713
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
714
|
+
const params: SqlParams = [ownerId];
|
|
715
|
+
|
|
716
|
+
let query = 'SELECT * FROM skills WHERE owner_id = ?';
|
|
717
|
+
|
|
718
|
+
if (options?.cursor) {
|
|
719
|
+
query += ' AND id > ?';
|
|
720
|
+
params.push(options.cursor);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
724
|
+
params.push(limit + 1);
|
|
725
|
+
|
|
726
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
727
|
+
const countResult = this.get<{ count: number }>('SELECT COUNT(*) as count FROM skills WHERE owner_id = ?', [ownerId]);
|
|
728
|
+
|
|
729
|
+
const hasMore = rows.length > limit;
|
|
730
|
+
const items = rows.slice(0, limit).map(row => this.rowToSkill(row));
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
items,
|
|
734
|
+
pagination: {
|
|
735
|
+
total: countResult?.count ?? 0,
|
|
736
|
+
limit,
|
|
737
|
+
hasMore,
|
|
738
|
+
cursor: options?.cursor,
|
|
739
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async getDerivedSkills(skillId: string, options?: PaginationOptions): Promise<PaginatedResult<Skill>> {
|
|
745
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
746
|
+
const params: SqlParams = [skillId];
|
|
747
|
+
|
|
748
|
+
let query = "SELECT * FROM skills WHERE json_extract(derived_from, '$.skillId') = ?";
|
|
749
|
+
|
|
750
|
+
if (options?.cursor) {
|
|
751
|
+
query += ' AND id > ?';
|
|
752
|
+
params.push(options.cursor);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
756
|
+
params.push(limit + 1);
|
|
757
|
+
|
|
758
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
759
|
+
const countResult = this.get<{ count: number }>(
|
|
760
|
+
"SELECT COUNT(*) as count FROM skills WHERE json_extract(derived_from, '$.skillId') = ?",
|
|
761
|
+
[skillId]
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
const hasMore = rows.length > limit;
|
|
765
|
+
const items = rows.slice(0, limit).map(row => this.rowToSkill(row));
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
items,
|
|
769
|
+
pagination: {
|
|
770
|
+
total: countResult?.count ?? 0,
|
|
771
|
+
limit,
|
|
772
|
+
hasMore,
|
|
773
|
+
cursor: options?.cursor,
|
|
774
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async incrementSkillUses(id: string): Promise<void> {
|
|
780
|
+
this.run(`
|
|
781
|
+
UPDATE skills
|
|
782
|
+
SET stats = json_set(stats, '$.totalUses', json_extract(stats, '$.totalUses') + 1)
|
|
783
|
+
WHERE id = ?
|
|
784
|
+
`, { $id: id });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// -------------------------------------------------------------------------
|
|
788
|
+
// Versions
|
|
789
|
+
// -------------------------------------------------------------------------
|
|
790
|
+
|
|
791
|
+
async createVersion(version: Version): Promise<Version> {
|
|
792
|
+
this.run(`
|
|
793
|
+
INSERT INTO versions (
|
|
794
|
+
id, skill_id, version, tag, content, package_url, package_size, package_hash,
|
|
795
|
+
changelog, scan_record_id, security_score, security_level, status,
|
|
796
|
+
deprecated_message, published_by, uses, created_at, published_at, deprecated_at
|
|
797
|
+
) VALUES (
|
|
798
|
+
$id, $skillId, $version, $tag, $content, $packageUrl, $packageSize, $packageHash,
|
|
799
|
+
$changelog, $scanRecordId, $securityScore, $securityLevel, $status,
|
|
800
|
+
$deprecatedMessage, $publishedBy, $uses, $createdAt, $publishedAt, $deprecatedAt
|
|
801
|
+
)
|
|
802
|
+
`, {
|
|
803
|
+
$id: version.id,
|
|
804
|
+
$skillId: version.skillId,
|
|
805
|
+
$version: version.version,
|
|
806
|
+
$tag: version.tag ?? null,
|
|
807
|
+
$content: JSON.stringify(version.content),
|
|
808
|
+
$packageUrl: version.packageUrl,
|
|
809
|
+
$packageSize: version.packageSize,
|
|
810
|
+
$packageHash: version.packageHash,
|
|
811
|
+
$changelog: version.changelog ?? null,
|
|
812
|
+
$scanRecordId: version.scanRecordId ?? null,
|
|
813
|
+
$securityScore: version.securityScore ?? null,
|
|
814
|
+
$securityLevel: version.securityLevel ?? null,
|
|
815
|
+
$status: version.status,
|
|
816
|
+
$deprecatedMessage: version.deprecatedMessage ?? null,
|
|
817
|
+
$publishedBy: version.publishedBy ?? null,
|
|
818
|
+
$uses: version.uses,
|
|
819
|
+
$createdAt: version.createdAt,
|
|
820
|
+
$publishedAt: version.publishedAt,
|
|
821
|
+
$deprecatedAt: version.deprecatedAt ?? null,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
return version;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async getVersionById(id: string): Promise<Version | null> {
|
|
828
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM versions WHERE id = ?', [id]);
|
|
829
|
+
return row ? this.rowToVersion(row) : null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async getVersion(skillId: string, version: string): Promise<Version | null> {
|
|
833
|
+
const row = this.get<Record<string, unknown>>(
|
|
834
|
+
'SELECT * FROM versions WHERE skill_id = ? AND version = ?',
|
|
835
|
+
[skillId, version]
|
|
836
|
+
);
|
|
837
|
+
return row ? this.rowToVersion(row) : null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async getLatestVersion(skillId: string): Promise<Version | null> {
|
|
841
|
+
let row = this.get<Record<string, unknown>>(
|
|
842
|
+
"SELECT * FROM versions WHERE skill_id = ? AND tag = 'latest' LIMIT 1",
|
|
843
|
+
[skillId]
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
if (!row) {
|
|
847
|
+
row = this.get<Record<string, unknown>>(
|
|
848
|
+
'SELECT * FROM versions WHERE skill_id = ? ORDER BY published_at DESC LIMIT 1',
|
|
849
|
+
[skillId]
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return row ? this.rowToVersion(row) : null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async getVersions(skillId: string, options?: VersionQueryOptions): Promise<PaginatedResult<Version>> {
|
|
857
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
858
|
+
const params: SqlParams = [skillId];
|
|
859
|
+
|
|
860
|
+
let query = 'SELECT * FROM versions WHERE skill_id = ?';
|
|
861
|
+
|
|
862
|
+
if (!options?.includeDeprecated) {
|
|
863
|
+
query += " AND status != 'deprecated'";
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (options?.cursor) {
|
|
867
|
+
query += ' AND id > ?';
|
|
868
|
+
params.push(options.cursor);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
query += ' ORDER BY published_at DESC LIMIT ?';
|
|
872
|
+
params.push(limit + 1);
|
|
873
|
+
|
|
874
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
875
|
+
const countResult = this.get<{ count: number }>(
|
|
876
|
+
'SELECT COUNT(*) as count FROM versions WHERE skill_id = ?',
|
|
877
|
+
[skillId]
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
const hasMore = rows.length > limit;
|
|
881
|
+
const items = rows.slice(0, limit).map(row => this.rowToVersion(row));
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
items,
|
|
885
|
+
pagination: {
|
|
886
|
+
total: countResult?.count ?? 0,
|
|
887
|
+
limit,
|
|
888
|
+
hasMore,
|
|
889
|
+
cursor: options?.cursor,
|
|
890
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async updateVersion(id: string, updates: Partial<Version>): Promise<Version | null> {
|
|
896
|
+
const existing = await this.getVersionById(id);
|
|
897
|
+
if (!existing) return null;
|
|
898
|
+
|
|
899
|
+
const updated = { ...existing, ...updates };
|
|
900
|
+
|
|
901
|
+
this.run(`
|
|
902
|
+
UPDATE versions SET
|
|
903
|
+
tag = $tag,
|
|
904
|
+
changelog = $changelog,
|
|
905
|
+
scan_record_id = $scanRecordId,
|
|
906
|
+
security_score = $securityScore,
|
|
907
|
+
security_level = $securityLevel,
|
|
908
|
+
status = $status,
|
|
909
|
+
deprecated_message = $deprecatedMessage,
|
|
910
|
+
uses = $uses,
|
|
911
|
+
deprecated_at = $deprecatedAt
|
|
912
|
+
WHERE id = $id
|
|
913
|
+
`, {
|
|
914
|
+
$id: updated.id,
|
|
915
|
+
$tag: updated.tag ?? null,
|
|
916
|
+
$changelog: updated.changelog ?? null,
|
|
917
|
+
$scanRecordId: updated.scanRecordId ?? null,
|
|
918
|
+
$securityScore: updated.securityScore ?? null,
|
|
919
|
+
$securityLevel: updated.securityLevel ?? null,
|
|
920
|
+
$status: updated.status,
|
|
921
|
+
$deprecatedMessage: updated.deprecatedMessage ?? null,
|
|
922
|
+
$uses: updated.uses,
|
|
923
|
+
$deprecatedAt: updated.deprecatedAt ?? null,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
return updated;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async deleteVersion(id: string): Promise<boolean> {
|
|
930
|
+
const before = this.get<{ count: number }>('SELECT COUNT(*) as count FROM versions WHERE id = ?', [id]);
|
|
931
|
+
this.run('DELETE FROM versions WHERE id = ?', { $id: id });
|
|
932
|
+
const after = this.get<{ count: number }>('SELECT COUNT(*) as count FROM versions WHERE id = ?', [id]);
|
|
933
|
+
return (before?.count ?? 0) > (after?.count ?? 0);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async deprecateVersion(id: string, message: string): Promise<Version | null> {
|
|
937
|
+
return this.updateVersion(id, {
|
|
938
|
+
status: 'deprecated',
|
|
939
|
+
deprecatedMessage: message,
|
|
940
|
+
deprecatedAt: now(),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async incrementVersionUses(id: string): Promise<void> {
|
|
945
|
+
this.run('UPDATE versions SET uses = uses + 1 WHERE id = ?', { $id: id });
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// -------------------------------------------------------------------------
|
|
949
|
+
// Scan Records
|
|
950
|
+
// -------------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
async createScanRecord(record: ScanRecord): Promise<ScanRecord> {
|
|
953
|
+
this.run(`
|
|
954
|
+
INSERT INTO scan_records (
|
|
955
|
+
id, skill_id, version_id, user_id, input_type, content_hash, content_size,
|
|
956
|
+
score, level, issue_count, scanner_version, rules_version, rules_applied,
|
|
957
|
+
duration, status, error_message, created_at, completed_at
|
|
958
|
+
) VALUES (
|
|
959
|
+
$id, $skillId, $versionId, $userId, $inputType, $contentHash, $contentSize,
|
|
960
|
+
$score, $level, $issueCount, $scannerVersion, $rulesVersion, $rulesApplied,
|
|
961
|
+
$duration, $status, $errorMessage, $createdAt, $completedAt
|
|
962
|
+
)
|
|
963
|
+
`, {
|
|
964
|
+
$id: record.id,
|
|
965
|
+
$skillId: record.skillId ?? null,
|
|
966
|
+
$versionId: record.versionId ?? null,
|
|
967
|
+
$userId: record.userId ?? null,
|
|
968
|
+
$inputType: record.inputType,
|
|
969
|
+
$contentHash: record.contentHash,
|
|
970
|
+
$contentSize: record.contentSize,
|
|
971
|
+
$score: record.score ?? null,
|
|
972
|
+
$level: record.level ?? null,
|
|
973
|
+
$issueCount: JSON.stringify(record.issueCount),
|
|
974
|
+
$scannerVersion: record.scannerVersion ?? null,
|
|
975
|
+
$rulesVersion: record.rulesVersion ?? null,
|
|
976
|
+
$rulesApplied: record.rulesApplied ?? null,
|
|
977
|
+
$duration: record.duration ?? null,
|
|
978
|
+
$status: record.status,
|
|
979
|
+
$errorMessage: record.errorMessage ?? null,
|
|
980
|
+
$createdAt: record.createdAt,
|
|
981
|
+
$completedAt: record.completedAt ?? null,
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
return record;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async getScanRecordById(id: string): Promise<ScanRecord | null> {
|
|
988
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM scan_records WHERE id = ?', [id]);
|
|
989
|
+
return row ? this.rowToScanRecord(row) : null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async getScanRecords(skillId: string, options?: PaginationOptions): Promise<PaginatedResult<ScanRecord>> {
|
|
993
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
994
|
+
const params: SqlParams = [skillId];
|
|
995
|
+
|
|
996
|
+
let query = 'SELECT * FROM scan_records WHERE skill_id = ?';
|
|
997
|
+
|
|
998
|
+
if (options?.cursor) {
|
|
999
|
+
query += ' AND id > ?';
|
|
1000
|
+
params.push(options.cursor);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
1004
|
+
params.push(limit + 1);
|
|
1005
|
+
|
|
1006
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
1007
|
+
const countResult = this.get<{ count: number }>(
|
|
1008
|
+
'SELECT COUNT(*) as count FROM scan_records WHERE skill_id = ?',
|
|
1009
|
+
[skillId]
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
const hasMore = rows.length > limit;
|
|
1013
|
+
const items = rows.slice(0, limit).map(row => this.rowToScanRecord(row));
|
|
1014
|
+
|
|
1015
|
+
return {
|
|
1016
|
+
items,
|
|
1017
|
+
pagination: {
|
|
1018
|
+
total: countResult?.count ?? 0,
|
|
1019
|
+
limit,
|
|
1020
|
+
hasMore,
|
|
1021
|
+
cursor: options?.cursor,
|
|
1022
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async updateScanRecord(id: string, updates: Partial<ScanRecord>): Promise<ScanRecord | null> {
|
|
1028
|
+
const existing = await this.getScanRecordById(id);
|
|
1029
|
+
if (!existing) return null;
|
|
1030
|
+
|
|
1031
|
+
const updated = { ...existing, ...updates };
|
|
1032
|
+
|
|
1033
|
+
this.run(`
|
|
1034
|
+
UPDATE scan_records SET
|
|
1035
|
+
score = $score,
|
|
1036
|
+
level = $level,
|
|
1037
|
+
issue_count = $issueCount,
|
|
1038
|
+
scanner_version = $scannerVersion,
|
|
1039
|
+
rules_version = $rulesVersion,
|
|
1040
|
+
rules_applied = $rulesApplied,
|
|
1041
|
+
duration = $duration,
|
|
1042
|
+
status = $status,
|
|
1043
|
+
error_message = $errorMessage,
|
|
1044
|
+
completed_at = $completedAt
|
|
1045
|
+
WHERE id = $id
|
|
1046
|
+
`, {
|
|
1047
|
+
$id: updated.id,
|
|
1048
|
+
$score: updated.score ?? null,
|
|
1049
|
+
$level: updated.level ?? null,
|
|
1050
|
+
$issueCount: JSON.stringify(updated.issueCount),
|
|
1051
|
+
$scannerVersion: updated.scannerVersion ?? null,
|
|
1052
|
+
$rulesVersion: updated.rulesVersion ?? null,
|
|
1053
|
+
$rulesApplied: updated.rulesApplied ?? null,
|
|
1054
|
+
$duration: updated.duration ?? null,
|
|
1055
|
+
$status: updated.status,
|
|
1056
|
+
$errorMessage: updated.errorMessage ?? null,
|
|
1057
|
+
$completedAt: updated.completedAt ?? null,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
return updated;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// -------------------------------------------------------------------------
|
|
1064
|
+
// Scan Issues
|
|
1065
|
+
// -------------------------------------------------------------------------
|
|
1066
|
+
|
|
1067
|
+
async createScanIssues(issues: ScanIssue[]): Promise<ScanIssue[]> {
|
|
1068
|
+
for (const issue of issues) {
|
|
1069
|
+
this.run(`
|
|
1070
|
+
INSERT INTO scan_issues (
|
|
1071
|
+
id, scan_record_id, rule_id, rule_name, rule_category, severity,
|
|
1072
|
+
file, line, column_num, end_line, end_column, content, context,
|
|
1073
|
+
message, suggestion, acknowledged, acknowledged_at, acknowledged_by,
|
|
1074
|
+
acknowledged_reason, created_at
|
|
1075
|
+
) VALUES (
|
|
1076
|
+
$id, $scanRecordId, $ruleId, $ruleName, $ruleCategory, $severity,
|
|
1077
|
+
$file, $line, $column, $endLine, $endColumn, $content, $context,
|
|
1078
|
+
$message, $suggestion, $acknowledged, $acknowledgedAt, $acknowledgedBy,
|
|
1079
|
+
$acknowledgedReason, $createdAt
|
|
1080
|
+
)
|
|
1081
|
+
`, {
|
|
1082
|
+
$id: issue.id,
|
|
1083
|
+
$scanRecordId: issue.scanRecordId,
|
|
1084
|
+
$ruleId: issue.ruleId,
|
|
1085
|
+
$ruleName: issue.ruleName,
|
|
1086
|
+
$ruleCategory: issue.ruleCategory,
|
|
1087
|
+
$severity: issue.severity,
|
|
1088
|
+
$file: issue.file ?? null,
|
|
1089
|
+
$line: issue.line ?? null,
|
|
1090
|
+
$column: issue.column ?? null,
|
|
1091
|
+
$endLine: issue.endLine ?? null,
|
|
1092
|
+
$endColumn: issue.endColumn ?? null,
|
|
1093
|
+
$content: issue.content,
|
|
1094
|
+
$context: issue.context ?? null,
|
|
1095
|
+
$message: issue.message,
|
|
1096
|
+
$suggestion: issue.suggestion ?? null,
|
|
1097
|
+
$acknowledged: issue.acknowledged ? 1 : 0,
|
|
1098
|
+
$acknowledgedAt: issue.acknowledgedAt ?? null,
|
|
1099
|
+
$acknowledgedBy: issue.acknowledgedBy ?? null,
|
|
1100
|
+
$acknowledgedReason: issue.acknowledgedReason ?? null,
|
|
1101
|
+
$createdAt: issue.createdAt,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return issues;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async getScanIssues(scanRecordId: string): Promise<ScanIssue[]> {
|
|
1109
|
+
const rows = this.all<Record<string, unknown>>(
|
|
1110
|
+
'SELECT * FROM scan_issues WHERE scan_record_id = ? ORDER BY severity DESC, line ASC',
|
|
1111
|
+
[scanRecordId]
|
|
1112
|
+
);
|
|
1113
|
+
return rows.map(row => this.rowToScanIssue(row));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async acknowledgeIssue(id: string, userId: string, reason?: string): Promise<ScanIssue | null> {
|
|
1117
|
+
this.run(`
|
|
1118
|
+
UPDATE scan_issues SET
|
|
1119
|
+
acknowledged = 1,
|
|
1120
|
+
acknowledged_at = $acknowledgedAt,
|
|
1121
|
+
acknowledged_by = $acknowledgedBy,
|
|
1122
|
+
acknowledged_reason = $acknowledgedReason
|
|
1123
|
+
WHERE id = $id
|
|
1124
|
+
`, {
|
|
1125
|
+
$id: id,
|
|
1126
|
+
$acknowledgedAt: now(),
|
|
1127
|
+
$acknowledgedBy: userId,
|
|
1128
|
+
$acknowledgedReason: reason ?? null,
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM scan_issues WHERE id = ?', [id]);
|
|
1132
|
+
return row ? this.rowToScanIssue(row) : null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// -------------------------------------------------------------------------
|
|
1136
|
+
// Feedback
|
|
1137
|
+
// -------------------------------------------------------------------------
|
|
1138
|
+
|
|
1139
|
+
async createFeedback(feedback: Feedback): Promise<Feedback> {
|
|
1140
|
+
this.run(`
|
|
1141
|
+
INSERT INTO feedbacks (
|
|
1142
|
+
id, skill_id, skill_version, feedback_type, rating, comment,
|
|
1143
|
+
context, status, reviewer_notes, reviewed_at, reviewed_by,
|
|
1144
|
+
source_ip_hash, created_at
|
|
1145
|
+
) VALUES (
|
|
1146
|
+
$id, $skillId, $skillVersion, $feedbackType, $rating, $comment,
|
|
1147
|
+
$context, $status, $reviewerNotes, $reviewedAt, $reviewedBy,
|
|
1148
|
+
$sourceIpHash, $createdAt
|
|
1149
|
+
)
|
|
1150
|
+
`, {
|
|
1151
|
+
$id: feedback.id,
|
|
1152
|
+
$skillId: feedback.skillId,
|
|
1153
|
+
$skillVersion: feedback.skillVersion,
|
|
1154
|
+
$feedbackType: feedback.feedbackType,
|
|
1155
|
+
$rating: feedback.rating,
|
|
1156
|
+
$comment: feedback.comment ?? null,
|
|
1157
|
+
$context: JSON.stringify(feedback.context),
|
|
1158
|
+
$status: feedback.status,
|
|
1159
|
+
$reviewerNotes: feedback.reviewerNotes ?? null,
|
|
1160
|
+
$reviewedAt: feedback.reviewedAt ?? null,
|
|
1161
|
+
$reviewedBy: feedback.reviewedBy ?? null,
|
|
1162
|
+
$sourceIpHash: feedback.sourceIpHash ?? null,
|
|
1163
|
+
$createdAt: feedback.createdAt,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
return feedback;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async getFeedbackById(id: string): Promise<Feedback | null> {
|
|
1170
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM feedbacks WHERE id = ?', [id]);
|
|
1171
|
+
return row ? this.rowToFeedback(row) : null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async getFeedbacks(skillId: string, options?: FeedbackQueryOptions): Promise<PaginatedResult<Feedback>> {
|
|
1175
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
1176
|
+
const params: SqlParams = [skillId];
|
|
1177
|
+
const conditions: string[] = ['skill_id = ?'];
|
|
1178
|
+
|
|
1179
|
+
if (options?.feedbackType) {
|
|
1180
|
+
conditions.push('feedback_type = ?');
|
|
1181
|
+
params.push(options.feedbackType);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (options?.status) {
|
|
1185
|
+
conditions.push('status = ?');
|
|
1186
|
+
params.push(options.status);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (options?.cursor) {
|
|
1190
|
+
conditions.push('id > ?');
|
|
1191
|
+
params.push(options.cursor);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const whereClause = conditions.join(' AND ');
|
|
1195
|
+
const query = `SELECT * FROM feedbacks WHERE ${whereClause} ORDER BY created_at DESC LIMIT ?`;
|
|
1196
|
+
params.push(limit + 1);
|
|
1197
|
+
|
|
1198
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
1199
|
+
const countResult = this.get<{ count: number }>(
|
|
1200
|
+
'SELECT COUNT(*) as count FROM feedbacks WHERE skill_id = ?',
|
|
1201
|
+
[skillId]
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
const hasMore = rows.length > limit;
|
|
1205
|
+
const items = rows.slice(0, limit).map(row => this.rowToFeedback(row));
|
|
1206
|
+
|
|
1207
|
+
return {
|
|
1208
|
+
items,
|
|
1209
|
+
pagination: {
|
|
1210
|
+
total: countResult?.count ?? 0,
|
|
1211
|
+
limit,
|
|
1212
|
+
hasMore,
|
|
1213
|
+
cursor: options?.cursor,
|
|
1214
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
1215
|
+
},
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async updateFeedback(id: string, updates: Partial<Feedback>): Promise<Feedback | null> {
|
|
1220
|
+
const existing = await this.getFeedbackById(id);
|
|
1221
|
+
if (!existing) return null;
|
|
1222
|
+
|
|
1223
|
+
const updated = { ...existing, ...updates };
|
|
1224
|
+
|
|
1225
|
+
this.run(`
|
|
1226
|
+
UPDATE feedbacks SET
|
|
1227
|
+
status = $status,
|
|
1228
|
+
reviewer_notes = $reviewerNotes,
|
|
1229
|
+
reviewed_at = $reviewedAt,
|
|
1230
|
+
reviewed_by = $reviewedBy
|
|
1231
|
+
WHERE id = $id
|
|
1232
|
+
`, {
|
|
1233
|
+
$id: updated.id,
|
|
1234
|
+
$status: updated.status,
|
|
1235
|
+
$reviewerNotes: updated.reviewerNotes ?? null,
|
|
1236
|
+
$reviewedAt: updated.reviewedAt ?? null,
|
|
1237
|
+
$reviewedBy: updated.reviewedBy ?? null,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
return updated;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// -------------------------------------------------------------------------
|
|
1244
|
+
// Feedback Queue
|
|
1245
|
+
// -------------------------------------------------------------------------
|
|
1246
|
+
|
|
1247
|
+
async enqueueFeedback(item: FeedbackQueueItem): Promise<FeedbackQueueItem> {
|
|
1248
|
+
this.run(`
|
|
1249
|
+
INSERT INTO feedback_queue (
|
|
1250
|
+
id, skill_name, skill_version, feedback, status, attempts, max_attempts,
|
|
1251
|
+
last_attempt_at, next_retry_at, last_error, base_delay, current_delay,
|
|
1252
|
+
max_delay, created_at, processed_at
|
|
1253
|
+
) VALUES (
|
|
1254
|
+
$id, $skillName, $skillVersion, $feedback, $status, $attempts, $maxAttempts,
|
|
1255
|
+
$lastAttemptAt, $nextRetryAt, $lastError, $baseDelay, $currentDelay,
|
|
1256
|
+
$maxDelay, $createdAt, $processedAt
|
|
1257
|
+
)
|
|
1258
|
+
`, {
|
|
1259
|
+
$id: item.id,
|
|
1260
|
+
$skillName: item.skillName,
|
|
1261
|
+
$skillVersion: item.skillVersion,
|
|
1262
|
+
$feedback: JSON.stringify(item.feedback),
|
|
1263
|
+
$status: item.status,
|
|
1264
|
+
$attempts: item.attempts,
|
|
1265
|
+
$maxAttempts: item.maxAttempts,
|
|
1266
|
+
$lastAttemptAt: item.lastAttemptAt ?? null,
|
|
1267
|
+
$nextRetryAt: item.nextRetryAt ?? null,
|
|
1268
|
+
$lastError: item.lastError ?? null,
|
|
1269
|
+
$baseDelay: item.baseDelay,
|
|
1270
|
+
$currentDelay: item.currentDelay,
|
|
1271
|
+
$maxDelay: item.maxDelay,
|
|
1272
|
+
$createdAt: item.createdAt,
|
|
1273
|
+
$processedAt: item.processedAt ?? null,
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
return item;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async getPendingQueueItems(options?: QueueQueryOptions): Promise<FeedbackQueueItem[]> {
|
|
1280
|
+
const limit = options?.limit ?? 10;
|
|
1281
|
+
|
|
1282
|
+
let query: string;
|
|
1283
|
+
let params: SqlParams;
|
|
1284
|
+
|
|
1285
|
+
if (options?.status) {
|
|
1286
|
+
query = `
|
|
1287
|
+
SELECT * FROM feedback_queue
|
|
1288
|
+
WHERE status = ?
|
|
1289
|
+
AND (next_retry_at IS NULL OR next_retry_at <= datetime('now'))
|
|
1290
|
+
ORDER BY created_at ASC LIMIT ?
|
|
1291
|
+
`;
|
|
1292
|
+
params = [options.status, limit];
|
|
1293
|
+
} else {
|
|
1294
|
+
query = `
|
|
1295
|
+
SELECT * FROM feedback_queue
|
|
1296
|
+
WHERE status IN ('pending', 'retrying')
|
|
1297
|
+
AND (next_retry_at IS NULL OR next_retry_at <= datetime('now'))
|
|
1298
|
+
ORDER BY created_at ASC LIMIT ?
|
|
1299
|
+
`;
|
|
1300
|
+
params = [limit];
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
1304
|
+
return rows.map(row => this.rowToQueueItem(row));
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async updateQueueItem(id: string, updates: Partial<FeedbackQueueItem>): Promise<FeedbackQueueItem | null> {
|
|
1308
|
+
const row = this.get<Record<string, unknown>>('SELECT * FROM feedback_queue WHERE id = ?', [id]);
|
|
1309
|
+
if (!row) return null;
|
|
1310
|
+
|
|
1311
|
+
const existing = this.rowToQueueItem(row);
|
|
1312
|
+
const updated = { ...existing, ...updates };
|
|
1313
|
+
|
|
1314
|
+
this.run(`
|
|
1315
|
+
UPDATE feedback_queue SET
|
|
1316
|
+
status = $status,
|
|
1317
|
+
attempts = $attempts,
|
|
1318
|
+
last_attempt_at = $lastAttemptAt,
|
|
1319
|
+
next_retry_at = $nextRetryAt,
|
|
1320
|
+
last_error = $lastError,
|
|
1321
|
+
current_delay = $currentDelay,
|
|
1322
|
+
processed_at = $processedAt
|
|
1323
|
+
WHERE id = $id
|
|
1324
|
+
`, {
|
|
1325
|
+
$id: updated.id,
|
|
1326
|
+
$status: updated.status,
|
|
1327
|
+
$attempts: updated.attempts,
|
|
1328
|
+
$lastAttemptAt: updated.lastAttemptAt ?? null,
|
|
1329
|
+
$nextRetryAt: updated.nextRetryAt ?? null,
|
|
1330
|
+
$lastError: updated.lastError ?? null,
|
|
1331
|
+
$currentDelay: updated.currentDelay,
|
|
1332
|
+
$processedAt: updated.processedAt ?? null,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
return updated;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async removeFromQueue(id: string): Promise<boolean> {
|
|
1339
|
+
const before = this.get<{ count: number }>('SELECT COUNT(*) as count FROM feedback_queue WHERE id = ?', [id]);
|
|
1340
|
+
this.run('DELETE FROM feedback_queue WHERE id = ?', { $id: id });
|
|
1341
|
+
const after = this.get<{ count: number }>('SELECT COUNT(*) as count FROM feedback_queue WHERE id = ?', [id]);
|
|
1342
|
+
return (before?.count ?? 0) > (after?.count ?? 0);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
async getQueueSize(): Promise<number> {
|
|
1346
|
+
const result = this.get<{ count: number }>(
|
|
1347
|
+
"SELECT COUNT(*) as count FROM feedback_queue WHERE status IN ('pending', 'retrying')"
|
|
1348
|
+
);
|
|
1349
|
+
return result?.count ?? 0;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
async getDeadLetterItems(options?: PaginationOptions): Promise<PaginatedResult<FeedbackQueueItem>> {
|
|
1353
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
1354
|
+
const params: SqlParams = [];
|
|
1355
|
+
|
|
1356
|
+
let query = "SELECT * FROM feedback_queue WHERE status = 'dead_letter'";
|
|
1357
|
+
|
|
1358
|
+
if (options?.cursor) {
|
|
1359
|
+
query += ' AND id > ?';
|
|
1360
|
+
params.push(options.cursor);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
1364
|
+
params.push(limit + 1);
|
|
1365
|
+
|
|
1366
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
1367
|
+
const countResult = this.get<{ count: number }>(
|
|
1368
|
+
"SELECT COUNT(*) as count FROM feedback_queue WHERE status = 'dead_letter'"
|
|
1369
|
+
);
|
|
1370
|
+
|
|
1371
|
+
const hasMore = rows.length > limit;
|
|
1372
|
+
const items = rows.slice(0, limit).map(row => this.rowToQueueItem(row));
|
|
1373
|
+
|
|
1374
|
+
return {
|
|
1375
|
+
items,
|
|
1376
|
+
pagination: {
|
|
1377
|
+
total: countResult?.count ?? 0,
|
|
1378
|
+
limit,
|
|
1379
|
+
hasMore,
|
|
1380
|
+
cursor: options?.cursor,
|
|
1381
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
1382
|
+
},
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// -------------------------------------------------------------------------
|
|
1387
|
+
// Audit Logs
|
|
1388
|
+
// -------------------------------------------------------------------------
|
|
1389
|
+
|
|
1390
|
+
async createAuditLog(log: AuditLog): Promise<AuditLog> {
|
|
1391
|
+
this.run(`
|
|
1392
|
+
INSERT INTO audit_logs (
|
|
1393
|
+
id, timestamp, event_type, actor, resource, action, result,
|
|
1394
|
+
details, changes, error_code, error_message
|
|
1395
|
+
) VALUES (
|
|
1396
|
+
$id, $timestamp, $eventType, $actor, $resource, $action, $result,
|
|
1397
|
+
$details, $changes, $errorCode, $errorMessage
|
|
1398
|
+
)
|
|
1399
|
+
`, {
|
|
1400
|
+
$id: log.id,
|
|
1401
|
+
$timestamp: log.timestamp,
|
|
1402
|
+
$eventType: log.eventType,
|
|
1403
|
+
$actor: JSON.stringify(log.actor),
|
|
1404
|
+
$resource: JSON.stringify(log.resource),
|
|
1405
|
+
$action: log.action,
|
|
1406
|
+
$result: log.result,
|
|
1407
|
+
$details: log.details ? JSON.stringify(log.details) : null,
|
|
1408
|
+
$changes: log.changes ? JSON.stringify(log.changes) : null,
|
|
1409
|
+
$errorCode: log.errorCode ?? null,
|
|
1410
|
+
$errorMessage: log.errorMessage ?? null,
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
return log;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async getAuditLogs(options?: AuditQueryOptions): Promise<PaginatedResult<AuditLog>> {
|
|
1417
|
+
const limit = Math.min(options?.limit ?? 50, 100);
|
|
1418
|
+
const conditions: string[] = ['1=1'];
|
|
1419
|
+
const params: SqlParams = [];
|
|
1420
|
+
|
|
1421
|
+
if (options?.eventType) {
|
|
1422
|
+
conditions.push('event_type = ?');
|
|
1423
|
+
params.push(options.eventType);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (options?.actor) {
|
|
1427
|
+
conditions.push("json_extract(actor, '$.id') = ?");
|
|
1428
|
+
params.push(options.actor);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (options?.resourceType) {
|
|
1432
|
+
conditions.push("json_extract(resource, '$.type') = ?");
|
|
1433
|
+
params.push(options.resourceType);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (options?.resourceName) {
|
|
1437
|
+
conditions.push("json_extract(resource, '$.name') = ?");
|
|
1438
|
+
params.push(options.resourceName);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (options?.from) {
|
|
1442
|
+
conditions.push('timestamp >= ?');
|
|
1443
|
+
params.push(options.from);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (options?.to) {
|
|
1447
|
+
conditions.push('timestamp <= ?');
|
|
1448
|
+
params.push(options.to);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (options?.cursor) {
|
|
1452
|
+
conditions.push('id > ?');
|
|
1453
|
+
params.push(options.cursor);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const whereClause = conditions.join(' AND ');
|
|
1457
|
+
const countQuery = `SELECT COUNT(*) as count FROM audit_logs WHERE ${whereClause}`;
|
|
1458
|
+
const query = `SELECT * FROM audit_logs WHERE ${whereClause} ORDER BY timestamp DESC LIMIT ?`;
|
|
1459
|
+
|
|
1460
|
+
const countParams = [...params];
|
|
1461
|
+
params.push(limit + 1);
|
|
1462
|
+
|
|
1463
|
+
const rows = this.all<Record<string, unknown>>(query, params);
|
|
1464
|
+
const countResult = this.get<{ count: number }>(countQuery, countParams);
|
|
1465
|
+
|
|
1466
|
+
const hasMore = rows.length > limit;
|
|
1467
|
+
const items = rows.slice(0, limit).map(row => this.rowToAuditLog(row));
|
|
1468
|
+
|
|
1469
|
+
return {
|
|
1470
|
+
items,
|
|
1471
|
+
pagination: {
|
|
1472
|
+
total: countResult?.count ?? 0,
|
|
1473
|
+
limit,
|
|
1474
|
+
hasMore,
|
|
1475
|
+
cursor: options?.cursor,
|
|
1476
|
+
nextCursor: hasMore && items.length > 0 ? items[items.length - 1]?.id : undefined,
|
|
1477
|
+
},
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async getResourceAuditLogs(
|
|
1482
|
+
resourceType: string,
|
|
1483
|
+
resourceId: string,
|
|
1484
|
+
options?: PaginationOptions
|
|
1485
|
+
): Promise<PaginatedResult<AuditLog>> {
|
|
1486
|
+
return this.getAuditLogs({
|
|
1487
|
+
...options,
|
|
1488
|
+
resourceType,
|
|
1489
|
+
resourceName: resourceId,
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// -------------------------------------------------------------------------
|
|
1494
|
+
// Cache Metadata
|
|
1495
|
+
// -------------------------------------------------------------------------
|
|
1496
|
+
|
|
1497
|
+
async setCacheMetadata(metadata: CacheMetadata): Promise<CacheMetadata> {
|
|
1498
|
+
this.run(`
|
|
1499
|
+
INSERT OR REPLACE INTO cache_metadata (
|
|
1500
|
+
id, skill_name, version, cached_at, expires_at, size,
|
|
1501
|
+
hit_count, last_hit_at, source, integrity_hash
|
|
1502
|
+
) VALUES (
|
|
1503
|
+
$id, $skillName, $version, $cachedAt, $expiresAt, $size,
|
|
1504
|
+
$hitCount, $lastHitAt, $source, $integrityHash
|
|
1505
|
+
)
|
|
1506
|
+
`, {
|
|
1507
|
+
$id: metadata.id,
|
|
1508
|
+
$skillName: metadata.skillName,
|
|
1509
|
+
$version: metadata.version,
|
|
1510
|
+
$cachedAt: metadata.cachedAt,
|
|
1511
|
+
$expiresAt: metadata.expiresAt ?? null,
|
|
1512
|
+
$size: metadata.size ?? null,
|
|
1513
|
+
$hitCount: metadata.hitCount,
|
|
1514
|
+
$lastHitAt: metadata.lastHitAt ?? null,
|
|
1515
|
+
$source: metadata.source ?? null,
|
|
1516
|
+
$integrityHash: metadata.integrityHash ?? null,
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
return metadata;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
async getCacheMetadata(skillName: string, version: string): Promise<CacheMetadata | null> {
|
|
1523
|
+
const row = this.get<Record<string, unknown>>(
|
|
1524
|
+
'SELECT * FROM cache_metadata WHERE skill_name = ? AND version = ?',
|
|
1525
|
+
[skillName, version]
|
|
1526
|
+
);
|
|
1527
|
+
return row ? this.rowToCacheMetadata(row) : null;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
async getAllCacheMetadata(): Promise<CacheMetadata[]> {
|
|
1531
|
+
const rows = this.all<Record<string, unknown>>('SELECT * FROM cache_metadata ORDER BY cached_at DESC');
|
|
1532
|
+
return rows.map(row => this.rowToCacheMetadata(row));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
async deleteCacheMetadata(skillName: string, version?: string): Promise<boolean> {
|
|
1536
|
+
const before = version
|
|
1537
|
+
? this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ? AND version = ?', [skillName, version])
|
|
1538
|
+
: this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ?', [skillName]);
|
|
1539
|
+
|
|
1540
|
+
if (version) {
|
|
1541
|
+
this.run('DELETE FROM cache_metadata WHERE skill_name = ? AND version = ?', { $skillName: skillName, $version: version });
|
|
1542
|
+
} else {
|
|
1543
|
+
this.run('DELETE FROM cache_metadata WHERE skill_name = ?', { $skillName: skillName });
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const after = version
|
|
1547
|
+
? this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ? AND version = ?', [skillName, version])
|
|
1548
|
+
: this.get<{ count: number }>('SELECT COUNT(*) as count FROM cache_metadata WHERE skill_name = ?', [skillName]);
|
|
1549
|
+
|
|
1550
|
+
return (before?.count ?? 0) > (after?.count ?? 0);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async incrementCacheHit(skillName: string, version: string): Promise<void> {
|
|
1554
|
+
this.run(`
|
|
1555
|
+
UPDATE cache_metadata SET
|
|
1556
|
+
hit_count = hit_count + 1,
|
|
1557
|
+
last_hit_at = datetime('now')
|
|
1558
|
+
WHERE skill_name = ? AND version = ?
|
|
1559
|
+
`, { $skillName: skillName, $version: version });
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async getExpiredCacheEntries(): Promise<CacheMetadata[]> {
|
|
1563
|
+
const rows = this.all<Record<string, unknown>>(
|
|
1564
|
+
"SELECT * FROM cache_metadata WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')"
|
|
1565
|
+
);
|
|
1566
|
+
return rows.map(row => this.rowToCacheMetadata(row));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// -------------------------------------------------------------------------
|
|
1570
|
+
// Use Records
|
|
1571
|
+
// -------------------------------------------------------------------------
|
|
1572
|
+
|
|
1573
|
+
async createUseRecord(record: UseRecord): Promise<UseRecord> {
|
|
1574
|
+
this.run(`
|
|
1575
|
+
INSERT INTO use_records (
|
|
1576
|
+
id, skill_id, version_id, user_id, source, cache_hit, client_version,
|
|
1577
|
+
platform, arch, ip, user_agent, country, region, created_at
|
|
1578
|
+
) VALUES (
|
|
1579
|
+
$id, $skillId, $versionId, $userId, $source, $cacheHit, $clientVersion,
|
|
1580
|
+
$platform, $arch, $ip, $userAgent, $country, $region, $createdAt
|
|
1581
|
+
)
|
|
1582
|
+
`, {
|
|
1583
|
+
$id: record.id,
|
|
1584
|
+
$skillId: record.skillId,
|
|
1585
|
+
$versionId: record.versionId ?? null,
|
|
1586
|
+
$userId: record.userId ?? null,
|
|
1587
|
+
$source: record.source,
|
|
1588
|
+
$cacheHit: record.cacheHit ? 1 : 0,
|
|
1589
|
+
$clientVersion: record.clientVersion ?? null,
|
|
1590
|
+
$platform: record.platform ?? null,
|
|
1591
|
+
$arch: record.arch ?? null,
|
|
1592
|
+
$ip: record.ip,
|
|
1593
|
+
$userAgent: record.userAgent ?? null,
|
|
1594
|
+
$country: record.country ?? null,
|
|
1595
|
+
$region: record.region ?? null,
|
|
1596
|
+
$createdAt: record.createdAt,
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
return record;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async getSkillUseStats(skillId: string, days: number): Promise<{
|
|
1603
|
+
total: number;
|
|
1604
|
+
bySource: Record<string, number>;
|
|
1605
|
+
cacheHitRate: number;
|
|
1606
|
+
daily: { date: string; count: number }[];
|
|
1607
|
+
}> {
|
|
1608
|
+
const fromDate = new Date();
|
|
1609
|
+
fromDate.setDate(fromDate.getDate() - days);
|
|
1610
|
+
const fromDateStr = fromDate.toISOString();
|
|
1611
|
+
|
|
1612
|
+
// Total uses
|
|
1613
|
+
const totalResult = this.get<{ count: number }>(`
|
|
1614
|
+
SELECT COUNT(*) as count FROM use_records
|
|
1615
|
+
WHERE skill_id = ? AND created_at >= ?
|
|
1616
|
+
`, [skillId, fromDateStr]);
|
|
1617
|
+
|
|
1618
|
+
// By source
|
|
1619
|
+
const sourceRows = this.all<{ source: string; count: number }>(`
|
|
1620
|
+
SELECT source, COUNT(*) as count FROM use_records
|
|
1621
|
+
WHERE skill_id = ? AND created_at >= ?
|
|
1622
|
+
GROUP BY source
|
|
1623
|
+
`, [skillId, fromDateStr]);
|
|
1624
|
+
|
|
1625
|
+
const bySource: Record<string, number> = {};
|
|
1626
|
+
sourceRows.forEach(row => {
|
|
1627
|
+
bySource[row.source] = row.count;
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
// Cache hit rate
|
|
1631
|
+
const cacheHitResult = this.get<{ hits: number; total: number }>(`
|
|
1632
|
+
SELECT
|
|
1633
|
+
SUM(cache_hit) as hits,
|
|
1634
|
+
COUNT(*) as total
|
|
1635
|
+
FROM use_records
|
|
1636
|
+
WHERE skill_id = ? AND created_at >= ?
|
|
1637
|
+
`, [skillId, fromDateStr]);
|
|
1638
|
+
|
|
1639
|
+
const cacheHitRate = (cacheHitResult?.total ?? 0) > 0
|
|
1640
|
+
? (cacheHitResult?.hits ?? 0) / (cacheHitResult?.total ?? 1)
|
|
1641
|
+
: 0;
|
|
1642
|
+
|
|
1643
|
+
// Daily breakdown
|
|
1644
|
+
const dailyRows = this.all<{ date: string; count: number }>(`
|
|
1645
|
+
SELECT
|
|
1646
|
+
date(created_at) as date,
|
|
1647
|
+
COUNT(*) as count
|
|
1648
|
+
FROM use_records
|
|
1649
|
+
WHERE skill_id = ? AND created_at >= ?
|
|
1650
|
+
GROUP BY date(created_at)
|
|
1651
|
+
ORDER BY date ASC
|
|
1652
|
+
`, [skillId, fromDateStr]);
|
|
1653
|
+
|
|
1654
|
+
return {
|
|
1655
|
+
total: totalResult?.count ?? 0,
|
|
1656
|
+
bySource,
|
|
1657
|
+
cacheHitRate,
|
|
1658
|
+
daily: dailyRows,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// -------------------------------------------------------------------------
|
|
1663
|
+
// Row Converters
|
|
1664
|
+
// -------------------------------------------------------------------------
|
|
1665
|
+
|
|
1666
|
+
private rowToSkill(row: Record<string, unknown>): Skill {
|
|
1667
|
+
return {
|
|
1668
|
+
id: row['id'] as string,
|
|
1669
|
+
name: row['name'] as string,
|
|
1670
|
+
scope: row['scope'] as string | undefined,
|
|
1671
|
+
fullName: row['full_name'] as string,
|
|
1672
|
+
ownerId: row['owner_id'] as string | undefined,
|
|
1673
|
+
ownerType: (row['owner_type'] as 'user' | 'organization') ?? 'user',
|
|
1674
|
+
displayName: row['display_name'] as string | undefined,
|
|
1675
|
+
description: row['description'] as string,
|
|
1676
|
+
category: row['category'] as string | undefined,
|
|
1677
|
+
keywords: JSON.parse(row['keywords'] as string || '[]'),
|
|
1678
|
+
license: row['license'] as string | undefined,
|
|
1679
|
+
repository: row['repository'] as string | undefined,
|
|
1680
|
+
homepage: row['homepage'] as string | undefined,
|
|
1681
|
+
derivedFrom: row['derived_from'] ? JSON.parse(row['derived_from'] as string) : undefined,
|
|
1682
|
+
latestVersion: row['latest_version'] as string,
|
|
1683
|
+
latestVersionId: row['latest_version_id'] as string | undefined,
|
|
1684
|
+
visibility: (row['visibility'] as 'public' | 'private' | 'unlisted') ?? 'public',
|
|
1685
|
+
status: (row['status'] as 'active' | 'deprecated' | 'deleted') ?? 'active',
|
|
1686
|
+
deprecatedMessage: row['deprecated_message'] as string | undefined,
|
|
1687
|
+
securityScore: row['security_score'] as number | undefined,
|
|
1688
|
+
securityLevel: row['security_level'] as 'safe' | 'low' | 'medium' | 'high' | undefined,
|
|
1689
|
+
lastScanAt: row['last_scan_at'] as string | undefined,
|
|
1690
|
+
stats: JSON.parse(row['stats'] as string || '{}'),
|
|
1691
|
+
rating: JSON.parse(row['rating'] as string || '{"average":0,"count":0}'),
|
|
1692
|
+
createdAt: row['created_at'] as string,
|
|
1693
|
+
updatedAt: row['updated_at'] as string,
|
|
1694
|
+
publishedAt: row['published_at'] as string | undefined,
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
private rowToVersion(row: Record<string, unknown>): Version {
|
|
1699
|
+
return {
|
|
1700
|
+
id: row['id'] as string,
|
|
1701
|
+
skillId: row['skill_id'] as string,
|
|
1702
|
+
version: row['version'] as string,
|
|
1703
|
+
tag: row['tag'] as string | undefined,
|
|
1704
|
+
content: JSON.parse(row['content'] as string),
|
|
1705
|
+
packageUrl: row['package_url'] as string,
|
|
1706
|
+
packageSize: row['package_size'] as number,
|
|
1707
|
+
packageHash: row['package_hash'] as string,
|
|
1708
|
+
changelog: row['changelog'] as string | undefined,
|
|
1709
|
+
scanRecordId: row['scan_record_id'] as string | undefined,
|
|
1710
|
+
securityScore: row['security_score'] as number | undefined,
|
|
1711
|
+
securityLevel: row['security_level'] as 'safe' | 'low' | 'medium' | 'high' | undefined,
|
|
1712
|
+
status: (row['status'] as 'published' | 'deprecated' | 'yanked') ?? 'published',
|
|
1713
|
+
deprecatedMessage: row['deprecated_message'] as string | undefined,
|
|
1714
|
+
publishedBy: row['published_by'] as string | undefined,
|
|
1715
|
+
uses: row['uses'] as number,
|
|
1716
|
+
createdAt: row['created_at'] as string,
|
|
1717
|
+
publishedAt: row['published_at'] as string,
|
|
1718
|
+
deprecatedAt: row['deprecated_at'] as string | undefined,
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private rowToScanRecord(row: Record<string, unknown>): ScanRecord {
|
|
1723
|
+
return {
|
|
1724
|
+
id: row['id'] as string,
|
|
1725
|
+
skillId: row['skill_id'] as string | undefined,
|
|
1726
|
+
versionId: row['version_id'] as string | undefined,
|
|
1727
|
+
userId: row['user_id'] as string | undefined,
|
|
1728
|
+
inputType: row['input_type'] as 'publish' | 'manual' | 'scheduled' | 'api' | 'cache',
|
|
1729
|
+
contentHash: row['content_hash'] as string,
|
|
1730
|
+
contentSize: row['content_size'] as number,
|
|
1731
|
+
score: row['score'] as number | undefined,
|
|
1732
|
+
level: row['level'] as 'safe' | 'low' | 'medium' | 'high' | undefined,
|
|
1733
|
+
issueCount: JSON.parse(row['issue_count'] as string || '{"high":0,"medium":0,"low":0}'),
|
|
1734
|
+
scannerVersion: row['scanner_version'] as string | undefined,
|
|
1735
|
+
rulesVersion: row['rules_version'] as string | undefined,
|
|
1736
|
+
rulesApplied: row['rules_applied'] as number | undefined,
|
|
1737
|
+
duration: row['duration'] as number | undefined,
|
|
1738
|
+
status: (row['status'] as 'pending' | 'scanning' | 'completed' | 'failed') ?? 'pending',
|
|
1739
|
+
errorMessage: row['error_message'] as string | undefined,
|
|
1740
|
+
createdAt: row['created_at'] as string,
|
|
1741
|
+
completedAt: row['completed_at'] as string | undefined,
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
private rowToScanIssue(row: Record<string, unknown>): ScanIssue {
|
|
1746
|
+
return {
|
|
1747
|
+
id: row['id'] as string,
|
|
1748
|
+
scanRecordId: row['scan_record_id'] as string,
|
|
1749
|
+
ruleId: row['rule_id'] as string,
|
|
1750
|
+
ruleName: row['rule_name'] as string,
|
|
1751
|
+
ruleCategory: row['rule_category'] as 'command' | 'secret' | 'injection' | 'resource' | 'privacy' | 'network',
|
|
1752
|
+
severity: row['severity'] as 'high' | 'medium' | 'low',
|
|
1753
|
+
file: row['file'] as string | undefined,
|
|
1754
|
+
line: row['line'] as number | undefined,
|
|
1755
|
+
column: row['column_num'] as number | undefined,
|
|
1756
|
+
endLine: row['end_line'] as number | undefined,
|
|
1757
|
+
endColumn: row['end_column'] as number | undefined,
|
|
1758
|
+
content: row['content'] as string,
|
|
1759
|
+
context: row['context'] as string | undefined,
|
|
1760
|
+
message: row['message'] as string,
|
|
1761
|
+
suggestion: row['suggestion'] as string | undefined,
|
|
1762
|
+
acknowledged: !!row['acknowledged'],
|
|
1763
|
+
acknowledgedAt: row['acknowledged_at'] as string | undefined,
|
|
1764
|
+
acknowledgedBy: row['acknowledged_by'] as string | undefined,
|
|
1765
|
+
acknowledgedReason: row['acknowledged_reason'] as string | undefined,
|
|
1766
|
+
createdAt: row['created_at'] as string,
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
private rowToFeedback(row: Record<string, unknown>): Feedback {
|
|
1771
|
+
return {
|
|
1772
|
+
id: row['id'] as string,
|
|
1773
|
+
skillId: row['skill_id'] as string,
|
|
1774
|
+
skillVersion: row['skill_version'] as string,
|
|
1775
|
+
feedbackType: row['feedback_type'] as 'success' | 'failure' | 'suggestion' | 'bug',
|
|
1776
|
+
rating: row['rating'] as number,
|
|
1777
|
+
comment: row['comment'] as string | undefined,
|
|
1778
|
+
context: JSON.parse(row['context'] as string || '{}'),
|
|
1779
|
+
status: (row['status'] as 'pending' | 'reviewed' | 'accepted' | 'rejected') ?? 'pending',
|
|
1780
|
+
reviewerNotes: row['reviewer_notes'] as string | undefined,
|
|
1781
|
+
reviewedAt: row['reviewed_at'] as string | undefined,
|
|
1782
|
+
reviewedBy: row['reviewed_by'] as string | undefined,
|
|
1783
|
+
sourceIpHash: row['source_ip_hash'] as string | undefined,
|
|
1784
|
+
createdAt: row['created_at'] as string,
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
private rowToQueueItem(row: Record<string, unknown>): FeedbackQueueItem {
|
|
1789
|
+
return {
|
|
1790
|
+
id: row['id'] as string,
|
|
1791
|
+
skillName: row['skill_name'] as string,
|
|
1792
|
+
skillVersion: row['skill_version'] as string,
|
|
1793
|
+
feedback: JSON.parse(row['feedback'] as string),
|
|
1794
|
+
status: (row['status'] as 'pending' | 'retrying' | 'failed' | 'dead_letter') ?? 'pending',
|
|
1795
|
+
attempts: row['attempts'] as number,
|
|
1796
|
+
maxAttempts: row['max_attempts'] as number,
|
|
1797
|
+
lastAttemptAt: row['last_attempt_at'] as string | undefined,
|
|
1798
|
+
nextRetryAt: row['next_retry_at'] as string | undefined,
|
|
1799
|
+
lastError: row['last_error'] as string | undefined,
|
|
1800
|
+
baseDelay: row['base_delay'] as number,
|
|
1801
|
+
currentDelay: row['current_delay'] as number,
|
|
1802
|
+
maxDelay: row['max_delay'] as number,
|
|
1803
|
+
createdAt: row['created_at'] as string,
|
|
1804
|
+
processedAt: row['processed_at'] as string | undefined,
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
private rowToAuditLog(row: Record<string, unknown>): AuditLog {
|
|
1809
|
+
return {
|
|
1810
|
+
id: row['id'] as string,
|
|
1811
|
+
timestamp: row['timestamp'] as string,
|
|
1812
|
+
eventType: row['event_type'] as AuditLog['eventType'],
|
|
1813
|
+
actor: JSON.parse(row['actor'] as string),
|
|
1814
|
+
resource: JSON.parse(row['resource'] as string),
|
|
1815
|
+
action: row['action'] as string,
|
|
1816
|
+
result: row['result'] as 'success' | 'failure',
|
|
1817
|
+
details: row['details'] ? JSON.parse(row['details'] as string) : undefined,
|
|
1818
|
+
changes: row['changes'] ? JSON.parse(row['changes'] as string) : undefined,
|
|
1819
|
+
errorCode: row['error_code'] as string | undefined,
|
|
1820
|
+
errorMessage: row['error_message'] as string | undefined,
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
private rowToCacheMetadata(row: Record<string, unknown>): CacheMetadata {
|
|
1825
|
+
return {
|
|
1826
|
+
id: row['id'] as string,
|
|
1827
|
+
skillName: row['skill_name'] as string,
|
|
1828
|
+
version: row['version'] as string,
|
|
1829
|
+
cachedAt: row['cached_at'] as string,
|
|
1830
|
+
expiresAt: row['expires_at'] as string | undefined,
|
|
1831
|
+
size: row['size'] as number | undefined,
|
|
1832
|
+
hitCount: row['hit_count'] as number,
|
|
1833
|
+
lastHitAt: row['last_hit_at'] as string | undefined,
|
|
1834
|
+
source: row['source'] as 'remote' | 'local' | undefined,
|
|
1835
|
+
integrityHash: row['integrity_hash'] as string | undefined,
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
}
|