wiki-formant 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/entities.d.ts +3 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +30 -0
- package/dist/entities.js.map +1 -0
- package/dist/http.d.ts +25 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +72 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +25 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +107 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.d.ts +123 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +317 -0
- package/dist/mcp.js.map +1 -0
- package/dist/pagination.d.ts +22 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +26 -0
- package/dist/pagination.js.map +1 -0
- package/dist/taxonomy.d.ts +74 -0
- package/dist/taxonomy.d.ts.map +1 -0
- package/dist/taxonomy.js +156 -0
- package/dist/taxonomy.js.map +1 -0
- package/dist/versioning.d.ts +14 -0
- package/dist/versioning.d.ts.map +1 -0
- package/dist/versioning.js +41 -0
- package/dist/versioning.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// mcp.ts — a minimal Model Context Protocol server over Streamable HTTP
|
|
2
|
+
// (JSON-RPC). Spec: https://modelcontextprotocol.io/specification/2025-03-26
|
|
3
|
+
//
|
|
4
|
+
// Web-standard `Request`/`Response` only, so this runs unchanged on Next route
|
|
5
|
+
// handlers (NextResponse extends Response), Hono, Bun, Deno and workers.
|
|
6
|
+
//
|
|
7
|
+
// Two things exist here purely so a caller that gets it wrong gets it right on
|
|
8
|
+
// the retry: `inputSchema` is a shape this module actually validates against —
|
|
9
|
+
// naming the bad field and enumerating the legal values rather than coercing
|
|
10
|
+
// junk and returning an empty result — and `instructions` rides on `initialize`
|
|
11
|
+
// so an agent learns the intended call sequence before it has to guess.
|
|
12
|
+
//
|
|
13
|
+
// A caller-fixable mistake is a tool result with `isError`, never a -32603.
|
|
14
|
+
// That split is the one most implementations get wrong.
|
|
15
|
+
export const MCP_PROTOCOL_VERSION = '2025-03-26';
|
|
16
|
+
/**
|
|
17
|
+
* A caller-fixable failure inside a handler (page not found, empty input).
|
|
18
|
+
* Reported as a tool result with `isError` so the model sees the text and can
|
|
19
|
+
* correct itself, per the spec's split between protocol and execution errors.
|
|
20
|
+
*/
|
|
21
|
+
export class McpToolError extends Error {
|
|
22
|
+
details;
|
|
23
|
+
constructor(message, details) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.details = details;
|
|
26
|
+
this.name = 'McpToolError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const DEFAULT_MAX_BATCH = 20;
|
|
30
|
+
const quote = (list) => list.map(s => `"${s}"`).join(', ');
|
|
31
|
+
function typeOf(v) {
|
|
32
|
+
if (Array.isArray(v))
|
|
33
|
+
return 'array';
|
|
34
|
+
if (v === null)
|
|
35
|
+
return 'null';
|
|
36
|
+
return typeof v;
|
|
37
|
+
}
|
|
38
|
+
const rpcError = (id, code, message, data) => ({
|
|
39
|
+
jsonrpc: '2.0',
|
|
40
|
+
id,
|
|
41
|
+
error: { code, message, ...(data ? { data } : {}) },
|
|
42
|
+
});
|
|
43
|
+
const toolText = (id, text, isError = false) => ({
|
|
44
|
+
jsonrpc: '2.0',
|
|
45
|
+
id,
|
|
46
|
+
result: { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) },
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Every problem with the call at once, each naming the field and its legal
|
|
50
|
+
* values, plus the schema — one retry should be able to fix all of them.
|
|
51
|
+
*/
|
|
52
|
+
function validateArgs(tool, args) {
|
|
53
|
+
const { properties, required = [] } = tool.inputSchema;
|
|
54
|
+
const allowed = Object.keys(properties);
|
|
55
|
+
const problems = [];
|
|
56
|
+
for (const key of Object.keys(args)) {
|
|
57
|
+
if (key in properties)
|
|
58
|
+
continue;
|
|
59
|
+
problems.push(allowed.length
|
|
60
|
+
? `Unknown parameter "${key}". Allowed parameters: ${quote(allowed)}.`
|
|
61
|
+
: `Unknown parameter "${key}". This tool takes no parameters.`);
|
|
62
|
+
}
|
|
63
|
+
for (const key of required) {
|
|
64
|
+
if (args[key] === undefined || args[key] === null) {
|
|
65
|
+
problems.push(`Missing required parameter "${key}". Required: ${quote(required)}.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const [key, spec] of Object.entries(properties)) {
|
|
69
|
+
const value = args[key];
|
|
70
|
+
if (value === undefined || value === null)
|
|
71
|
+
continue;
|
|
72
|
+
const actual = typeOf(value);
|
|
73
|
+
if (spec.type === 'number') {
|
|
74
|
+
// A numeric string is accepted — some clients stringify everything.
|
|
75
|
+
const numeric = actual === 'number' ||
|
|
76
|
+
(actual === 'string' && String(value).trim() !== '' && Number.isFinite(Number(value)));
|
|
77
|
+
if (!numeric) {
|
|
78
|
+
problems.push(`Parameter "${key}" must be a number; received ${actual} (${JSON.stringify(value)}).`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (spec.type === 'array') {
|
|
82
|
+
const itemType = spec.items?.type ?? 'string';
|
|
83
|
+
if (actual !== 'array') {
|
|
84
|
+
problems.push(`Parameter "${key}" must be an array of ${itemType}s; received ${actual} (${JSON.stringify(value)}).`);
|
|
85
|
+
}
|
|
86
|
+
else if (value.some(item => typeof item !== itemType)) {
|
|
87
|
+
problems.push(`Parameter "${key}" must contain only ${itemType}s.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (actual !== spec.type) {
|
|
91
|
+
problems.push(`Parameter "${key}" must be a ${spec.type}; received ${actual} (${JSON.stringify(value)}).`);
|
|
92
|
+
}
|
|
93
|
+
else if (spec.enum && !spec.enum.includes(value)) {
|
|
94
|
+
problems.push(`Parameter "${key}" must be one of ${quote(spec.enum)}; received ${JSON.stringify(value)}.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!problems.length)
|
|
98
|
+
return null;
|
|
99
|
+
return [
|
|
100
|
+
`Invalid arguments for ${tool.name}:`,
|
|
101
|
+
...problems.map(p => `- ${p}`),
|
|
102
|
+
'',
|
|
103
|
+
`Expected schema: ${JSON.stringify(tool.inputSchema)}`,
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
const BASE_METHODS = ['initialize', 'ping', 'tools/list', 'tools/call'];
|
|
107
|
+
function methodsFor(config) {
|
|
108
|
+
return [
|
|
109
|
+
...BASE_METHODS,
|
|
110
|
+
...(config.resources?.length ? ['resources/list', 'resources/read'] : []),
|
|
111
|
+
...(config.prompts?.length ? ['prompts/list', 'prompts/get'] : []),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
async function handleRpc(req, config) {
|
|
115
|
+
const { id, method, params } = req;
|
|
116
|
+
const p = (params ?? {});
|
|
117
|
+
const resources = config.resources ?? [];
|
|
118
|
+
const prompts = config.prompts ?? [];
|
|
119
|
+
try {
|
|
120
|
+
switch (method) {
|
|
121
|
+
case 'initialize':
|
|
122
|
+
return {
|
|
123
|
+
jsonrpc: '2.0',
|
|
124
|
+
id,
|
|
125
|
+
result: {
|
|
126
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
127
|
+
// Only advertise capabilities the config actually populates — an
|
|
128
|
+
// advertised `resources` whose list comes back empty reads as a bug
|
|
129
|
+
// to a client, not as honesty.
|
|
130
|
+
capabilities: {
|
|
131
|
+
tools: {},
|
|
132
|
+
...(resources.length ? { resources: {} } : {}),
|
|
133
|
+
...(prompts.length ? { prompts: {} } : {}),
|
|
134
|
+
},
|
|
135
|
+
serverInfo: config.serverInfo,
|
|
136
|
+
instructions: config.instructions,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
// Notifications have no response at all.
|
|
140
|
+
case 'notifications/initialized':
|
|
141
|
+
case 'notifications/cancelled':
|
|
142
|
+
return null;
|
|
143
|
+
case 'ping':
|
|
144
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
145
|
+
case 'tools/list':
|
|
146
|
+
return {
|
|
147
|
+
jsonrpc: '2.0',
|
|
148
|
+
id,
|
|
149
|
+
result: {
|
|
150
|
+
tools: config.tools.map(({ name, description, inputSchema, annotations }) => ({
|
|
151
|
+
name,
|
|
152
|
+
description,
|
|
153
|
+
inputSchema,
|
|
154
|
+
...(annotations ? { annotations } : {}),
|
|
155
|
+
})),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
case 'resources/list':
|
|
159
|
+
return {
|
|
160
|
+
jsonrpc: '2.0',
|
|
161
|
+
id,
|
|
162
|
+
result: {
|
|
163
|
+
resources: resources.map(({ uri, name, description, mimeType }) => ({
|
|
164
|
+
uri,
|
|
165
|
+
name,
|
|
166
|
+
description,
|
|
167
|
+
mimeType,
|
|
168
|
+
})),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
case 'resources/read': {
|
|
172
|
+
const { uri } = p;
|
|
173
|
+
const resource = uri ? resources.find(r => r.uri === uri) : undefined;
|
|
174
|
+
const content = resource ? await resource.read() : null;
|
|
175
|
+
if (content == null) {
|
|
176
|
+
const uris = resources.map(r => r.uri);
|
|
177
|
+
const lead = uri ? `Unknown resource "${uri}".` : 'Missing "uri" in params.';
|
|
178
|
+
return rpcError(id, -32602, `${lead} This server exposes: ${uris.length ? quote(uris) : 'no resources'}.`, { availableResources: uris });
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
id,
|
|
183
|
+
result: { contents: [{ uri, mimeType: resource.mimeType, text: content }] },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
case 'prompts/list':
|
|
187
|
+
return {
|
|
188
|
+
jsonrpc: '2.0',
|
|
189
|
+
id,
|
|
190
|
+
result: { prompts: prompts.map(({ name, description }) => ({ name, description })) },
|
|
191
|
+
};
|
|
192
|
+
case 'prompts/get': {
|
|
193
|
+
const { name } = p;
|
|
194
|
+
const prompt = name ? prompts.find(x => x.name === name) : undefined;
|
|
195
|
+
if (!prompt) {
|
|
196
|
+
const names = prompts.map(x => x.name);
|
|
197
|
+
const lead = name ? `Unknown prompt "${name}".` : 'Missing "name" in params.';
|
|
198
|
+
return rpcError(id, -32602, `${lead} This server exposes: ${names.length ? quote(names) : 'no prompts'}.`, { availablePrompts: names });
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
jsonrpc: '2.0',
|
|
202
|
+
id,
|
|
203
|
+
result: {
|
|
204
|
+
description: prompt.description,
|
|
205
|
+
messages: [{ role: 'user', content: { type: 'text', text: prompt.text } }],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
case 'tools/call': {
|
|
210
|
+
const { name, arguments: rawArgs = {} } = p;
|
|
211
|
+
const names = config.tools.map(t => t.name);
|
|
212
|
+
const tool = name ? config.tools.find(t => t.name === name) : undefined;
|
|
213
|
+
if (!tool) {
|
|
214
|
+
const lead = name ? `Unknown tool "${name}".` : 'Missing "name" in params.';
|
|
215
|
+
return rpcError(id, -32602, `${lead} This server exposes: ${quote(names)}.`, {
|
|
216
|
+
availableTools: names,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (typeOf(rawArgs) !== 'object') {
|
|
220
|
+
return toolText(id, `"arguments" must be an object; received ${typeOf(rawArgs)}. Expected schema: ${JSON.stringify(tool.inputSchema)}`, true);
|
|
221
|
+
}
|
|
222
|
+
const args = rawArgs;
|
|
223
|
+
const invalid = validateArgs(tool, args);
|
|
224
|
+
if (invalid)
|
|
225
|
+
return toolText(id, invalid, true);
|
|
226
|
+
try {
|
|
227
|
+
const data = await tool.handler(args);
|
|
228
|
+
return toolText(id, typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
if (!(err instanceof McpToolError))
|
|
232
|
+
throw err;
|
|
233
|
+
return toolText(id, JSON.stringify({ error: err.message, ...err.details }, null, 2), true);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
default: {
|
|
237
|
+
const methods = methodsFor(config);
|
|
238
|
+
return rpcError(id, -32601, `Method not found: "${method}". This server implements: ${quote(methods)}.`, { supportedMethods: methods });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
console.error('[MCP]', method, err);
|
|
244
|
+
return rpcError(id, -32603, 'Internal error');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|
|
248
|
+
export async function handleMcp(body, config) {
|
|
249
|
+
const maxBatch = config.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
250
|
+
const isBatch = Array.isArray(body);
|
|
251
|
+
if (isBatch && body.length > maxBatch) {
|
|
252
|
+
return rpcError(null, -32600, `Batch too large: ${body.length} requests (max ${maxBatch}). Split into smaller batches.`);
|
|
253
|
+
}
|
|
254
|
+
if (isBatch && body.length === 0) {
|
|
255
|
+
return rpcError(null, -32600, 'Batch is empty. Send at least one JSON-RPC request.');
|
|
256
|
+
}
|
|
257
|
+
const responses = (await Promise.all((isBatch ? body : [body]).map(r => handleRpc(r, config)))).filter(Boolean);
|
|
258
|
+
return isBatch ? responses : (responses[0] ?? null);
|
|
259
|
+
}
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// HTTP transport — the spec edges, in one place.
|
|
262
|
+
//
|
|
263
|
+
// A public, anonymous, read-only server means `*` is the correct CORS posture:
|
|
264
|
+
// the spec's Origin-validation MUST exists to protect localhost servers from
|
|
265
|
+
// DNS rebinding, which is the opposite situation. The allow-headers list
|
|
266
|
+
// matters more than it looks — MCP clients preflight with `Accept` and
|
|
267
|
+
// `Mcp-Protocol-Version`, and one missing entry fails the preflight, not the
|
|
268
|
+
// POST, which reads as "the server is down".
|
|
269
|
+
export const MCP_CORS = {
|
|
270
|
+
'Access-Control-Allow-Origin': '*',
|
|
271
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
272
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID',
|
|
273
|
+
'Access-Control-Max-Age': '86400',
|
|
274
|
+
};
|
|
275
|
+
export function withMcpCors(res) {
|
|
276
|
+
for (const [k, v] of Object.entries(MCP_CORS))
|
|
277
|
+
res.headers.set(k, v);
|
|
278
|
+
return res;
|
|
279
|
+
}
|
|
280
|
+
export function mcpOptions() {
|
|
281
|
+
return new Response(null, { status: 204, headers: MCP_CORS });
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Streamable HTTP lets a server decline the SSE leg by answering GET with 405.
|
|
285
|
+
* A framework's automatic 405 carries no CORS headers, so a browser client
|
|
286
|
+
* could not even read the refusal — answer it here, and say what to do instead.
|
|
287
|
+
*/
|
|
288
|
+
export function mcpGet(docsUrl) {
|
|
289
|
+
const message = 'This server speaks JSON-RPC over POST only (no SSE stream). Send your request as an HTTP POST.' +
|
|
290
|
+
(docsUrl ? ` Docs: ${docsUrl}` : '');
|
|
291
|
+
return Response.json(rpcError(null, -32600, message), {
|
|
292
|
+
status: 405,
|
|
293
|
+
headers: { ...MCP_CORS, Allow: 'POST, OPTIONS' },
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/** The whole POST leg: parse, track, dispatch, and answer with the right status. */
|
|
297
|
+
export async function mcpResponse(request, config) {
|
|
298
|
+
let body;
|
|
299
|
+
try {
|
|
300
|
+
body = await request.json();
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return Response.json(rpcError(null, -32700, 'Parse error: request body is not valid JSON.'), {
|
|
304
|
+
status: 400,
|
|
305
|
+
headers: MCP_CORS,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
config.onCall?.(request, body);
|
|
309
|
+
const result = await handleMcp(body, config);
|
|
310
|
+
// Notification-only input produces no response bodies; the spec requires a
|
|
311
|
+
// bare 202 there, not a 200 carrying a JSON `null`.
|
|
312
|
+
if (result == null || (Array.isArray(result) && !result.length)) {
|
|
313
|
+
return new Response(null, { status: 202, headers: MCP_CORS });
|
|
314
|
+
}
|
|
315
|
+
return Response.json(result, { headers: MCP_CORS });
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Pagination {
|
|
2
|
+
page: number;
|
|
3
|
+
pageSize: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PaginatedResponse<T> extends Pagination {
|
|
6
|
+
items: T[];
|
|
7
|
+
total: number;
|
|
8
|
+
totalPages: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const MAX_PAGE_SIZE = 100;
|
|
11
|
+
export declare const DEFAULT_PAGE_SIZE = 20;
|
|
12
|
+
/** `page` clamped to ≥1, `pageSize` clamped to 1–100. Never trust either. */
|
|
13
|
+
export declare function parsePagination(searchParams: URLSearchParams, defaults?: {
|
|
14
|
+
pageSize?: number;
|
|
15
|
+
}): Pagination;
|
|
16
|
+
export declare function paginatedResponse<T>(items: T[], total: number, page: number, pageSize: number): PaginatedResponse<T>;
|
|
17
|
+
/** The `skip`/`take` an ORM wants, from the same clamped pair. */
|
|
18
|
+
export declare function toOffset({ page, pageSize }: Pagination): {
|
|
19
|
+
skip: number;
|
|
20
|
+
take: number;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,CAAE,SAAQ,UAAU;IACtD,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,6EAA6E;AAC7E,wBAAgB,eAAe,CAC7B,YAAY,EAAE,eAAe,EAC7B,QAAQ,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,UAAU,CAYZ;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,iBAAiB,CAAC,CAAC,CAAC,CAEtB;AAED,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// pagination.ts — one offset-pagination shape, everywhere.
|
|
2
|
+
//
|
|
3
|
+
// Forked three ways once: a re-typed clamp that dropped `totalPages`, and an
|
|
4
|
+
// offset redone by hand in raw SQL. The response shape is the contract a client
|
|
5
|
+
// codes against, so reshaping it per repo is a breaking change nobody declared.
|
|
6
|
+
export const MAX_PAGE_SIZE = 100;
|
|
7
|
+
export const DEFAULT_PAGE_SIZE = 20;
|
|
8
|
+
/** `page` clamped to ≥1, `pageSize` clamped to 1–100. Never trust either. */
|
|
9
|
+
export function parsePagination(searchParams, defaults) {
|
|
10
|
+
const rawPage = parseInt(searchParams.get('page') || '1', 10);
|
|
11
|
+
const rawSize = parseInt(searchParams.get('pageSize') || String(defaults?.pageSize ?? DEFAULT_PAGE_SIZE), 10);
|
|
12
|
+
return {
|
|
13
|
+
page: Number.isFinite(rawPage) ? Math.max(1, rawPage) : 1,
|
|
14
|
+
pageSize: Number.isFinite(rawSize)
|
|
15
|
+
? Math.min(MAX_PAGE_SIZE, Math.max(1, rawSize))
|
|
16
|
+
: (defaults?.pageSize ?? DEFAULT_PAGE_SIZE),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function paginatedResponse(items, total, page, pageSize) {
|
|
20
|
+
return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };
|
|
21
|
+
}
|
|
22
|
+
/** The `skip`/`take` an ORM wants, from the same clamped pair. */
|
|
23
|
+
export function toOffset({ page, pageSize }) {
|
|
24
|
+
return { skip: (page - 1) * pageSize, take: pageSize };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.js","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,gFAAgF;AAahF,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AACjC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,UAAU,eAAe,CAC7B,YAA6B,EAC7B,QAAgC;IAEhC,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,QAAQ,CACtB,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,QAAQ,IAAI,iBAAiB,CAAC,EAC/E,EAAE,CACH,CAAC;IACF,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;YAChC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;YAC/C,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,IAAI,iBAAiB,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAU,EACV,KAAa,EACb,IAAY,EACZ,QAAgB;IAEhB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,CAAC;AACnF,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAc;IACrD,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** A metadata key a tag path declares. Only `select` keys become facets. */
|
|
2
|
+
export interface MetadataKeyDefinition {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
type: 'text' | 'date' | 'url' | 'select';
|
|
6
|
+
options?: string[];
|
|
7
|
+
}
|
|
8
|
+
/** The minimum a page must be for this module to classify it. */
|
|
9
|
+
export interface FacetablePage {
|
|
10
|
+
title: string;
|
|
11
|
+
metadata?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface FacetValue {
|
|
14
|
+
value: string;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
export interface Facet {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
values: FacetValue[];
|
|
21
|
+
}
|
|
22
|
+
export type FacetFilters = Record<string, string>;
|
|
23
|
+
export interface SharedFacet {
|
|
24
|
+
key: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RelatedRanking<T> {
|
|
28
|
+
pages: T[];
|
|
29
|
+
sharedFacet: SharedFacet | null;
|
|
30
|
+
}
|
|
31
|
+
/** State a category URL can carry. `href` receives this and owns the format. */
|
|
32
|
+
export interface CategoryState {
|
|
33
|
+
sort?: string;
|
|
34
|
+
filters?: FacetFilters;
|
|
35
|
+
letter?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface TaxonomyConfig {
|
|
38
|
+
/**
|
|
39
|
+
* The metadata keys a tag path declares, nearest-last or nearest-first — the
|
|
40
|
+
* order only matters for `label` collisions. Host-supplied because one repo
|
|
41
|
+
* resolves it down a nested tree and another down a flat trail.
|
|
42
|
+
*/
|
|
43
|
+
getMetadataKeys: (tagPath: string) => MetadataKeyDefinition[];
|
|
44
|
+
/**
|
|
45
|
+
* The category URL contract. Every chip, letter, sort button and infobox row
|
|
46
|
+
* builds through this one function — a sort button that drops the active
|
|
47
|
+
* filters is the tell that a project grew a second one.
|
|
48
|
+
*/
|
|
49
|
+
href?: (tagPath: string, state: CategoryState) => string;
|
|
50
|
+
/** Below this a category fits on a screen or two; above it, readers need an index. */
|
|
51
|
+
alphaIndexMinPages?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Default: `/{tagPath}?sort=…&{facet}=…&letter=…`. Override for a prefixed mount. */
|
|
54
|
+
export declare function defaultHref(tagPath: string, state: CategoryState): string;
|
|
55
|
+
export declare const DEFAULT_ALPHA_INDEX_MIN_PAGES = 40;
|
|
56
|
+
/** Bucket for titles that don't start with a letter (numerals, `$CAPER`). */
|
|
57
|
+
export declare function firstLetter(title: string): string;
|
|
58
|
+
/** Adding a filter already active removes it, so every chip is its own off-switch. */
|
|
59
|
+
export declare function toggleFilter(filters: FacetFilters, key: string, value: string): FacetFilters;
|
|
60
|
+
export interface Taxonomy {
|
|
61
|
+
/** The select-typed keys a tag path declares — its facetable axes. */
|
|
62
|
+
facetKeys: (tagPath: string) => MetadataKeyDefinition[];
|
|
63
|
+
/** Query params narrowed to facets this tag path actually declares. */
|
|
64
|
+
facetFilters: (tagPath: string, params: Record<string, string | string[] | undefined>) => FacetFilters;
|
|
65
|
+
filterPages: <T extends FacetablePage>(pages: T[], filters: FacetFilters, letter?: string) => T[];
|
|
66
|
+
buildFacets: <T extends FacetablePage>(tagPath: string, pages: T[], filters: FacetFilters, letter?: string) => Facet[];
|
|
67
|
+
alphaIndex: <T extends FacetablePage>(pages: T[], filters: FacetFilters) => FacetValue[];
|
|
68
|
+
/** Whether a set has outgrown a grid and needs the A–Z index. */
|
|
69
|
+
needsAlphaIndex: (pageCount: number) => boolean;
|
|
70
|
+
href: (tagPath: string, state: CategoryState) => string;
|
|
71
|
+
rankRelated: <T extends FacetablePage>(page: T, siblings: T[], tagPath: string, limit?: number) => RelatedRanking<T>;
|
|
72
|
+
}
|
|
73
|
+
export declare function createTaxonomy(config: TaxonomyConfig): Taxonomy;
|
|
74
|
+
//# sourceMappingURL=taxonomy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxonomy.d.ts","sourceRoot":"","sources":["../src/taxonomy.ts"],"names":[],"mappings":"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"}
|
package/dist/taxonomy.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// taxonomy.ts — the second axis of category browsing.
|
|
2
|
+
//
|
|
3
|
+
// A `tagPath` puts a page in exactly one place in a tree. The `select`-typed
|
|
4
|
+
// metadata it already carries is a cross-cutting axis that is almost always
|
|
5
|
+
// stored and never surfaced — the failure this module exists to prevent is a
|
|
6
|
+
// 141-page category rendered as one flat grid while the data to split it sits
|
|
7
|
+
// unread in a JSON column.
|
|
8
|
+
//
|
|
9
|
+
// Everything here is derived. Nothing is hand-curated, so nothing can go stale:
|
|
10
|
+
// fix a page and it leaves the queue on the next revalidation.
|
|
11
|
+
//
|
|
12
|
+
// Two consumers proved the mechanism travels — one walks a nested tag tree, the
|
|
13
|
+
// other a flat category list — so the tree itself is NOT in this module. Supply
|
|
14
|
+
// `getMetadataKeys(tagPath)` and a `href` builder and the rest follows.
|
|
15
|
+
/** Default: `/{tagPath}?sort=…&{facet}=…&letter=…`. Override for a prefixed mount. */
|
|
16
|
+
export function defaultHref(tagPath, state) {
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
if (state.sort)
|
|
19
|
+
params.set('sort', state.sort);
|
|
20
|
+
for (const [key, value] of Object.entries(state.filters ?? {}))
|
|
21
|
+
params.set(key, value);
|
|
22
|
+
if (state.letter)
|
|
23
|
+
params.set('letter', state.letter);
|
|
24
|
+
const query = params.toString();
|
|
25
|
+
return `/${tagPath}${query ? `?${query}` : ''}`;
|
|
26
|
+
}
|
|
27
|
+
export const DEFAULT_ALPHA_INDEX_MIN_PAGES = 40;
|
|
28
|
+
const metaValue = (page, key) => (page.metadata?.[key] ?? '').trim();
|
|
29
|
+
/** Bucket for titles that don't start with a letter (numerals, `$CAPER`). */
|
|
30
|
+
export function firstLetter(title) {
|
|
31
|
+
const ch = title.trim()[0]?.toUpperCase() ?? '';
|
|
32
|
+
return ch >= 'A' && ch <= 'Z' ? ch : '#';
|
|
33
|
+
}
|
|
34
|
+
/** Adding a filter already active removes it, so every chip is its own off-switch. */
|
|
35
|
+
export function toggleFilter(filters, key, value) {
|
|
36
|
+
const next = { ...filters };
|
|
37
|
+
if (next[key] === value)
|
|
38
|
+
delete next[key];
|
|
39
|
+
else
|
|
40
|
+
next[key] = value;
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
export function createTaxonomy(config) {
|
|
44
|
+
const href = config.href ?? defaultHref;
|
|
45
|
+
const minPages = config.alphaIndexMinPages ?? DEFAULT_ALPHA_INDEX_MIN_PAGES;
|
|
46
|
+
const facetKeys = (tagPath) => config.getMetadataKeys(tagPath).filter(k => k.type === 'select');
|
|
47
|
+
const matches = (page, filters, letter) => {
|
|
48
|
+
if (letter && firstLetter(page.title) !== letter)
|
|
49
|
+
return false;
|
|
50
|
+
return Object.entries(filters).every(([key, value]) => metaValue(page, key) === value);
|
|
51
|
+
};
|
|
52
|
+
const filterPages = (pages, filters, letter) => {
|
|
53
|
+
if (!Object.keys(filters).length && !letter)
|
|
54
|
+
return pages;
|
|
55
|
+
return pages.filter(p => matches(p, filters, letter));
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
facetKeys,
|
|
59
|
+
href,
|
|
60
|
+
filterPages,
|
|
61
|
+
needsAlphaIndex: (pageCount) => pageCount >= minPages,
|
|
62
|
+
facetFilters(tagPath, params) {
|
|
63
|
+
const filters = {};
|
|
64
|
+
for (const key of facetKeys(tagPath)) {
|
|
65
|
+
const value = params[key.key];
|
|
66
|
+
if (typeof value === 'string' && value)
|
|
67
|
+
filters[key.key] = value;
|
|
68
|
+
}
|
|
69
|
+
return filters;
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Facet values with counts. Each facet is counted over the set narrowed by
|
|
73
|
+
* every *other* active filter, so its own options stay switchable rather
|
|
74
|
+
* than collapsing to the one already chosen.
|
|
75
|
+
*
|
|
76
|
+
* Values are read from the data, never from the declared `options`: a key
|
|
77
|
+
* that declares four and holds seven would otherwise hide three behind a
|
|
78
|
+
* bar claiming to cover everything. Render what is there and the drift
|
|
79
|
+
* becomes visible instead of silent.
|
|
80
|
+
*/
|
|
81
|
+
buildFacets(tagPath, pages, filters, letter) {
|
|
82
|
+
return facetKeys(tagPath).flatMap(key => {
|
|
83
|
+
const others = Object.fromEntries(Object.entries(filters).filter(([k]) => k !== key.key));
|
|
84
|
+
const counts = new Map();
|
|
85
|
+
for (const page of filterPages(pages, others, letter)) {
|
|
86
|
+
const value = metaValue(page, key.key);
|
|
87
|
+
if (value)
|
|
88
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
// A single-valued facet is no choice, so it stays hidden — unless it is
|
|
91
|
+
// the active one. An infobox row can set a filter the chips never
|
|
92
|
+
// offered, and without its chip the reader lands on a narrowed list
|
|
93
|
+
// with no way to widen it.
|
|
94
|
+
if (counts.size < 2 && !(key.key in filters))
|
|
95
|
+
return [];
|
|
96
|
+
const values = [...counts]
|
|
97
|
+
.map(([value, count]) => ({ value, count }))
|
|
98
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
99
|
+
return [{ key: key.key, label: key.label.replace(/:$/, ''), values }];
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
/** A–Z buckets present in the set, `#` last. */
|
|
103
|
+
alphaIndex(pages, filters) {
|
|
104
|
+
const counts = new Map();
|
|
105
|
+
for (const page of filterPages(pages, filters)) {
|
|
106
|
+
const letter = firstLetter(page.title);
|
|
107
|
+
counts.set(letter, (counts.get(letter) ?? 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
return [...counts]
|
|
110
|
+
.map(([value, count]) => ({ value, count }))
|
|
111
|
+
.sort((a, b) => a.value === '#' ? 1 : b.value === '#' ? -1 : a.value.localeCompare(b.value));
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* Siblings ranked by how many facet values they share with the page — a
|
|
115
|
+
* real relatedness signal. The behaviour this replaces (`pages.slice(0, 5)`
|
|
116
|
+
* under a *See also* heading) showed every page in a 141-page category the
|
|
117
|
+
* same five links. Ties keep the incoming order, so the tail degrades to
|
|
118
|
+
* the category's own sort rather than to nothing.
|
|
119
|
+
*
|
|
120
|
+
* The shared facet comes back as `{key, value}` rather than a label so the
|
|
121
|
+
* heading above the five can be the link into the whole filtered set, not
|
|
122
|
+
* a sentence explaining what "related" meant.
|
|
123
|
+
*/
|
|
124
|
+
rankRelated(page, siblings, tagPath, limit = 5) {
|
|
125
|
+
const keys = facetKeys(tagPath);
|
|
126
|
+
if (!keys.length)
|
|
127
|
+
return { pages: siblings.slice(0, limit), sharedFacet: null };
|
|
128
|
+
const score = (other) => keys.reduce((n, k) => n +
|
|
129
|
+
(metaValue(page, k.key) && metaValue(other, k.key) === metaValue(page, k.key) ? 1 : 0), 0);
|
|
130
|
+
const ranked = siblings
|
|
131
|
+
.map((p, i) => ({ p, score: score(p), i }))
|
|
132
|
+
.sort((a, b) => b.score - a.score || a.i - b.i)
|
|
133
|
+
.slice(0, limit);
|
|
134
|
+
// Name the narrowest axis the page ACTUALLY SHARES with something in the
|
|
135
|
+
// list — a `category` with 15 options says more than a `status` with 4, so
|
|
136
|
+
// headline the narrower one, but only among the axes that are shared.
|
|
137
|
+
//
|
|
138
|
+
// Picking the narrowest axis the *page* carries, without checking whether
|
|
139
|
+
// any returned sibling matches on it, produces a heading that links to a
|
|
140
|
+
// set the list below it is not in: "More Classical pages in Nomenclature"
|
|
141
|
+
// over five pages that are not Classical. The heading is the link into the
|
|
142
|
+
// filtered set, so it has to name a filter that contains them.
|
|
143
|
+
const matched = ranked.filter(r => r.score > 0);
|
|
144
|
+
const narrowest = keys
|
|
145
|
+
.filter(k => metaValue(page, k.key) &&
|
|
146
|
+
matched.some(r => metaValue(r.p, k.key) === metaValue(page, k.key)))
|
|
147
|
+
.sort((a, b) => (b.options?.length ?? 0) - (a.options?.length ?? 0))[0];
|
|
148
|
+
const value = narrowest ? metaValue(page, narrowest.key) : '';
|
|
149
|
+
return {
|
|
150
|
+
pages: ranked.map(r => r.p),
|
|
151
|
+
sharedFacet: narrowest && value ? { key: narrowest.key, value } : null,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=taxonomy.js.map
|