@icure/cardinal-mcp-server 1.1.1 → 2.6.1
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/helpers/docs-loader.d.ts +15 -0
- package/dist/helpers/docs-loader.d.ts.map +1 -1
- package/dist/helpers/docs-loader.js.map +1 -1
- package/dist/helpers/oauth-server.d.ts +12 -0
- package/dist/helpers/oauth-server.d.ts.map +1 -0
- package/dist/helpers/oauth-server.js +184 -0
- package/dist/helpers/oauth-server.js.map +1 -0
- package/dist/resources/filter-docs.d.ts.map +1 -1
- package/dist/resources/filter-docs.js +20 -1
- package/dist/resources/filter-docs.js.map +1 -1
- package/dist/resources/guide-docs.d.ts +3 -0
- package/dist/resources/guide-docs.d.ts.map +1 -0
- package/dist/resources/guide-docs.js +42 -0
- package/dist/resources/guide-docs.js.map +1 -0
- package/dist/resources/model-docs.d.ts.map +1 -1
- package/dist/resources/model-docs.js +16 -5
- package/dist/resources/model-docs.js.map +1 -1
- package/dist/resources/overview.d.ts.map +1 -1
- package/dist/resources/overview.js +11 -4
- package/dist/resources/overview.js.map +1 -1
- package/dist/resources/search.d.ts.map +1 -1
- package/dist/resources/search.js +17 -1
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tutorial-docs.js +1 -1
- package/dist/resources/tutorial-docs.js.map +1 -1
- package/dist/scripts/extract-docs.d.ts +2 -0
- package/dist/scripts/extract-docs.d.ts.map +1 -0
- package/dist/scripts/extract-docs.js +795 -0
- package/dist/scripts/extract-docs.js.map +1 -0
- package/dist/scripts/generate-registry.d.ts +2 -0
- package/dist/scripts/generate-registry.d.ts.map +1 -0
- package/dist/scripts/generate-registry.js +196 -0
- package/dist/scripts/generate-registry.js.map +1 -0
- package/dist/scripts/tsconfig.tsbuildinfo +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/oauth-init.d.ts +3 -0
- package/dist/tools/oauth-init.d.ts.map +1 -0
- package/dist/tools/oauth-init.js +80 -0
- package/dist/tools/oauth-init.js.map +1 -0
- package/generated/docs-manifest.json +14654 -12635
- package/generated/method-registry.ts +362 -1570
- package/package.json +10 -10
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses Kotlin KDoc + TypeScript type declarations + SDK.md to produce a documentation manifest.
|
|
3
|
+
* Sources:
|
|
4
|
+
* - Kotlin API files: cardinal-sdk/src/commonMain/kotlin/.../api/*.kt (KDoc comments)
|
|
5
|
+
* - TypeScript API types: node_modules/@icure/cardinal-sdk/api/*.d.mts (method signatures)
|
|
6
|
+
* - TypeScript model types: node_modules/@icure/cardinal-sdk/model/** (model fields)
|
|
7
|
+
* - TypeScript filter types: node_modules/@icure/cardinal-sdk/filters/* (filter factories)
|
|
8
|
+
* - SDK.md: tutorials, how-to guides, data model docs, filter reference, encryption docs
|
|
9
|
+
*
|
|
10
|
+
* Output: generated/docs-manifest.json
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
const CARDINAL_SDK_ROOT = process.env.CARDINAL_SDK_ROOT || path.resolve(import.meta.dirname, "..", "..");
|
|
15
|
+
const NPM_SDK = path.resolve(import.meta.dirname, "..", "node_modules", "@icure", "cardinal-sdk");
|
|
16
|
+
const SDK_MD = path.resolve(import.meta.dirname, "..", "SDK.md");
|
|
17
|
+
const OUT_FILE = path.resolve(import.meta.dirname, "..", "generated", "docs-manifest.json");
|
|
18
|
+
function parseSdkMdSections() {
|
|
19
|
+
if (!fs.existsSync(SDK_MD)) {
|
|
20
|
+
console.warn("SDK.md not found:", SDK_MD);
|
|
21
|
+
return new Map();
|
|
22
|
+
}
|
|
23
|
+
const content = fs.readFileSync(SDK_MD, "utf-8");
|
|
24
|
+
const sections = new Map();
|
|
25
|
+
// Split on <!-- Source: ... --> markers
|
|
26
|
+
const sourcePattern = /<!-- Source: (.*?) -->/g;
|
|
27
|
+
const matches = [...content.matchAll(sourcePattern)];
|
|
28
|
+
for (let i = 0; i < matches.length; i++) {
|
|
29
|
+
const source = matches[i][1];
|
|
30
|
+
const startIdx = matches[i].index + matches[i][0].length;
|
|
31
|
+
const endIdx = i + 1 < matches.length ? matches[i + 1].index : content.length;
|
|
32
|
+
const body = content.substring(startIdx, endIdx).trim();
|
|
33
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
34
|
+
const title = titleMatch ? titleMatch[1].trim() : source;
|
|
35
|
+
sections.set(source, { source, title, body });
|
|
36
|
+
}
|
|
37
|
+
return sections;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Filter content to keep only TypeScript code examples and general prose.
|
|
41
|
+
* Removes kotlin/python/dart language-specific blocks.
|
|
42
|
+
*/
|
|
43
|
+
function filterToTypescript(body) {
|
|
44
|
+
const lines = body.split("\n");
|
|
45
|
+
const result = [];
|
|
46
|
+
let skipLang = false;
|
|
47
|
+
let inCodeBlock = false;
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
// Track code blocks
|
|
52
|
+
if (trimmed.startsWith("```")) {
|
|
53
|
+
if (!inCodeBlock) {
|
|
54
|
+
inCodeBlock = true;
|
|
55
|
+
if (skipLang) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
inCodeBlock = false;
|
|
61
|
+
if (skipLang) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (inCodeBlock && skipLang) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Detect language markers: **kotlin:**, **python:**, **dart:**
|
|
70
|
+
if (/^\*\*(kotlin|python|dart):\*\*\s*$/i.test(trimmed)) {
|
|
71
|
+
skipLang = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Detect TypeScript marker — keep it but mark as active language
|
|
75
|
+
if (/^\*\*typescript:\*\*\s*$/i.test(trimmed)) {
|
|
76
|
+
skipLang = false;
|
|
77
|
+
// Don't emit the marker itself, just keep the content that follows
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// If we're in a non-TS language block (prose between language marker and code block), skip
|
|
81
|
+
if (skipLang && !inCodeBlock) {
|
|
82
|
+
// Check if this line is a heading or another section start — stop skipping
|
|
83
|
+
if (/^#{1,4}\s/.test(trimmed) || /^<!-- Source:/.test(trimmed)) {
|
|
84
|
+
skipLang = false;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
result.push(line);
|
|
91
|
+
}
|
|
92
|
+
return result.join("\n").trim();
|
|
93
|
+
}
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Kotlin KDoc Parsing
|
|
96
|
+
// =============================================================================
|
|
97
|
+
function parseKDocComments(kotlinSource) {
|
|
98
|
+
const result = new Map();
|
|
99
|
+
const kdocPattern = /\/\*\*([\s\S]*?)\*\/\s*(?:(?:suspend\s+)?fun\s+(\w+))/g;
|
|
100
|
+
let match;
|
|
101
|
+
while ((match = kdocPattern.exec(kotlinSource)) !== null) {
|
|
102
|
+
const [, rawDoc, methodName] = match;
|
|
103
|
+
const lines = rawDoc.split("\n").map(l => l.replace(/^\s*\*\s?/, "").trim());
|
|
104
|
+
let description = "";
|
|
105
|
+
const params = new Map();
|
|
106
|
+
let returns = "";
|
|
107
|
+
const throws = [];
|
|
108
|
+
let currentTag = null;
|
|
109
|
+
let currentTagName = "";
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (line.startsWith("@param")) {
|
|
112
|
+
const paramMatch = line.match(/@param\s+(\w+)\s*(.*)/);
|
|
113
|
+
if (paramMatch) {
|
|
114
|
+
currentTag = "param";
|
|
115
|
+
currentTagName = paramMatch[1];
|
|
116
|
+
params.set(paramMatch[1], paramMatch[2]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (line.startsWith("@return")) {
|
|
120
|
+
currentTag = "return";
|
|
121
|
+
returns = line.replace(/@returns?\s*/, "");
|
|
122
|
+
}
|
|
123
|
+
else if (line.startsWith("@throws") || line.startsWith("@exception")) {
|
|
124
|
+
currentTag = "throws";
|
|
125
|
+
throws.push(line.replace(/@(?:throws|exception)\s*/, ""));
|
|
126
|
+
}
|
|
127
|
+
else if (line.startsWith("@")) {
|
|
128
|
+
currentTag = null;
|
|
129
|
+
}
|
|
130
|
+
else if (currentTag === "param") {
|
|
131
|
+
params.set(currentTagName, (params.get(currentTagName) || "") + " " + line);
|
|
132
|
+
}
|
|
133
|
+
else if (currentTag === "return") {
|
|
134
|
+
returns += " " + line;
|
|
135
|
+
}
|
|
136
|
+
else if (!currentTag) {
|
|
137
|
+
description += (description ? " " : "") + line;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
result.set(methodName, { description: description.trim(), params, returns: returns.trim(), throws });
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// TypeScript API Parsing
|
|
146
|
+
// =============================================================================
|
|
147
|
+
const ENCRYPTABLE_ENTITIES = new Set([
|
|
148
|
+
"AccessLog", "CalendarItem", "Contact", "Document",
|
|
149
|
+
"Form", "HealthElement", "Invoice", "MaintenanceTask", "Message",
|
|
150
|
+
"Patient", "Receipt", "Topic",
|
|
151
|
+
]);
|
|
152
|
+
/**
|
|
153
|
+
* Only APIs explicitly listed in tool registrations (admin-tools, data-owner-tools, crypto-tools)
|
|
154
|
+
* should be included in the manifest. This excludes deprecated or internal APIs.
|
|
155
|
+
*/
|
|
156
|
+
const ALLOWED_APIS = new Set([
|
|
157
|
+
// data-owner-tools
|
|
158
|
+
"HealthcareParty", "Patient", "Device",
|
|
159
|
+
// admin-tools
|
|
160
|
+
"Group", "User", "Role", "Permission", "System", "Auth",
|
|
161
|
+
// crypto-tools
|
|
162
|
+
"Crypto", "Recovery", "ShamirKeysManager", "DataOwner", "CardinalMaintenanceTask",
|
|
163
|
+
// Supporting entities
|
|
164
|
+
"CalendarItemType", "FormTemplate", "DocumentTemplate",
|
|
165
|
+
// Encryptable entity APIs (used via dispatch)
|
|
166
|
+
"AccessLog", "Agenda", "CalendarItem", "Code", "Contact",
|
|
167
|
+
"Document", "Form", "HealthElement", "Insurance", "Invoice",
|
|
168
|
+
"MaintenanceTask", "Message", "Receipt", "Topic",
|
|
169
|
+
]);
|
|
170
|
+
function parseApiInterface(dtsContent, _apiName) {
|
|
171
|
+
const methods = [];
|
|
172
|
+
const methodPattern = /^\s+(\w+)\(([^)]*)\):\s*Promise<([^>]+)>|^\s+(\w+)\(([^)]*)\):\s*([^;]+);/gm;
|
|
173
|
+
let match;
|
|
174
|
+
while ((match = methodPattern.exec(dtsContent)) !== null) {
|
|
175
|
+
const name = match[1] || match[4];
|
|
176
|
+
const rawParams = match[2] || match[5] || "";
|
|
177
|
+
const returnType = match[3] || match[6] || "void";
|
|
178
|
+
if (!name || name === "constructor")
|
|
179
|
+
continue;
|
|
180
|
+
const params = parseParams(rawParams);
|
|
181
|
+
methods.push({
|
|
182
|
+
name,
|
|
183
|
+
description: "",
|
|
184
|
+
params,
|
|
185
|
+
returnType: returnType.trim(),
|
|
186
|
+
flavours: ["none"],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return methods;
|
|
190
|
+
}
|
|
191
|
+
function parseParams(rawParams) {
|
|
192
|
+
if (!rawParams.trim())
|
|
193
|
+
return [];
|
|
194
|
+
const params = [];
|
|
195
|
+
const parts = splitTopLevel(rawParams, ",");
|
|
196
|
+
for (const part of parts) {
|
|
197
|
+
const trimmed = part.trim();
|
|
198
|
+
if (!trimmed)
|
|
199
|
+
continue;
|
|
200
|
+
const paramMatch = trimmed.match(/^(\w+)(\?)?:\s*(.+)$/);
|
|
201
|
+
if (paramMatch) {
|
|
202
|
+
params.push({
|
|
203
|
+
name: paramMatch[1],
|
|
204
|
+
type: paramMatch[3].trim(),
|
|
205
|
+
description: "",
|
|
206
|
+
optional: !!paramMatch[2],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return params;
|
|
211
|
+
}
|
|
212
|
+
function splitTopLevel(str, sep) {
|
|
213
|
+
const parts = [];
|
|
214
|
+
let depth = 0;
|
|
215
|
+
let current = "";
|
|
216
|
+
for (const ch of str) {
|
|
217
|
+
if (ch === "(" || ch === "{" || ch === "[" || ch === "<")
|
|
218
|
+
depth++;
|
|
219
|
+
else if (ch === ")" || ch === "}" || ch === "]" || ch === ">")
|
|
220
|
+
depth--;
|
|
221
|
+
if (depth === 0 && ch === sep) {
|
|
222
|
+
parts.push(current);
|
|
223
|
+
current = "";
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
current += ch;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (current.trim())
|
|
230
|
+
parts.push(current);
|
|
231
|
+
return parts;
|
|
232
|
+
}
|
|
233
|
+
function extractApis() {
|
|
234
|
+
const apis = {};
|
|
235
|
+
const apiDir = path.join(NPM_SDK, "api");
|
|
236
|
+
if (!fs.existsSync(apiDir)) {
|
|
237
|
+
console.warn("API directory not found:", apiDir);
|
|
238
|
+
return apis;
|
|
239
|
+
}
|
|
240
|
+
// Load Kotlin KDoc for enrichment
|
|
241
|
+
const kotlinApiDir = path.join(CARDINAL_SDK_ROOT, "cardinal-sdk", "src", "commonMain", "kotlin", "com", "icure", "cardinal", "sdk", "api");
|
|
242
|
+
const kotlinDocs = new Map();
|
|
243
|
+
if (fs.existsSync(kotlinApiDir)) {
|
|
244
|
+
for (const file of fs.readdirSync(kotlinApiDir)) {
|
|
245
|
+
if (!file.endsWith(".kt"))
|
|
246
|
+
continue;
|
|
247
|
+
const content = fs.readFileSync(path.join(kotlinApiDir, file), "utf-8");
|
|
248
|
+
const baseName = file.replace(".kt", "");
|
|
249
|
+
kotlinDocs.set(baseName, parseKDocComments(content));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// We only want the main API files (not Basic, Flavoured, InGroup variants)
|
|
253
|
+
const mainApiPattern = /^(\w+)Api\.d\.mts$/;
|
|
254
|
+
const skipPatterns = [/Basic/, /Flavoured/, /InGroup/, /Anonymous/];
|
|
255
|
+
for (const file of fs.readdirSync(apiDir).sort()) {
|
|
256
|
+
const match = file.match(mainApiPattern);
|
|
257
|
+
if (!match)
|
|
258
|
+
continue;
|
|
259
|
+
if (skipPatterns.some(p => p.test(file)))
|
|
260
|
+
continue;
|
|
261
|
+
const entityName = match[1];
|
|
262
|
+
if (!ALLOWED_APIS.has(entityName))
|
|
263
|
+
continue;
|
|
264
|
+
const dtsContent = fs.readFileSync(path.join(apiDir, file), "utf-8");
|
|
265
|
+
const isEncryptable = ENCRYPTABLE_ENTITIES.has(entityName);
|
|
266
|
+
const methods = parseApiInterface(dtsContent, entityName);
|
|
267
|
+
if (isEncryptable) {
|
|
268
|
+
for (const method of methods) {
|
|
269
|
+
method.flavours = ["decrypted", "encrypted", "tryAndRecover"];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Enrich with Kotlin KDoc
|
|
273
|
+
for (const kn of [entityName + "Api", entityName + "FlavouredApi", entityName + "BasicFlavourlessApi"]) {
|
|
274
|
+
const kdoc = kotlinDocs.get(kn);
|
|
275
|
+
if (!kdoc)
|
|
276
|
+
continue;
|
|
277
|
+
for (const method of methods) {
|
|
278
|
+
const doc = kdoc.get(method.name);
|
|
279
|
+
if (!doc)
|
|
280
|
+
continue;
|
|
281
|
+
if (doc.description)
|
|
282
|
+
method.description = doc.description;
|
|
283
|
+
if (doc.throws.length)
|
|
284
|
+
method.throws = doc.throws;
|
|
285
|
+
for (const param of method.params) {
|
|
286
|
+
const paramDesc = doc.params.get(param.name);
|
|
287
|
+
if (paramDesc)
|
|
288
|
+
param.description = paramDesc.trim();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
apis[entityName] = {
|
|
293
|
+
name: entityName + "Api",
|
|
294
|
+
description: `API for ${entityName} operations`,
|
|
295
|
+
isEncryptable,
|
|
296
|
+
methods,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return apis;
|
|
300
|
+
}
|
|
301
|
+
// =============================================================================
|
|
302
|
+
// TypeScript Model Parsing
|
|
303
|
+
// =============================================================================
|
|
304
|
+
function extractModels() {
|
|
305
|
+
const models = {};
|
|
306
|
+
const modelBase = path.join(NPM_SDK, "model");
|
|
307
|
+
if (!fs.existsSync(modelBase)) {
|
|
308
|
+
console.warn("Model directory not found:", modelBase);
|
|
309
|
+
return models;
|
|
310
|
+
}
|
|
311
|
+
function walkDir(dir) {
|
|
312
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
313
|
+
if (entry.isDirectory()) {
|
|
314
|
+
walkDir(path.join(dir, entry.name));
|
|
315
|
+
}
|
|
316
|
+
else if (entry.name.endsWith(".d.mts")) {
|
|
317
|
+
parseModelFile(path.join(dir, entry.name), models);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
walkDir(modelBase);
|
|
322
|
+
return models;
|
|
323
|
+
}
|
|
324
|
+
function parseModelFile(filePath, models) {
|
|
325
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
326
|
+
// Extract class declarations
|
|
327
|
+
const classPattern = /export\s+declare\s+class\s+(\w+)(?:\s+extends\s+[\w.]+)?(?:\s+implements\s+([\w,\s.]+))?\s*\{/g;
|
|
328
|
+
let match;
|
|
329
|
+
while ((match = classPattern.exec(content)) !== null) {
|
|
330
|
+
const className = match[1];
|
|
331
|
+
const implements_ = match[2]?.split(",").map(s => s.trim()).filter(Boolean);
|
|
332
|
+
const fields = extractClassFields(content, match.index + match[0].length);
|
|
333
|
+
models[className] = {
|
|
334
|
+
name: className,
|
|
335
|
+
implements: implements_,
|
|
336
|
+
fields,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// Extract interface declarations
|
|
340
|
+
const interfacePattern = /export\s+declare\s+interface\s+(\w+)(?:\s+extends\s+([\w,\s.<>]+))?\s*\{/g;
|
|
341
|
+
while ((match = interfacePattern.exec(content)) !== null) {
|
|
342
|
+
const interfaceName = match[1];
|
|
343
|
+
const extends_ = match[2]?.split(",").map(s => s.trim()).filter(Boolean);
|
|
344
|
+
if (interfaceName.includes("$metadata$"))
|
|
345
|
+
continue;
|
|
346
|
+
const fields = extractInterfaceFields(content, match.index + match[0].length);
|
|
347
|
+
const isEncryptable = ENCRYPTABLE_ENTITIES.has(interfaceName);
|
|
348
|
+
const variants = isEncryptable
|
|
349
|
+
? [`Decrypted${interfaceName}`, `Encrypted${interfaceName}`]
|
|
350
|
+
: undefined;
|
|
351
|
+
if (!models[interfaceName]) {
|
|
352
|
+
models[interfaceName] = {
|
|
353
|
+
name: interfaceName,
|
|
354
|
+
variants,
|
|
355
|
+
implements: extends_,
|
|
356
|
+
fields,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function extractClassFields(content, startIdx) {
|
|
362
|
+
const fields = [];
|
|
363
|
+
let depth = 1;
|
|
364
|
+
let i = startIdx;
|
|
365
|
+
while (i < content.length && depth > 0) {
|
|
366
|
+
if (content[i] === "{")
|
|
367
|
+
depth++;
|
|
368
|
+
else if (content[i] === "}")
|
|
369
|
+
depth--;
|
|
370
|
+
i++;
|
|
371
|
+
}
|
|
372
|
+
const body = content.substring(startIdx, i - 1);
|
|
373
|
+
const fieldPattern = /(?:readonly\s+)?(\w+)(\?)?:\s*([^;]+);/g;
|
|
374
|
+
let match;
|
|
375
|
+
while ((match = fieldPattern.exec(body)) !== null) {
|
|
376
|
+
const name = match[1];
|
|
377
|
+
const type = match[3].trim();
|
|
378
|
+
if (name === "constructor" || name === "__doNotUseOrImplementIt")
|
|
379
|
+
continue;
|
|
380
|
+
fields.push({ name, type, description: "" });
|
|
381
|
+
}
|
|
382
|
+
return fields;
|
|
383
|
+
}
|
|
384
|
+
function extractInterfaceFields(content, startIdx) {
|
|
385
|
+
return extractClassFields(content, startIdx);
|
|
386
|
+
}
|
|
387
|
+
// =============================================================================
|
|
388
|
+
// Filter Parsing (from TypeScript declarations)
|
|
389
|
+
// =============================================================================
|
|
390
|
+
function extractFilters() {
|
|
391
|
+
const filters = {};
|
|
392
|
+
const filterDir = path.join(NPM_SDK, "filters");
|
|
393
|
+
if (!fs.existsSync(filterDir)) {
|
|
394
|
+
console.warn("Filters directory not found:", filterDir);
|
|
395
|
+
return filters;
|
|
396
|
+
}
|
|
397
|
+
for (const file of fs.readdirSync(filterDir).sort()) {
|
|
398
|
+
if (!file.endsWith(".d.mts"))
|
|
399
|
+
continue;
|
|
400
|
+
const content = fs.readFileSync(path.join(filterDir, file), "utf-8");
|
|
401
|
+
const entityName = file.replace("Filters.d.mts", "").replace(".d.mts", "");
|
|
402
|
+
if (!entityName || entityName === "index")
|
|
403
|
+
continue;
|
|
404
|
+
const methods = [];
|
|
405
|
+
const methodPattern = /(\w+)\(([^)]*)\):\s*(?:Base)?(?:Sortable)?FilterOptions<\w+>/g;
|
|
406
|
+
let match;
|
|
407
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
408
|
+
const name = match[1];
|
|
409
|
+
const rawParams = match[2];
|
|
410
|
+
const params = rawParams
|
|
411
|
+
.split(",")
|
|
412
|
+
.map(p => p.trim())
|
|
413
|
+
.filter(Boolean)
|
|
414
|
+
.map(p => {
|
|
415
|
+
const m = p.match(/^(\w+)\??:\s*(.+)$/);
|
|
416
|
+
return m ? { name: m[1], type: m[2].trim() } : { name: p, type: "unknown" };
|
|
417
|
+
});
|
|
418
|
+
if (!methods.some(m => m.name === name)) {
|
|
419
|
+
methods.push({ name, description: "", params });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (methods.length > 0) {
|
|
423
|
+
filters[entityName] = { entityName, methods };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return filters;
|
|
427
|
+
}
|
|
428
|
+
// =============================================================================
|
|
429
|
+
// SDK.md Content Extraction
|
|
430
|
+
// =============================================================================
|
|
431
|
+
function extractTutorials(sections) {
|
|
432
|
+
const tutorials = [];
|
|
433
|
+
for (const [source, section] of sections) {
|
|
434
|
+
if (!source.startsWith("sdk/tutorial/"))
|
|
435
|
+
continue;
|
|
436
|
+
const slug = deriveSlug(source, "tutorial");
|
|
437
|
+
const content = filterToTypescript(section.body);
|
|
438
|
+
if (content.length > 50) {
|
|
439
|
+
tutorials.push({
|
|
440
|
+
slug,
|
|
441
|
+
title: section.title,
|
|
442
|
+
content,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return tutorials;
|
|
447
|
+
}
|
|
448
|
+
function extractGuides(sections) {
|
|
449
|
+
const guides = [];
|
|
450
|
+
for (const [source, section] of sections) {
|
|
451
|
+
let category = null;
|
|
452
|
+
if (source.startsWith("sdk/how-to/")) {
|
|
453
|
+
category = "how-to";
|
|
454
|
+
}
|
|
455
|
+
else if (source.startsWith("sdk/quickstart/")) {
|
|
456
|
+
category = "quickstart";
|
|
457
|
+
}
|
|
458
|
+
else if (source.startsWith("sdk/explanations/end-to-end-encryption/")) {
|
|
459
|
+
category = "encryption";
|
|
460
|
+
}
|
|
461
|
+
else if (source.startsWith("sdk/troubleshooting/")) {
|
|
462
|
+
category = "troubleshooting";
|
|
463
|
+
}
|
|
464
|
+
if (!category)
|
|
465
|
+
continue;
|
|
466
|
+
const slug = deriveSlug(source, category === "encryption" ? "encryption" : category === "quickstart" ? "quickstart" : category === "troubleshooting" ? "troubleshooting" : "how-to");
|
|
467
|
+
const content = (category === "encryption" || category === "troubleshooting")
|
|
468
|
+
? section.body
|
|
469
|
+
: filterToTypescript(section.body);
|
|
470
|
+
if (content.length > 50) {
|
|
471
|
+
guides.push({
|
|
472
|
+
slug,
|
|
473
|
+
title: section.title,
|
|
474
|
+
content,
|
|
475
|
+
category,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return guides;
|
|
480
|
+
}
|
|
481
|
+
function deriveSlug(source, prefix) {
|
|
482
|
+
let slug = source
|
|
483
|
+
.replace(/^sdk\/tutorial\//, "")
|
|
484
|
+
.replace(/^sdk\/how-to\//, "")
|
|
485
|
+
.replace(/^sdk\/quickstart\//, "")
|
|
486
|
+
.replace(/^sdk\/explanations\/end-to-end-encryption\//, "")
|
|
487
|
+
.replace(/^sdk\/troubleshooting\//, "")
|
|
488
|
+
.replace(/\.mdx?$/, "")
|
|
489
|
+
.replace(/\/modules\/\d+_/, "-")
|
|
490
|
+
.replace(/\/index$/, "")
|
|
491
|
+
.replace(/\//g, "-")
|
|
492
|
+
.replace(/_/g, "-");
|
|
493
|
+
// Remove leading number prefix if any
|
|
494
|
+
slug = slug.replace(/^\d+-/, "");
|
|
495
|
+
return `${prefix}-${slug}`.replace(/--+/g, "-").replace(/-$/, "");
|
|
496
|
+
}
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// SDK.md Enrichment
|
|
499
|
+
// =============================================================================
|
|
500
|
+
const DATA_MODEL_TO_ENTITY = {
|
|
501
|
+
"patient": "Patient",
|
|
502
|
+
"contact": "Contact",
|
|
503
|
+
"service": "Service",
|
|
504
|
+
"content": "Content",
|
|
505
|
+
"subcontact": "SubContact",
|
|
506
|
+
"healthelement": "HealthElement",
|
|
507
|
+
"document": "Document",
|
|
508
|
+
"message": "Message",
|
|
509
|
+
"topic": "Topic",
|
|
510
|
+
"healthcareparty": "HealthcareParty",
|
|
511
|
+
"device": "Device",
|
|
512
|
+
"user": "User",
|
|
513
|
+
"code": "Code",
|
|
514
|
+
"codestub": "CodeStub",
|
|
515
|
+
"identfier": "Identifier",
|
|
516
|
+
"agenda": "Agenda",
|
|
517
|
+
"calendaritem": "CalendarItem",
|
|
518
|
+
};
|
|
519
|
+
function enrichApiDescriptions(apis, sections) {
|
|
520
|
+
for (const [source, section] of sections) {
|
|
521
|
+
if (!source.startsWith("sdk/explanations/data-model/") || source.endsWith("index.mdx"))
|
|
522
|
+
continue;
|
|
523
|
+
const baseName = path.basename(source, ".mdx");
|
|
524
|
+
const entityName = DATA_MODEL_TO_ENTITY[baseName];
|
|
525
|
+
if (!entityName || !apis[entityName])
|
|
526
|
+
continue;
|
|
527
|
+
const firstParagraph = extractFirstParagraph(section.body);
|
|
528
|
+
if (firstParagraph) {
|
|
529
|
+
apis[entityName].description = firstParagraph;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function extractFirstParagraph(body) {
|
|
534
|
+
const lines = body.split("\n");
|
|
535
|
+
let inParagraph = false;
|
|
536
|
+
const paragraphLines = [];
|
|
537
|
+
for (const line of lines) {
|
|
538
|
+
const trimmed = line.trim();
|
|
539
|
+
if (trimmed.startsWith("# "))
|
|
540
|
+
continue;
|
|
541
|
+
if (!inParagraph && !trimmed)
|
|
542
|
+
continue;
|
|
543
|
+
if (!inParagraph && trimmed && !trimmed.startsWith("#") && !trimmed.startsWith(">") && !trimmed.startsWith("-") && !trimmed.startsWith("```")) {
|
|
544
|
+
inParagraph = true;
|
|
545
|
+
}
|
|
546
|
+
if (inParagraph) {
|
|
547
|
+
if (!trimmed)
|
|
548
|
+
break;
|
|
549
|
+
paragraphLines.push(trimmed);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return paragraphLines.join(" ")
|
|
553
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
554
|
+
.trim();
|
|
555
|
+
}
|
|
556
|
+
function enrichModelFieldDescriptions(models, sections) {
|
|
557
|
+
for (const [source, section] of sections) {
|
|
558
|
+
if (!source.startsWith("sdk/explanations/data-model/") || source.endsWith("index.mdx"))
|
|
559
|
+
continue;
|
|
560
|
+
const baseName = path.basename(source, ".mdx");
|
|
561
|
+
const entityName = DATA_MODEL_TO_ENTITY[baseName];
|
|
562
|
+
if (!entityName)
|
|
563
|
+
continue;
|
|
564
|
+
const entityDescription = extractFirstParagraph(section.body);
|
|
565
|
+
const fieldDocs = parseFieldDocs(section.body);
|
|
566
|
+
if (fieldDocs.size === 0 && !entityDescription)
|
|
567
|
+
continue;
|
|
568
|
+
const modelKeys = getModelKeysForEntity(entityName, models);
|
|
569
|
+
for (const modelKey of modelKeys) {
|
|
570
|
+
const model = models[modelKey];
|
|
571
|
+
if (!model)
|
|
572
|
+
continue;
|
|
573
|
+
if (entityDescription) {
|
|
574
|
+
model.description = entityDescription;
|
|
575
|
+
}
|
|
576
|
+
for (const field of model.fields) {
|
|
577
|
+
const doc = fieldDocs.get(field.name);
|
|
578
|
+
if (doc) {
|
|
579
|
+
field.description = doc;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function parseFieldDocs(body) {
|
|
586
|
+
const fields = new Map();
|
|
587
|
+
const lines = body.split("\n");
|
|
588
|
+
let currentField = null;
|
|
589
|
+
const descLines = [];
|
|
590
|
+
for (const line of lines) {
|
|
591
|
+
const trimmed = line.trim();
|
|
592
|
+
const fieldMatch = trimmed.match(/^###\s+(\w+)\s*$/);
|
|
593
|
+
if (fieldMatch) {
|
|
594
|
+
if (currentField && descLines.length > 0) {
|
|
595
|
+
fields.set(currentField, descLines.join(" ").trim());
|
|
596
|
+
}
|
|
597
|
+
currentField = fieldMatch[1];
|
|
598
|
+
descLines.length = 0;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (/^##\s/.test(trimmed) && currentField) {
|
|
602
|
+
if (descLines.length > 0) {
|
|
603
|
+
fields.set(currentField, descLines.join(" ").trim());
|
|
604
|
+
}
|
|
605
|
+
currentField = null;
|
|
606
|
+
descLines.length = 0;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (currentField) {
|
|
610
|
+
if (trimmed.startsWith(">"))
|
|
611
|
+
continue;
|
|
612
|
+
if (trimmed.startsWith("```")) {
|
|
613
|
+
currentField = null;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (trimmed) {
|
|
617
|
+
descLines.push(trimmed);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (currentField && descLines.length > 0) {
|
|
622
|
+
fields.set(currentField, descLines.join(" ").trim());
|
|
623
|
+
}
|
|
624
|
+
for (const [key, value] of fields) {
|
|
625
|
+
fields.set(key, value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"));
|
|
626
|
+
}
|
|
627
|
+
return fields;
|
|
628
|
+
}
|
|
629
|
+
function getModelKeysForEntity(entityName, models) {
|
|
630
|
+
const keys = [];
|
|
631
|
+
if (ENCRYPTABLE_ENTITIES.has(entityName)) {
|
|
632
|
+
if (models[`Decrypted${entityName}`])
|
|
633
|
+
keys.push(`Decrypted${entityName}`);
|
|
634
|
+
if (models[`Encrypted${entityName}`])
|
|
635
|
+
keys.push(`Encrypted${entityName}`);
|
|
636
|
+
}
|
|
637
|
+
if (models[entityName])
|
|
638
|
+
keys.push(entityName);
|
|
639
|
+
return keys;
|
|
640
|
+
}
|
|
641
|
+
function enrichFilterDescriptions(filters, sections) {
|
|
642
|
+
const filterSection = sections.get("sdk/explanations/everything-about-filters.mdx");
|
|
643
|
+
if (!filterSection)
|
|
644
|
+
return;
|
|
645
|
+
const body = filterSection.body;
|
|
646
|
+
const entityFilterPattern = /#{3,4}\s+(\w+)Filters\s*\n/g;
|
|
647
|
+
let match;
|
|
648
|
+
const entityPositions = [];
|
|
649
|
+
while ((match = entityFilterPattern.exec(body)) !== null) {
|
|
650
|
+
entityPositions.push({ entityName: match[1], startIdx: match.index + match[0].length });
|
|
651
|
+
}
|
|
652
|
+
for (let i = 0; i < entityPositions.length; i++) {
|
|
653
|
+
const { entityName, startIdx } = entityPositions[i];
|
|
654
|
+
const endIdx = i + 1 < entityPositions.length ? entityPositions[i + 1].startIdx : body.length;
|
|
655
|
+
const sectionBody = body.substring(startIdx, endIdx);
|
|
656
|
+
const filterKey = entityName;
|
|
657
|
+
if (!filters[filterKey])
|
|
658
|
+
continue;
|
|
659
|
+
const tableRows = parseFilterTable(sectionBody);
|
|
660
|
+
for (const row of tableRows) {
|
|
661
|
+
const method = filters[filterKey].methods.find(m => m.name === row.method);
|
|
662
|
+
if (method) {
|
|
663
|
+
method.description = row.keyParams !== "—" ? `Key parameters: ${row.keyParams}` : "";
|
|
664
|
+
method.returnType = row.returnType;
|
|
665
|
+
method.sortable = row.sortable;
|
|
666
|
+
method.sortOrder = row.sortOrder;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const introLines = sectionBody.split("\n").filter(l => l.trim() && !l.startsWith("|") && !l.startsWith(">")).slice(0, 2);
|
|
670
|
+
if (introLines.length > 0 && !introLines[0].startsWith("#")) {
|
|
671
|
+
filters[filterKey].description = introLines.join(" ").trim();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Propagate enrichment from ForSelf to ForDataOwner and ForDataOwnerInGroup variants
|
|
675
|
+
for (const filter of Object.values(filters)) {
|
|
676
|
+
const enrichedByBase = new Map();
|
|
677
|
+
for (const method of filter.methods) {
|
|
678
|
+
if (method.returnType) {
|
|
679
|
+
// Strip suffix to get base name
|
|
680
|
+
const baseName = method.name
|
|
681
|
+
.replace(/ForSelf$/, "")
|
|
682
|
+
.replace(/ForDataOwner$/, "")
|
|
683
|
+
.replace(/ForDataOwnerInGroup$/, "");
|
|
684
|
+
if (!enrichedByBase.has(baseName)) {
|
|
685
|
+
enrichedByBase.set(baseName, method);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
for (const method of filter.methods) {
|
|
690
|
+
if (method.returnType)
|
|
691
|
+
continue; // Already enriched
|
|
692
|
+
const baseName = method.name
|
|
693
|
+
.replace(/ForSelf$/, "")
|
|
694
|
+
.replace(/ForDataOwner$/, "")
|
|
695
|
+
.replace(/ForDataOwnerInGroup$/, "");
|
|
696
|
+
const source = enrichedByBase.get(baseName);
|
|
697
|
+
if (source) {
|
|
698
|
+
method.description = source.description;
|
|
699
|
+
method.returnType = source.returnType;
|
|
700
|
+
method.sortable = source.sortable;
|
|
701
|
+
method.sortOrder = source.sortOrder;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function parseFilterTable(sectionBody) {
|
|
707
|
+
const rows = [];
|
|
708
|
+
const lines = sectionBody.split("\n");
|
|
709
|
+
for (const line of lines) {
|
|
710
|
+
const trimmed = line.trim();
|
|
711
|
+
if (!trimmed.startsWith("|"))
|
|
712
|
+
continue;
|
|
713
|
+
const cells = trimmed.split("|").map(c => c.trim()).filter(Boolean);
|
|
714
|
+
if (cells.length < 4)
|
|
715
|
+
continue;
|
|
716
|
+
if (cells[0].startsWith("-") || cells[0] === "Method")
|
|
717
|
+
continue;
|
|
718
|
+
const method = cells[0].replace(/`/g, "").trim();
|
|
719
|
+
const keyParams = cells[1].trim();
|
|
720
|
+
const returnType = cells[2].replace(/`/g, "").trim();
|
|
721
|
+
const sortable = cells[3].trim().toLowerCase() === "yes";
|
|
722
|
+
const sortOrder = cells.length > 4 ? cells[4].trim() : "—";
|
|
723
|
+
if (method) {
|
|
724
|
+
rows.push({ method, keyParams, returnType, sortable, sortOrder: sortOrder === "—" ? "" : sortOrder });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return rows;
|
|
728
|
+
}
|
|
729
|
+
function extractFilterReference(sections) {
|
|
730
|
+
const filterSection = sections.get("sdk/explanations/everything-about-filters.mdx");
|
|
731
|
+
if (!filterSection)
|
|
732
|
+
return "";
|
|
733
|
+
const body = filterSection.body;
|
|
734
|
+
const entityRefIdx = body.indexOf("## Entity filter reference");
|
|
735
|
+
if (entityRefIdx === -1)
|
|
736
|
+
return filterToTypescript(body);
|
|
737
|
+
return filterToTypescript(body.substring(0, entityRefIdx).trim());
|
|
738
|
+
}
|
|
739
|
+
// =============================================================================
|
|
740
|
+
// Main
|
|
741
|
+
// =============================================================================
|
|
742
|
+
function main() {
|
|
743
|
+
console.log("Extracting documentation...");
|
|
744
|
+
console.log(" Cardinal SDK root:", CARDINAL_SDK_ROOT);
|
|
745
|
+
console.log(" NPM SDK path:", NPM_SDK);
|
|
746
|
+
console.log(" SDK.md path:", SDK_MD);
|
|
747
|
+
const sections = parseSdkMdSections();
|
|
748
|
+
console.log(` SDK.md sections: ${sections.size}`);
|
|
749
|
+
const apis = extractApis();
|
|
750
|
+
const models = extractModels();
|
|
751
|
+
const filters = extractFilters();
|
|
752
|
+
// Enrich from SDK.md
|
|
753
|
+
enrichApiDescriptions(apis, sections);
|
|
754
|
+
enrichModelFieldDescriptions(models, sections);
|
|
755
|
+
enrichFilterDescriptions(filters, sections);
|
|
756
|
+
const manifest = {
|
|
757
|
+
apis,
|
|
758
|
+
models,
|
|
759
|
+
filters,
|
|
760
|
+
tutorials: extractTutorials(sections),
|
|
761
|
+
guides: extractGuides(sections),
|
|
762
|
+
filterReference: extractFilterReference(sections),
|
|
763
|
+
};
|
|
764
|
+
console.log(` APIs: ${Object.keys(manifest.apis).length}`);
|
|
765
|
+
console.log(` Models: ${Object.keys(manifest.models).length}`);
|
|
766
|
+
console.log(` Filters: ${Object.keys(manifest.filters).length}`);
|
|
767
|
+
console.log(` Tutorials: ${manifest.tutorials.length}`);
|
|
768
|
+
console.log(` Guides: ${manifest.guides.length}`);
|
|
769
|
+
console.log(` Filter reference length: ${manifest.filterReference.length} chars`);
|
|
770
|
+
let modelsWithDesc = 0, fieldsWithDesc = 0, totalFields = 0;
|
|
771
|
+
for (const model of Object.values(manifest.models)) {
|
|
772
|
+
if (model.description)
|
|
773
|
+
modelsWithDesc++;
|
|
774
|
+
for (const field of model.fields) {
|
|
775
|
+
totalFields++;
|
|
776
|
+
if (field.description)
|
|
777
|
+
fieldsWithDesc++;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
console.log(` Models with descriptions: ${modelsWithDesc}/${Object.keys(manifest.models).length}`);
|
|
781
|
+
console.log(` Fields with descriptions: ${fieldsWithDesc}/${totalFields}`);
|
|
782
|
+
let filtersWithDesc = 0;
|
|
783
|
+
for (const filter of Object.values(manifest.filters)) {
|
|
784
|
+
for (const method of filter.methods) {
|
|
785
|
+
if (method.returnType)
|
|
786
|
+
filtersWithDesc++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
console.log(` Filter methods with enrichment: ${filtersWithDesc}`);
|
|
790
|
+
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
|
791
|
+
fs.writeFileSync(OUT_FILE, JSON.stringify(manifest, null, 2));
|
|
792
|
+
console.log(" Written to:", OUT_FILE);
|
|
793
|
+
}
|
|
794
|
+
main();
|
|
795
|
+
//# sourceMappingURL=extract-docs.js.map
|