path-handlers.mjs
150 lines 4.2 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * Shared learning-path list/get handlers — Hub REST (KN-WORK-PATH-LIST-b).
3 *
4 * @see docs/KN-WORK-PATH-LIST-FREEZE.md §4
5 */
6
7 import {
8 listLearningPaths,
9 getLearningPath,
10 learningPathForClient,
11 pathGetEffectiveScope,
12 PATH_ID_RE,
13 WORKSPACE_ID_RE,
14 PATH_STATUSES,
15 MAX_LEARNING_PATH_SUMMARIES,
16 LEARNING_PATH_GET_SCHEMA,
17 } from './path-store.mjs';
18 import { resolveFlowScopeQuery } from '../flow/flow-scope.mjs';
19 import { resolveHandlerVisibleScopes } from '../flow/flow-handlers.mjs';
20
21 /**
22 * @typedef {import('../flow/flow-scope.mjs').FlowScope} FlowScope
23 */
24
25 /**
26 * Parse list limit: integer 1–200; invalid or absent → 200.
27 *
28 * @param {unknown} raw
29 * @returns {number}
30 */
31 export function parsePathListLimit(raw) {
32 if (raw === undefined || raw === null || raw === '') return MAX_LEARNING_PATH_SUMMARIES;
33 const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10);
34 if (!Number.isInteger(n) || n < 1 || n > MAX_LEARNING_PATH_SUMMARIES) {
35 return MAX_LEARNING_PATH_SUMMARIES;
36 }
37 return n;
38 }
39
40 /**
41 * @param {{
42 * dataDir: string,
43 * vaultId: string,
44 * userId?: string,
45 * role?: string,
46 * cliScopes?: FlowScope[],
47 * visibleScopes?: Set<FlowScope>,
48 * ambiguous?: boolean,
49 * scope?: string,
50 * workspace_id?: string,
51 * workspaceId?: string,
52 * status?: string,
53 * limit?: number|string,
54 * }} input
55 */
56 export function handlePathListRequest(input) {
57 const resolved = resolveHandlerVisibleScopes(input);
58 if (resolved.ambiguous) {
59 return {
60 ok: false,
61 status: 400,
62 error: 'Ambiguous path scope',
63 code: 'PATH_SCOPE_AMBIGUOUS',
64 };
65 }
66
67 const scopeQuery = resolveFlowScopeQuery(resolved.visibleScopes, input.scope);
68 if (!scopeQuery.ok) {
69 const code =
70 scopeQuery.code === 'FLOW_SCOPE_DENIED' ? 'PATH_SCOPE_DENIED' : scopeQuery.code;
71 const error =
72 scopeQuery.code === 'FLOW_SCOPE_DENIED' ? 'Path scope not authorized' : scopeQuery.error;
73 return { ok: false, status: scopeQuery.status, error, code };
74 }
75
76 const status = typeof input.status === 'string' ? input.status.trim() : '';
77 if (status && !PATH_STATUSES.includes(/** @type {typeof PATH_STATUSES[number]} */ (status))) {
78 return { ok: false, status: 400, error: 'Invalid status', code: 'BAD_REQUEST' };
79 }
80
81 const workspaceId =
82 typeof input.workspaceId === 'string'
83 ? input.workspaceId
84 : typeof input.workspace_id === 'string'
85 ? input.workspace_id
86 : undefined;
87 if (workspaceId != null && String(workspaceId).trim() !== '') {
88 const ws = String(workspaceId).trim();
89 if (!WORKSPACE_ID_RE.test(ws)) {
90 return { ok: false, status: 400, error: 'Invalid workspace_id', code: 'BAD_REQUEST' };
91 }
92 }
93
94 const payload = listLearningPaths(input.dataDir, input.vaultId, {
95 visibleScopes: resolved.visibleScopes,
96 filterScopes: scopeQuery.filterScopes,
97 effectiveScope: pathGetEffectiveScope(resolved.visibleScopes),
98 workspaceId: workspaceId && String(workspaceId).trim() ? String(workspaceId).trim() : undefined,
99 status: status || undefined,
100 limit: parsePathListLimit(input.limit),
101 });
102
103 return { ok: true, payload };
104 }
105
106 /**
107 * @param {{
108 * dataDir: string,
109 * vaultId: string,
110 * pathId: string,
111 * userId?: string,
112 * role?: string,
113 * cliScopes?: FlowScope[],
114 * visibleScopes?: Set<FlowScope>,
115 * ambiguous?: boolean,
116 * }} input
117 */
118 export function handlePathGetRequest(input) {
119 const resolved = resolveHandlerVisibleScopes(input);
120 if (resolved.ambiguous) {
121 return {
122 ok: false,
123 status: 400,
124 error: 'Ambiguous path scope',
125 code: 'PATH_SCOPE_AMBIGUOUS',
126 };
127 }
128
129 const pathId = typeof input.pathId === 'string' ? input.pathId.trim() : '';
130 if (!PATH_ID_RE.test(pathId)) {
131 return { ok: false, status: 404, error: 'PATH_NOT_FOUND', code: 'PATH_NOT_FOUND' };
132 }
133
134 const row = getLearningPath(input.dataDir, input.vaultId, pathId, {
135 visibleScopes: resolved.visibleScopes,
136 });
137 if (!row) {
138 return { ok: false, status: 404, error: 'PATH_NOT_FOUND', code: 'PATH_NOT_FOUND' };
139 }
140
141 return {
142 ok: true,
143 payload: {
144 schema: LEARNING_PATH_GET_SCHEMA,
145 vault_id: input.vaultId,
146 effective_scope: pathGetEffectiveScope(resolved.visibleScopes),
147 path: learningPathForClient(row),
148 },
149 };
150 }
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago