@open-skills-hub/api 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/dist/controllers/audit.d.ts +33 -0
- package/dist/controllers/audit.d.ts.map +1 -0
- package/dist/controllers/audit.js +122 -0
- package/dist/controllers/audit.js.map +1 -0
- package/dist/controllers/cache.d.ts +42 -0
- package/dist/controllers/cache.d.ts.map +1 -0
- package/dist/controllers/cache.js +247 -0
- package/dist/controllers/cache.js.map +1 -0
- package/dist/controllers/feedback.d.ts +44 -0
- package/dist/controllers/feedback.d.ts.map +1 -0
- package/dist/controllers/feedback.js +216 -0
- package/dist/controllers/feedback.js.map +1 -0
- package/dist/controllers/index.d.ts +9 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +9 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/skills.d.ts +66 -0
- package/dist/controllers/skills.d.ts.map +1 -0
- package/dist/controllers/skills.js +355 -0
- package/dist/controllers/skills.js.map +1 -0
- package/dist/controllers/versions.d.ts +43 -0
- package/dist/controllers/versions.d.ts.map +1 -0
- package/dist/controllers/versions.js +298 -0
- package/dist/controllers/versions.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +34 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +148 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error.d.ts +26 -0
- package/dist/middleware/error.d.ts.map +1 -0
- package/dist/middleware/error.js +102 -0
- package/dist/middleware/error.js.map +1 -0
- package/dist/middleware/index.d.ts +8 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +8 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +19 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +54 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/validation.d.ts +671 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +225 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/routes/audit.d.ts +6 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +54 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/cache.d.ts +6 -0
- package/dist/routes/cache.d.ts.map +1 -0
- package/dist/routes/cache.js +70 -0
- package/dist/routes/cache.js.map +1 -0
- package/dist/routes/feedback.d.ts +6 -0
- package/dist/routes/feedback.d.ts.map +1 -0
- package/dist/routes/feedback.js +68 -0
- package/dist/routes/feedback.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +122 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/scan.d.ts +8 -0
- package/dist/routes/scan.d.ts.map +1 -0
- package/dist/routes/scan.js +315 -0
- package/dist/routes/scan.js.map +1 -0
- package/dist/routes/search.d.ts +6 -0
- package/dist/routes/search.d.ts.map +1 -0
- package/dist/routes/search.js +44 -0
- package/dist/routes/search.js.map +1 -0
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +74 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/versions.d.ts +6 -0
- package/dist/routes/versions.d.ts.map +1 -0
- package/dist/routes/versions.js +66 -0
- package/dist/routes/versions.js.map +1 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +166 -0
- package/dist/server.js.map +1 -0
- package/package.json +42 -0
- package/src/controllers/audit.ts +175 -0
- package/src/controllers/cache.ts +344 -0
- package/src/controllers/feedback.ts +309 -0
- package/src/controllers/index.ts +9 -0
- package/src/controllers/skills.ts +489 -0
- package/src/controllers/versions.ts +427 -0
- package/src/index.ts +87 -0
- package/src/middleware/auth.ts +219 -0
- package/src/middleware/error.ts +180 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/logger.ts +71 -0
- package/src/middleware/validation.ts +270 -0
- package/src/routes/audit.ts +74 -0
- package/src/routes/cache.ts +93 -0
- package/src/routes/feedback.ts +93 -0
- package/src/routes/health.ts +151 -0
- package/src/routes/index.ts +12 -0
- package/src/routes/scan.ts +428 -0
- package/src/routes/search.ts +51 -0
- package/src/routes/skills.ts +102 -0
- package/src/routes/versions.ts +91 -0
- package/src/server.ts +205 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open Skills Hub - Skills Controller
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
6
|
+
import {
|
|
7
|
+
getStorage,
|
|
8
|
+
generateUUID,
|
|
9
|
+
now,
|
|
10
|
+
sha256,
|
|
11
|
+
buildSkillFullName,
|
|
12
|
+
parseSkillFullName,
|
|
13
|
+
AppError,
|
|
14
|
+
ErrorCodes,
|
|
15
|
+
} from '@open-skills-hub/core';
|
|
16
|
+
import type { Skill, Version, SkillContent, AuditLog } from '@open-skills-hub/core';
|
|
17
|
+
import { sendSuccess, sendError } from '../middleware/error.js';
|
|
18
|
+
import { logger } from '../middleware/logger.js';
|
|
19
|
+
import type {
|
|
20
|
+
SkillNameParam,
|
|
21
|
+
SkillVersionParam,
|
|
22
|
+
CreateSkillBody,
|
|
23
|
+
UpdateSkillBody,
|
|
24
|
+
PublishVersionBody,
|
|
25
|
+
PaginationQuery,
|
|
26
|
+
SearchQuery,
|
|
27
|
+
} from '../middleware/validation.js';
|
|
28
|
+
// Re-export for use in routes
|
|
29
|
+
export type { SearchQuery };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Search skills
|
|
33
|
+
*/
|
|
34
|
+
export async function searchSkills(
|
|
35
|
+
request: FastifyRequest<{ Querystring: SearchQuery }>,
|
|
36
|
+
reply: FastifyReply
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const storage = await getStorage();
|
|
39
|
+
const { q, category, author, sort, order, cursor, limit } = request.query;
|
|
40
|
+
|
|
41
|
+
const result = await storage.searchSkills({
|
|
42
|
+
query: q,
|
|
43
|
+
category,
|
|
44
|
+
author,
|
|
45
|
+
sort,
|
|
46
|
+
order,
|
|
47
|
+
cursor,
|
|
48
|
+
limit,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
sendSuccess(request, reply, result.items, 200, result.pagination);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get skill by name
|
|
56
|
+
*/
|
|
57
|
+
export async function getSkillByName(
|
|
58
|
+
request: FastifyRequest<{ Params: SkillNameParam }>,
|
|
59
|
+
reply: FastifyReply
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const storage = await getStorage();
|
|
62
|
+
const { name } = request.params;
|
|
63
|
+
|
|
64
|
+
// Parse name (might be scoped like @scope/name)
|
|
65
|
+
const { scope, name: skillName } = parseSkillFullName(name);
|
|
66
|
+
const fullName = buildSkillFullName(skillName, scope);
|
|
67
|
+
|
|
68
|
+
const skill = await storage.getSkillByName(fullName);
|
|
69
|
+
if (!skill) {
|
|
70
|
+
throw new AppError(
|
|
71
|
+
ErrorCodes.SKILL_NOT_FOUND,
|
|
72
|
+
`Skill '${fullName}' not found`,
|
|
73
|
+
404
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get latest version info
|
|
78
|
+
const latestVersion = await storage.getLatestVersion(skill.id);
|
|
79
|
+
|
|
80
|
+
sendSuccess(request, reply, {
|
|
81
|
+
...skill,
|
|
82
|
+
latestVersionDetails: latestVersion ? {
|
|
83
|
+
version: latestVersion.version,
|
|
84
|
+
publishedAt: latestVersion.publishedAt,
|
|
85
|
+
securityLevel: latestVersion.securityLevel,
|
|
86
|
+
} : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get skill content (fetch-on-use)
|
|
92
|
+
*/
|
|
93
|
+
export async function getSkillContent(
|
|
94
|
+
request: FastifyRequest<{ Params: SkillNameParam; Querystring: { version?: string } }>,
|
|
95
|
+
reply: FastifyReply
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const storage = await getStorage();
|
|
98
|
+
const { name } = request.params;
|
|
99
|
+
const { version: requestedVersion } = request.query;
|
|
100
|
+
|
|
101
|
+
// Parse name
|
|
102
|
+
const { scope, name: skillName } = parseSkillFullName(name);
|
|
103
|
+
const fullName = buildSkillFullName(skillName, scope);
|
|
104
|
+
|
|
105
|
+
const skill = await storage.getSkillByName(fullName);
|
|
106
|
+
if (!skill) {
|
|
107
|
+
throw new AppError(
|
|
108
|
+
ErrorCodes.SKILL_NOT_FOUND,
|
|
109
|
+
`Skill '${fullName}' not found`,
|
|
110
|
+
404
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get version
|
|
115
|
+
let version: Version | null;
|
|
116
|
+
if (requestedVersion) {
|
|
117
|
+
version = await storage.getVersion(skill.id, requestedVersion);
|
|
118
|
+
if (!version) {
|
|
119
|
+
throw new AppError(
|
|
120
|
+
ErrorCodes.VERSION_NOT_FOUND,
|
|
121
|
+
`Version '${requestedVersion}' not found for skill '${fullName}'`,
|
|
122
|
+
404
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
version = await storage.getLatestVersion(skill.id);
|
|
127
|
+
if (!version) {
|
|
128
|
+
throw new AppError(
|
|
129
|
+
ErrorCodes.VERSION_NOT_FOUND,
|
|
130
|
+
`No versions found for skill '${fullName}'`,
|
|
131
|
+
404
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Increment use counters
|
|
137
|
+
await Promise.all([
|
|
138
|
+
storage.incrementSkillUses(skill.id),
|
|
139
|
+
storage.incrementVersionUses(version.id),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// Record use
|
|
143
|
+
const useRecord = {
|
|
144
|
+
id: generateUUID(),
|
|
145
|
+
skillId: skill.id,
|
|
146
|
+
versionId: version.id,
|
|
147
|
+
userId: request.user?.id,
|
|
148
|
+
source: 'api' as const,
|
|
149
|
+
cacheHit: false,
|
|
150
|
+
clientVersion: request.headers['x-client-version'] as string | undefined,
|
|
151
|
+
platform: request.headers['x-platform'] as string | undefined,
|
|
152
|
+
arch: request.headers['x-arch'] as string | undefined,
|
|
153
|
+
ip: request.ip,
|
|
154
|
+
userAgent: request.headers['user-agent'],
|
|
155
|
+
createdAt: now(),
|
|
156
|
+
};
|
|
157
|
+
await storage.createUseRecord(useRecord);
|
|
158
|
+
|
|
159
|
+
sendSuccess(request, reply, {
|
|
160
|
+
skill: {
|
|
161
|
+
name: skill.fullName,
|
|
162
|
+
displayName: skill.displayName,
|
|
163
|
+
description: skill.description,
|
|
164
|
+
securityLevel: skill.securityLevel,
|
|
165
|
+
},
|
|
166
|
+
version: version.version,
|
|
167
|
+
content: version.content,
|
|
168
|
+
packageHash: version.packageHash,
|
|
169
|
+
securityScore: version.securityScore,
|
|
170
|
+
securityLevel: version.securityLevel,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create/publish a new skill
|
|
176
|
+
*/
|
|
177
|
+
export async function createSkill(
|
|
178
|
+
request: FastifyRequest<{ Body: CreateSkillBody }>,
|
|
179
|
+
reply: FastifyReply
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const storage = await getStorage();
|
|
182
|
+
const body = request.body;
|
|
183
|
+
const userId = request.user?.id;
|
|
184
|
+
|
|
185
|
+
// Build full name
|
|
186
|
+
const fullName = buildSkillFullName(body.name, body.scope);
|
|
187
|
+
|
|
188
|
+
// Check if skill already exists
|
|
189
|
+
const existing = await storage.getSkillByName(fullName);
|
|
190
|
+
if (existing) {
|
|
191
|
+
throw new AppError(
|
|
192
|
+
ErrorCodes.SKILL_EXISTS,
|
|
193
|
+
`Skill '${fullName}' already exists`,
|
|
194
|
+
409
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Calculate content hash
|
|
199
|
+
const contentString = JSON.stringify(body.content);
|
|
200
|
+
const contentHash = sha256(contentString);
|
|
201
|
+
|
|
202
|
+
const skillId = generateUUID();
|
|
203
|
+
const versionId = generateUUID();
|
|
204
|
+
const timestamp = now();
|
|
205
|
+
|
|
206
|
+
// Extract author from frontmatter or use userId
|
|
207
|
+
const author = body.content.frontmatter.author || userId || 'anonymous';
|
|
208
|
+
const ownerType = (body.scope || author.startsWith('@')) ? 'organization' : 'user';
|
|
209
|
+
|
|
210
|
+
// Create skill
|
|
211
|
+
const skill: Skill = {
|
|
212
|
+
id: skillId,
|
|
213
|
+
name: body.name,
|
|
214
|
+
scope: body.scope,
|
|
215
|
+
fullName,
|
|
216
|
+
ownerId: author,
|
|
217
|
+
ownerType,
|
|
218
|
+
displayName: body.displayName,
|
|
219
|
+
description: body.description,
|
|
220
|
+
category: body.category,
|
|
221
|
+
keywords: body.keywords,
|
|
222
|
+
license: body.license,
|
|
223
|
+
repository: body.repository,
|
|
224
|
+
homepage: body.homepage,
|
|
225
|
+
latestVersion: body.version,
|
|
226
|
+
latestVersionId: versionId,
|
|
227
|
+
visibility: body.visibility,
|
|
228
|
+
status: 'active',
|
|
229
|
+
stats: { totalUses: 0, weeklyUses: 0, monthlyUses: 0, versionCount: 1, derivationCount: 0 },
|
|
230
|
+
rating: { average: 0, count: 0 },
|
|
231
|
+
createdAt: timestamp,
|
|
232
|
+
updatedAt: timestamp,
|
|
233
|
+
publishedAt: timestamp,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Create version
|
|
237
|
+
const version: Version = {
|
|
238
|
+
id: versionId,
|
|
239
|
+
skillId,
|
|
240
|
+
version: body.version,
|
|
241
|
+
tag: 'latest',
|
|
242
|
+
content: body.content,
|
|
243
|
+
packageUrl: `local://${fullName}/${body.version}`,
|
|
244
|
+
packageSize: contentString.length,
|
|
245
|
+
packageHash: contentHash,
|
|
246
|
+
changelog: body.changelog,
|
|
247
|
+
status: 'published',
|
|
248
|
+
publishedBy: author,
|
|
249
|
+
uses: 0,
|
|
250
|
+
createdAt: timestamp,
|
|
251
|
+
publishedAt: timestamp,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Save to storage
|
|
255
|
+
await storage.createSkill(skill);
|
|
256
|
+
await storage.createVersion(version);
|
|
257
|
+
|
|
258
|
+
// Create audit log
|
|
259
|
+
const auditLog: AuditLog = {
|
|
260
|
+
id: generateUUID(),
|
|
261
|
+
timestamp,
|
|
262
|
+
eventType: 'skill.published',
|
|
263
|
+
actor: {
|
|
264
|
+
type: request.user?.type ?? 'user',
|
|
265
|
+
id: userId,
|
|
266
|
+
username: request.user?.username,
|
|
267
|
+
ip: request.ip,
|
|
268
|
+
userAgent: request.headers['user-agent'],
|
|
269
|
+
},
|
|
270
|
+
resource: {
|
|
271
|
+
type: 'skill',
|
|
272
|
+
id: skillId,
|
|
273
|
+
name: fullName,
|
|
274
|
+
version: body.version,
|
|
275
|
+
},
|
|
276
|
+
action: 'create',
|
|
277
|
+
result: 'success',
|
|
278
|
+
details: {
|
|
279
|
+
visibility: body.visibility,
|
|
280
|
+
category: body.category,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
await storage.createAuditLog(auditLog);
|
|
284
|
+
|
|
285
|
+
logger.info('Skill published', { skillId, fullName, version: body.version });
|
|
286
|
+
|
|
287
|
+
sendSuccess(request, reply, { skill, version }, 201);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Update skill metadata
|
|
292
|
+
*/
|
|
293
|
+
export async function updateSkill(
|
|
294
|
+
request: FastifyRequest<{ Params: SkillNameParam; Body: UpdateSkillBody }>,
|
|
295
|
+
reply: FastifyReply
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const storage = await getStorage();
|
|
298
|
+
const { name } = request.params;
|
|
299
|
+
const updates = request.body;
|
|
300
|
+
const userId = request.user?.id;
|
|
301
|
+
|
|
302
|
+
// Parse name
|
|
303
|
+
const { scope, name: skillName } = parseSkillFullName(name);
|
|
304
|
+
const fullName = buildSkillFullName(skillName, scope);
|
|
305
|
+
|
|
306
|
+
const skill = await storage.getSkillByName(fullName);
|
|
307
|
+
if (!skill) {
|
|
308
|
+
throw new AppError(
|
|
309
|
+
ErrorCodes.SKILL_NOT_FOUND,
|
|
310
|
+
`Skill '${fullName}' not found`,
|
|
311
|
+
404
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check ownership (simplified - in production, check more thoroughly)
|
|
316
|
+
if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
|
|
317
|
+
throw new AppError(
|
|
318
|
+
ErrorCodes.FORBIDDEN,
|
|
319
|
+
'You do not have permission to update this skill',
|
|
320
|
+
403
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Build update object (remove null values to unset optional fields)
|
|
325
|
+
const updateData: Partial<Skill> = {};
|
|
326
|
+
if (updates.displayName !== undefined) updateData.displayName = updates.displayName;
|
|
327
|
+
if (updates.description !== undefined) updateData.description = updates.description;
|
|
328
|
+
if (updates.category !== undefined) updateData.category = updates.category;
|
|
329
|
+
if (updates.keywords !== undefined) updateData.keywords = updates.keywords;
|
|
330
|
+
if (updates.repository !== undefined) updateData.repository = updates.repository ?? undefined;
|
|
331
|
+
if (updates.homepage !== undefined) updateData.homepage = updates.homepage ?? undefined;
|
|
332
|
+
if (updates.visibility !== undefined) updateData.visibility = updates.visibility;
|
|
333
|
+
|
|
334
|
+
const updated = await storage.updateSkill(skill.id, updateData);
|
|
335
|
+
|
|
336
|
+
if (!updated) {
|
|
337
|
+
throw new AppError(
|
|
338
|
+
ErrorCodes.INTERNAL_ERROR,
|
|
339
|
+
'Failed to update skill',
|
|
340
|
+
500
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Create audit log
|
|
345
|
+
const auditLog: AuditLog = {
|
|
346
|
+
id: generateUUID(),
|
|
347
|
+
timestamp: now(),
|
|
348
|
+
eventType: 'skill.updated',
|
|
349
|
+
actor: {
|
|
350
|
+
type: request.user?.type ?? 'user',
|
|
351
|
+
id: userId,
|
|
352
|
+
username: request.user?.username,
|
|
353
|
+
ip: request.ip,
|
|
354
|
+
userAgent: request.headers['user-agent'],
|
|
355
|
+
},
|
|
356
|
+
resource: {
|
|
357
|
+
type: 'skill',
|
|
358
|
+
id: skill.id,
|
|
359
|
+
name: fullName,
|
|
360
|
+
},
|
|
361
|
+
action: 'update',
|
|
362
|
+
result: 'success',
|
|
363
|
+
changes: {
|
|
364
|
+
before: skill,
|
|
365
|
+
after: updated,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
await storage.createAuditLog(auditLog);
|
|
369
|
+
|
|
370
|
+
logger.info('Skill updated', { skillId: skill.id, fullName });
|
|
371
|
+
|
|
372
|
+
sendSuccess(request, reply, updated);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Delete/deprecate a skill
|
|
377
|
+
*/
|
|
378
|
+
export async function deleteSkill(
|
|
379
|
+
request: FastifyRequest<{ Params: SkillNameParam; Querystring: { hard?: string } }>,
|
|
380
|
+
reply: FastifyReply
|
|
381
|
+
): Promise<void> {
|
|
382
|
+
const storage = await getStorage();
|
|
383
|
+
const { name } = request.params;
|
|
384
|
+
const { hard } = request.query;
|
|
385
|
+
const userId = request.user?.id;
|
|
386
|
+
|
|
387
|
+
// Parse name
|
|
388
|
+
const { scope, name: skillName } = parseSkillFullName(name);
|
|
389
|
+
const fullName = buildSkillFullName(skillName, scope);
|
|
390
|
+
|
|
391
|
+
const skill = await storage.getSkillByName(fullName);
|
|
392
|
+
if (!skill) {
|
|
393
|
+
throw new AppError(
|
|
394
|
+
ErrorCodes.SKILL_NOT_FOUND,
|
|
395
|
+
`Skill '${fullName}' not found`,
|
|
396
|
+
404
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Check ownership
|
|
401
|
+
if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
|
|
402
|
+
throw new AppError(
|
|
403
|
+
ErrorCodes.FORBIDDEN,
|
|
404
|
+
'You do not have permission to delete this skill',
|
|
405
|
+
403
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let result: boolean;
|
|
410
|
+
if (hard === 'true') {
|
|
411
|
+
// Hard delete
|
|
412
|
+
result = await storage.deleteSkill(skill.id);
|
|
413
|
+
} else {
|
|
414
|
+
// Soft delete (mark as deleted)
|
|
415
|
+
const updated = await storage.updateSkill(skill.id, { status: 'deleted' });
|
|
416
|
+
result = !!updated;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Create audit log
|
|
420
|
+
const auditLog: AuditLog = {
|
|
421
|
+
id: generateUUID(),
|
|
422
|
+
timestamp: now(),
|
|
423
|
+
eventType: 'skill.deleted',
|
|
424
|
+
actor: {
|
|
425
|
+
type: request.user?.type ?? 'user',
|
|
426
|
+
id: userId,
|
|
427
|
+
username: request.user?.username,
|
|
428
|
+
ip: request.ip,
|
|
429
|
+
userAgent: request.headers['user-agent'],
|
|
430
|
+
},
|
|
431
|
+
resource: {
|
|
432
|
+
type: 'skill',
|
|
433
|
+
id: skill.id,
|
|
434
|
+
name: fullName,
|
|
435
|
+
},
|
|
436
|
+
action: hard === 'true' ? 'hard_delete' : 'soft_delete',
|
|
437
|
+
result: result ? 'success' : 'failure',
|
|
438
|
+
};
|
|
439
|
+
await storage.createAuditLog(auditLog);
|
|
440
|
+
|
|
441
|
+
logger.info('Skill deleted', { skillId: skill.id, fullName, hard: hard === 'true' });
|
|
442
|
+
|
|
443
|
+
sendSuccess(request, reply, { deleted: result });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get skills by owner
|
|
448
|
+
*/
|
|
449
|
+
export async function getSkillsByOwner(
|
|
450
|
+
request: FastifyRequest<{ Params: { ownerId: string }; Querystring: PaginationQuery }>,
|
|
451
|
+
reply: FastifyReply
|
|
452
|
+
): Promise<void> {
|
|
453
|
+
const storage = await getStorage();
|
|
454
|
+
const { ownerId } = request.params;
|
|
455
|
+
const { cursor, limit } = request.query;
|
|
456
|
+
|
|
457
|
+
const result = await storage.getSkillsByOwner(ownerId, { cursor, limit });
|
|
458
|
+
|
|
459
|
+
sendSuccess(request, reply, result.items, 200, result.pagination);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get derived skills
|
|
464
|
+
*/
|
|
465
|
+
export async function getDerivedSkills(
|
|
466
|
+
request: FastifyRequest<{ Params: SkillNameParam; Querystring: PaginationQuery }>,
|
|
467
|
+
reply: FastifyReply
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
const storage = await getStorage();
|
|
470
|
+
const { name } = request.params;
|
|
471
|
+
const { cursor, limit } = request.query;
|
|
472
|
+
|
|
473
|
+
// Parse name
|
|
474
|
+
const { scope, name: skillName } = parseSkillFullName(name);
|
|
475
|
+
const fullName = buildSkillFullName(skillName, scope);
|
|
476
|
+
|
|
477
|
+
const skill = await storage.getSkillByName(fullName);
|
|
478
|
+
if (!skill) {
|
|
479
|
+
throw new AppError(
|
|
480
|
+
ErrorCodes.SKILL_NOT_FOUND,
|
|
481
|
+
`Skill '${fullName}' not found`,
|
|
482
|
+
404
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const result = await storage.getDerivedSkills(skill.id, { cursor, limit });
|
|
487
|
+
|
|
488
|
+
sendSuccess(request, reply, result.items, 200, result.pagination);
|
|
489
|
+
}
|