@atomic-ehr/fhir-canonical-manager 0.0.8 → 0.0.9
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/README.md +72 -0
- package/dist/cache/core.d.ts +7 -0
- package/dist/cache/core.d.ts.map +1 -0
- package/dist/cache/core.js +11 -0
- package/dist/cache/core.js.map +1 -0
- package/dist/cache/index.d.ts +4 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +4 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/persistence.d.ts +5 -0
- package/dist/cache/persistence.d.ts.map +1 -0
- package/dist/cache/persistence.js +25 -0
- package/dist/cache/persistence.js.map +1 -0
- package/dist/cache/validation.d.ts +2 -0
- package/dist/cache/validation.d.ts.map +1 -0
- package/dist/cache/validation.js +21 -0
- package/dist/cache/validation.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +8 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/searchparam.d.ts +2 -0
- package/dist/cli/searchparam.d.ts.map +1 -0
- package/dist/cli/searchparam.js +131 -0
- package/dist/cli/searchparam.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/fs/index.d.ts +2 -0
- package/dist/fs/index.d.ts.map +1 -0
- package/dist/fs/index.js +2 -0
- package/dist/fs/index.js.map +1 -0
- package/dist/fs/utils.d.ts +4 -0
- package/dist/fs/utils.d.ts.map +1 -0
- package/dist/fs/utils.js +23 -0
- package/dist/fs/utils.js.map +1 -0
- package/dist/index.d.ts +6 -111
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -592
- package/dist/index.js.map +1 -1
- package/dist/manager/canonical.d.ts +3 -0
- package/dist/manager/canonical.d.ts.map +1 -0
- package/dist/manager/canonical.js +194 -0
- package/dist/manager/canonical.js.map +1 -0
- package/dist/manager/index.d.ts +3 -0
- package/dist/manager/index.d.ts.map +1 -0
- package/dist/manager/index.js +3 -0
- package/dist/manager/index.js.map +1 -0
- package/dist/package/detector.d.ts +2 -0
- package/dist/package/detector.d.ts.map +1 -0
- package/dist/package/detector.js +19 -0
- package/dist/package/detector.js.map +1 -0
- package/dist/package/index.d.ts +3 -0
- package/dist/package/index.d.ts.map +1 -0
- package/dist/package/index.js +3 -0
- package/dist/package/index.js.map +1 -0
- package/dist/package/installer.d.ts +2 -0
- package/dist/package/installer.d.ts.map +1 -0
- package/dist/package/installer.js +55 -0
- package/dist/package/installer.js.map +1 -0
- package/dist/reference/index.d.ts +5 -0
- package/dist/reference/index.d.ts.map +1 -0
- package/dist/reference/index.js +4 -0
- package/dist/reference/index.js.map +1 -0
- package/dist/reference/manager.d.ts +10 -0
- package/dist/reference/manager.d.ts.map +1 -0
- package/dist/reference/manager.js +36 -0
- package/dist/reference/manager.js.map +1 -0
- package/dist/reference/store.d.ts +6 -0
- package/dist/reference/store.d.ts.map +1 -0
- package/dist/reference/store.js +6 -0
- package/dist/reference/store.js.map +1 -0
- package/dist/resolver/context.d.ts +4 -0
- package/dist/resolver/context.d.ts.map +1 -0
- package/dist/resolver/context.js +14 -0
- package/dist/resolver/context.js.map +1 -0
- package/dist/resolver/index.d.ts +2 -0
- package/dist/resolver/index.d.ts.map +1 -0
- package/dist/resolver/index.js +2 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/scanner/directory.d.ts +3 -0
- package/dist/scanner/directory.d.ts.map +1 -0
- package/dist/scanner/directory.js +33 -0
- package/dist/scanner/directory.js.map +1 -0
- package/dist/scanner/index.d.ts +5 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +5 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/package.d.ts +3 -0
- package/dist/scanner/package.d.ts.map +1 -0
- package/dist/scanner/package.js +26 -0
- package/dist/scanner/package.js.map +1 -0
- package/dist/scanner/parser.d.ts +5 -0
- package/dist/scanner/parser.d.ts.map +1 -0
- package/dist/scanner/parser.js +39 -0
- package/dist/scanner/parser.js.map +1 -0
- package/dist/scanner/processor.d.ts +4 -0
- package/dist/scanner/processor.d.ts.map +1 -0
- package/dist/scanner/processor.js +53 -0
- package/dist/scanner/processor.js.map +1 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +3 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/smart.d.ts +9 -0
- package/dist/search/smart.d.ts.map +1 -0
- package/dist/search/smart.js +31 -0
- package/dist/search/smart.js.map +1 -0
- package/dist/search/terms.d.ts +2 -0
- package/dist/search/terms.d.ts.map +1 -0
- package/dist/search/terms.js +19 -0
- package/dist/search/terms.js.map +1 -0
- package/dist/types/core.d.ts +91 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/core.js +2 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/internal.d.ts +48 -0
- package/dist/types/internal.d.ts.map +1 -0
- package/dist/types/internal.js +2 -0
- package/dist/types/internal.js.map +1 -0
- package/package.json +1 -1
- package/src/cache/core.ts +20 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/persistence.ts +39 -0
- package/src/cache/validation.ts +27 -0
- package/src/cli/index.ts +8 -4
- package/src/cli/searchparam.ts +162 -0
- package/src/constants.ts +6 -0
- package/src/fs/index.ts +5 -0
- package/src/fs/utils.ts +28 -0
- package/src/index.ts +13 -990
- package/src/manager/canonical.ts +338 -0
- package/src/manager/index.ts +8 -0
- package/src/package/detector.ts +24 -0
- package/src/package/index.ts +6 -0
- package/src/package/installer.ts +77 -0
- package/src/reference/index.ts +10 -0
- package/src/reference/manager.ts +51 -0
- package/src/reference/store.ts +15 -0
- package/src/resolver/context.ts +25 -0
- package/src/resolver/index.ts +5 -0
- package/src/scanner/directory.ts +42 -0
- package/src/scanner/index.ts +8 -0
- package/src/scanner/package.ts +38 -0
- package/src/scanner/parser.ts +42 -0
- package/src/scanner/processor.ts +69 -0
- package/src/search/index.ts +6 -0
- package/src/search/smart.ts +57 -0
- package/src/search/terms.ts +22 -0
- package/src/types/core.ts +117 -0
- package/src/types/index.ts +6 -0
- package/src/types/internal.ts +57 -0
package/src/index.ts
CHANGED
|
@@ -1,998 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FHIR Canonical Manager -
|
|
2
|
+
* FHIR Canonical Manager - Modular Implementation
|
|
3
3
|
* A package manager for FHIR resources with canonical URL resolution
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { createHash } from "crypto";
|
|
9
|
-
import { exec } from "child_process";
|
|
10
|
-
import { promisify } from "util";
|
|
6
|
+
// Re-export all public types
|
|
7
|
+
export * from './types';
|
|
11
8
|
|
|
12
|
-
//
|
|
13
|
-
|
|
9
|
+
// Re-export reference management
|
|
10
|
+
export { createReferenceManager, ReferenceManagerFactory as ReferenceManager } from './reference';
|
|
11
|
+
export type { ReferenceManager as ReferenceManagerType } from './reference';
|
|
14
12
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
exitCode: number;
|
|
18
|
-
stdout: string;
|
|
19
|
-
stderr: string;
|
|
13
|
+
// Re-export main CanonicalManager
|
|
14
|
+
export { CanonicalManager, createCanonicalManager } from './manager';
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
exitCode: number,
|
|
24
|
-
stdout: string,
|
|
25
|
-
stderr: string,
|
|
26
|
-
) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "ShellError";
|
|
29
|
-
this.exitCode = exitCode;
|
|
30
|
-
this.stdout = stdout;
|
|
31
|
-
this.stderr = stderr;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
16
|
+
// Type export for CanonicalManager (the interface is already exported from types)
|
|
17
|
+
// CanonicalManager function is exported from manager as a factory function
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
exitCode: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ShellPromise extends Promise<ShellResult> {
|
|
42
|
-
quiet(): Promise<ShellResult>;
|
|
43
|
-
env(variables: Record<string, string>): ShellPromise;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Shell command execution using standard Node.js child_process
|
|
48
|
-
*/
|
|
49
|
-
function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
|
|
50
|
-
const command = strings.reduce((acc, str, i) => {
|
|
51
|
-
return acc + str + (values[i] || "");
|
|
52
|
-
}, "");
|
|
53
|
-
|
|
54
|
-
let envVars: Record<string, string> = {};
|
|
55
|
-
|
|
56
|
-
// Always use Node.js child_process for consistent behavior
|
|
57
|
-
const execute = async (options: { quiet?: boolean } = {}) => {
|
|
58
|
-
try {
|
|
59
|
-
const execOptions: any = {
|
|
60
|
-
shell: true,
|
|
61
|
-
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Apply environment variables if set
|
|
65
|
-
if (Object.keys(envVars).length > 0) {
|
|
66
|
-
execOptions.env = { ...process.env, ...envVars };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const { stdout, stderr } = await execAsync(command, execOptions);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
stdout: stdout?.toString() || "",
|
|
73
|
-
stderr: stderr?.toString() || "",
|
|
74
|
-
exitCode: 0,
|
|
75
|
-
};
|
|
76
|
-
} catch (error: any) {
|
|
77
|
-
// Extract error details
|
|
78
|
-
const code = error.code || 1;
|
|
79
|
-
const stdout = error.stdout?.toString() || "";
|
|
80
|
-
const stderr = error.stderr?.toString() || error.message || "";
|
|
81
|
-
|
|
82
|
-
throw new ShellError(`Command failed: ${command}`, code, stdout, stderr);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Create a lazy promise that only executes when awaited
|
|
87
|
-
let executed = false;
|
|
88
|
-
let resultPromise: Promise<ShellResult> | null = null;
|
|
89
|
-
|
|
90
|
-
const lazyPromise = {
|
|
91
|
-
then(onFulfilled?: any, onRejected?: any) {
|
|
92
|
-
if (!executed) {
|
|
93
|
-
executed = true;
|
|
94
|
-
resultPromise = execute();
|
|
95
|
-
}
|
|
96
|
-
return resultPromise!.then(onFulfilled, onRejected);
|
|
97
|
-
},
|
|
98
|
-
catch(onRejected?: any) {
|
|
99
|
-
return this.then(undefined, onRejected);
|
|
100
|
-
},
|
|
101
|
-
finally(onFinally?: any) {
|
|
102
|
-
return this.then(
|
|
103
|
-
(value: any) => {
|
|
104
|
-
onFinally?.();
|
|
105
|
-
return value;
|
|
106
|
-
},
|
|
107
|
-
(reason: any) => {
|
|
108
|
-
onFinally?.();
|
|
109
|
-
throw reason;
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
},
|
|
113
|
-
quiet() {
|
|
114
|
-
return execute({ quiet: true });
|
|
115
|
-
},
|
|
116
|
-
env(vars: Record<string, string>) {
|
|
117
|
-
envVars = vars;
|
|
118
|
-
return this;
|
|
119
|
-
},
|
|
120
|
-
// Add Symbol.toStringTag to satisfy Promise interface
|
|
121
|
-
[Symbol.toStringTag]: "ShellPromise" as const,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return lazyPromise as any as ShellPromise;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Detect available package manager
|
|
129
|
-
*/
|
|
130
|
-
async function detectPackageManager(): Promise<"bun" | "npm" | null> {
|
|
131
|
-
try {
|
|
132
|
-
// Check for bun first
|
|
133
|
-
await $`bun --version`.quiet();
|
|
134
|
-
return "bun";
|
|
135
|
-
} catch {
|
|
136
|
-
try {
|
|
137
|
-
// Fall back to npm
|
|
138
|
-
await $`npm --version`.quiet();
|
|
139
|
-
return "npm";
|
|
140
|
-
} catch {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Export shell utilities for testing
|
|
147
|
-
export { ShellError, detectPackageManager };
|
|
148
|
-
|
|
149
|
-
// Types
|
|
150
|
-
export interface Reference {
|
|
151
|
-
id: string;
|
|
152
|
-
resourceType: string;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface PackageId {
|
|
156
|
-
name: string;
|
|
157
|
-
version: string;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface IndexEntry extends Reference {
|
|
161
|
-
indexVersion: number;
|
|
162
|
-
kind?: string;
|
|
163
|
-
url?: string;
|
|
164
|
-
type?: string;
|
|
165
|
-
version?: string;
|
|
166
|
-
package?: PackageId;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface Resource extends Reference {
|
|
170
|
-
url?: string;
|
|
171
|
-
version?: string;
|
|
172
|
-
[key: string]: any;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export interface SourceContext {
|
|
176
|
-
id?: string;
|
|
177
|
-
package?: PackageId;
|
|
178
|
-
url?: string;
|
|
179
|
-
path?: string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export interface Config {
|
|
183
|
-
packages: string[];
|
|
184
|
-
workingDir: string;
|
|
185
|
-
registry?: string;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export interface CanonicalManager {
|
|
189
|
-
init(): Promise<void>;
|
|
190
|
-
destroy(): Promise<void>;
|
|
191
|
-
packages(): Promise<PackageId[]>;
|
|
192
|
-
resolveEntry(
|
|
193
|
-
canonicalUrl: string,
|
|
194
|
-
options?: {
|
|
195
|
-
package?: string;
|
|
196
|
-
version?: string;
|
|
197
|
-
sourceContext?: SourceContext;
|
|
198
|
-
},
|
|
199
|
-
): Promise<IndexEntry>;
|
|
200
|
-
resolve(
|
|
201
|
-
canonicalUrl: string,
|
|
202
|
-
options?: {
|
|
203
|
-
package?: string;
|
|
204
|
-
version?: string;
|
|
205
|
-
sourceContext?: SourceContext;
|
|
206
|
-
},
|
|
207
|
-
): Promise<Resource>;
|
|
208
|
-
read(reference: Reference): Promise<Resource>;
|
|
209
|
-
searchEntries(params: {
|
|
210
|
-
kind?: string;
|
|
211
|
-
url?: string;
|
|
212
|
-
type?: string;
|
|
213
|
-
version?: string;
|
|
214
|
-
package?: PackageId;
|
|
215
|
-
}): Promise<IndexEntry[]>;
|
|
216
|
-
search(params: {
|
|
217
|
-
kind?: string;
|
|
218
|
-
url?: string;
|
|
219
|
-
type?: string;
|
|
220
|
-
version?: string;
|
|
221
|
-
package?: PackageId;
|
|
222
|
-
}): Promise<Resource[]>;
|
|
223
|
-
smartSearch(
|
|
224
|
-
searchTerms: string[],
|
|
225
|
-
filters?: {
|
|
226
|
-
resourceType?: string;
|
|
227
|
-
type?: string;
|
|
228
|
-
kind?: string;
|
|
229
|
-
package?: PackageId;
|
|
230
|
-
}
|
|
231
|
-
): Promise<IndexEntry[]>;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Internal types
|
|
235
|
-
interface IndexFile {
|
|
236
|
-
"index-version": number;
|
|
237
|
-
files: IndexFileEntry[];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
interface IndexFileEntry {
|
|
241
|
-
filename: string;
|
|
242
|
-
resourceType: string;
|
|
243
|
-
id: string;
|
|
244
|
-
url?: string;
|
|
245
|
-
version?: string;
|
|
246
|
-
kind?: string;
|
|
247
|
-
type?: string;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
interface PackageJson {
|
|
251
|
-
name: string;
|
|
252
|
-
version: string;
|
|
253
|
-
fhirVersions?: string[];
|
|
254
|
-
type?: string;
|
|
255
|
-
canonical?: string;
|
|
256
|
-
dependencies?: Record<string, string>;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export interface PackageInfo {
|
|
260
|
-
id: PackageId;
|
|
261
|
-
path: string;
|
|
262
|
-
canonical?: string;
|
|
263
|
-
fhirVersions?: string[];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
interface ReferenceMetadata {
|
|
267
|
-
packageName: string;
|
|
268
|
-
packageVersion: string;
|
|
269
|
-
filePath: string;
|
|
270
|
-
resourceType: string;
|
|
271
|
-
url?: string;
|
|
272
|
-
version?: string;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
interface IndexCache {
|
|
276
|
-
entries: Record<string, IndexEntry[]>;
|
|
277
|
-
packages: Record<string, PackageInfo>;
|
|
278
|
-
references: Record<string, ReferenceMetadata>;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
interface CacheData {
|
|
282
|
-
entries: Record<string, IndexEntry[]>;
|
|
283
|
-
packages: Record<string, PackageInfo>;
|
|
284
|
-
references: Record<string, ReferenceMetadata>;
|
|
285
|
-
packageLockHash?: string; // Hash of package-lock.json to detect changes
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Reference management functions
|
|
289
|
-
export interface ReferenceStore {
|
|
290
|
-
get(id: string): ReferenceMetadata | undefined;
|
|
291
|
-
set(id: string, metadata: ReferenceMetadata): void;
|
|
292
|
-
has(id: string): boolean;
|
|
293
|
-
clear(): void;
|
|
294
|
-
size(): number;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export type { ReferenceMetadata };
|
|
298
|
-
|
|
299
|
-
const generateReferenceId = (metadata: {
|
|
300
|
-
packageName: string;
|
|
301
|
-
packageVersion: string;
|
|
302
|
-
filePath: string;
|
|
303
|
-
}): string => {
|
|
304
|
-
const input = `${metadata.packageName}@${metadata.packageVersion}:${metadata.filePath}`;
|
|
305
|
-
return createHash("sha256").update(input).digest("base64url");
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
export const ReferenceManager = (): ReferenceStore & {
|
|
309
|
-
generateId: typeof generateReferenceId;
|
|
310
|
-
getIdsByUrl: (url: string) => string[];
|
|
311
|
-
createReference: (id: string, metadata: ReferenceMetadata) => Reference;
|
|
312
|
-
getAllReferences: () => Record<string, ReferenceMetadata>;
|
|
313
|
-
} => {
|
|
314
|
-
const references: Record<string, ReferenceMetadata> = {};
|
|
315
|
-
const urlToIds: Record<string, string[]> = {};
|
|
316
|
-
|
|
317
|
-
const set = (id: string, metadata: ReferenceMetadata): void => {
|
|
318
|
-
references[id] = metadata;
|
|
319
|
-
if (metadata.url) {
|
|
320
|
-
if (!urlToIds[metadata.url]) {
|
|
321
|
-
urlToIds[metadata.url] = [];
|
|
322
|
-
}
|
|
323
|
-
const ids = urlToIds[metadata.url];
|
|
324
|
-
if (ids && !ids.includes(id)) {
|
|
325
|
-
ids.push(id);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const clear = (): void => {
|
|
331
|
-
Object.keys(references).forEach((key) => delete references[key]);
|
|
332
|
-
Object.keys(urlToIds).forEach((key) => delete urlToIds[key]);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
generateId: generateReferenceId,
|
|
337
|
-
get: (id: string) => references[id],
|
|
338
|
-
set,
|
|
339
|
-
has: (id: string) => id in references,
|
|
340
|
-
clear,
|
|
341
|
-
size: () => Object.keys(references).length,
|
|
342
|
-
getIdsByUrl: (url: string) => urlToIds[url] || [],
|
|
343
|
-
createReference: (id: string, metadata: ReferenceMetadata): Reference => ({
|
|
344
|
-
id,
|
|
345
|
-
resourceType: metadata.resourceType,
|
|
346
|
-
}),
|
|
347
|
-
getAllReferences: () => references,
|
|
348
|
-
};
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Parser functions
|
|
352
|
-
const isValidFileEntry = (entry: any): boolean => {
|
|
353
|
-
if (!entry || typeof entry !== "object") return false;
|
|
354
|
-
if (!entry.filename || typeof entry.filename !== "string") return false;
|
|
355
|
-
if (!entry.resourceType || typeof entry.resourceType !== "string")
|
|
356
|
-
return false;
|
|
357
|
-
if (!entry.id || typeof entry.id !== "string") return false;
|
|
358
|
-
|
|
359
|
-
const optionalStringFields = ["url", "version", "kind", "type"];
|
|
360
|
-
for (const field of optionalStringFields) {
|
|
361
|
-
if (entry[field] !== undefined && typeof entry[field] !== "string") {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return true;
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
const isValidIndexFile = (data: any): boolean => {
|
|
370
|
-
if (!data || typeof data !== "object") return false;
|
|
371
|
-
if (!data["index-version"] || typeof data["index-version"] !== "number")
|
|
372
|
-
return false;
|
|
373
|
-
if (!Array.isArray(data.files)) return false;
|
|
374
|
-
return data.files.every((file: any) => isValidFileEntry(file));
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const parseIndex = (content: string, filePath: string): IndexFile | null => {
|
|
378
|
-
try {
|
|
379
|
-
const data = JSON.parse(content);
|
|
380
|
-
if (!isValidIndexFile(data)) {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
return data as IndexFile;
|
|
384
|
-
} catch {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
// File system utilities
|
|
390
|
-
const fileExists = async (filePath: string): Promise<boolean> => {
|
|
391
|
-
try {
|
|
392
|
-
await fs.access(filePath);
|
|
393
|
-
return true;
|
|
394
|
-
} catch {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const ensureDir = async (dirPath: string): Promise<void> => {
|
|
400
|
-
try {
|
|
401
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
402
|
-
} catch {
|
|
403
|
-
// Ignore errors
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const isFhirPackage = async (dirPath: string): Promise<boolean> => {
|
|
408
|
-
const indexPath = path.join(dirPath, ".index.json");
|
|
409
|
-
return fileExists(indexPath);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Cache creation
|
|
413
|
-
const createCache = (): IndexCache & {
|
|
414
|
-
referenceManager: ReturnType<typeof ReferenceManager>;
|
|
415
|
-
} => {
|
|
416
|
-
const referenceManager = ReferenceManager();
|
|
417
|
-
return {
|
|
418
|
-
entries: {},
|
|
419
|
-
packages: {},
|
|
420
|
-
references: {},
|
|
421
|
-
referenceManager,
|
|
422
|
-
};
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
// Compute hash of package-lock.json or bun.lock for cache validation
|
|
426
|
-
const computePackageLockHash = async (
|
|
427
|
-
workingDir: string,
|
|
428
|
-
): Promise<string | null> => {
|
|
429
|
-
try {
|
|
430
|
-
// Try package-lock.json first
|
|
431
|
-
const packageLockPath = path.join(workingDir, "package-lock.json");
|
|
432
|
-
try {
|
|
433
|
-
const content = await fs.readFile(packageLockPath, "utf-8");
|
|
434
|
-
return createHash("sha256").update(content).digest("hex");
|
|
435
|
-
} catch {
|
|
436
|
-
// Try bun.lock if package-lock.json doesn't exist
|
|
437
|
-
const bunLockPath = path.join(workingDir, "bun.lock");
|
|
438
|
-
const content = await fs.readFile(bunLockPath, "utf-8");
|
|
439
|
-
return createHash("sha256").update(content).digest("hex");
|
|
440
|
-
}
|
|
441
|
-
} catch {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
// Cache persistence functions
|
|
447
|
-
const saveCacheToDisk = async (
|
|
448
|
-
cache: ReturnType<typeof createCache>,
|
|
449
|
-
cacheDir: string,
|
|
450
|
-
workingDir: string,
|
|
451
|
-
): Promise<void> => {
|
|
452
|
-
const packageLockHash = await computePackageLockHash(workingDir);
|
|
453
|
-
|
|
454
|
-
const cacheData: CacheData = {
|
|
455
|
-
entries: cache.entries,
|
|
456
|
-
packages: cache.packages,
|
|
457
|
-
references: cache.referenceManager.getAllReferences(),
|
|
458
|
-
packageLockHash: packageLockHash || undefined,
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const cachePath = path.join(cacheDir, "index.json");
|
|
462
|
-
await fs.writeFile(cachePath, JSON.stringify(cacheData, null, 2));
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
const loadCacheFromDisk = async (
|
|
466
|
-
cacheDir: string,
|
|
467
|
-
): Promise<CacheData | null> => {
|
|
468
|
-
try {
|
|
469
|
-
const cachePath = path.join(cacheDir, "index.json");
|
|
470
|
-
const content = await fs.readFile(cachePath, "utf-8");
|
|
471
|
-
return JSON.parse(content) as CacheData;
|
|
472
|
-
} catch {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
// Package management functions
|
|
478
|
-
const installPackages = async (
|
|
479
|
-
packages: string[],
|
|
480
|
-
workingDir: string,
|
|
481
|
-
registry?: string,
|
|
482
|
-
): Promise<void> => {
|
|
483
|
-
await ensureDir(workingDir);
|
|
484
|
-
|
|
485
|
-
// Check if package.json exists
|
|
486
|
-
const packageJsonPath = path.join(workingDir, "package.json");
|
|
487
|
-
if (!(await fileExists(packageJsonPath))) {
|
|
488
|
-
// Create minimal package.json
|
|
489
|
-
const minimalPackageJson = {
|
|
490
|
-
name: "fhir-canonical-manager-workspace",
|
|
491
|
-
version: "1.0.0",
|
|
492
|
-
private: true,
|
|
493
|
-
dependencies: {},
|
|
494
|
-
};
|
|
495
|
-
await fs.writeFile(
|
|
496
|
-
packageJsonPath,
|
|
497
|
-
JSON.stringify(minimalPackageJson, null, 2),
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Detect available package manager
|
|
502
|
-
const packageManager = await detectPackageManager();
|
|
503
|
-
if (!packageManager) {
|
|
504
|
-
throw new Error("No package manager found. Please install npm or bun.");
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Install packages
|
|
508
|
-
for (const pkg of packages) {
|
|
509
|
-
try {
|
|
510
|
-
if (packageManager === "bun") {
|
|
511
|
-
// Use bun with auth bypass trick for FHIR registry
|
|
512
|
-
const env = {
|
|
513
|
-
HOME: workingDir, // Prevent reading user's .npmrc
|
|
514
|
-
NPM_CONFIG_USERCONFIG: "/dev/null", // Extra safety
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const cmd = registry
|
|
518
|
-
? `cd ${workingDir} && bun add ${pkg} --registry ${registry}`
|
|
519
|
-
: `cd ${workingDir} && bun add ${pkg}`;
|
|
520
|
-
|
|
521
|
-
await $`${cmd}`.env(env);
|
|
522
|
-
} else {
|
|
523
|
-
// Use npm (handles auth correctly)
|
|
524
|
-
const cmd = registry
|
|
525
|
-
? `cd ${workingDir} && npm add ${pkg} --registry ${registry}`
|
|
526
|
-
: `cd ${workingDir} && npm add ${pkg}`;
|
|
527
|
-
|
|
528
|
-
await $`${cmd}`;
|
|
529
|
-
}
|
|
530
|
-
} catch (err) {
|
|
531
|
-
console.error(`Failed to install package ${pkg}:`, err);
|
|
532
|
-
throw err;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
// Package processing functions
|
|
538
|
-
const processIndex = async (
|
|
539
|
-
basePath: string,
|
|
540
|
-
packageJson: PackageJson,
|
|
541
|
-
cache: ReturnType<typeof createCache>,
|
|
542
|
-
): Promise<void> => {
|
|
543
|
-
const indexPath = path.join(basePath, ".index.json");
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const indexContent = await fs.readFile(indexPath, "utf-8");
|
|
547
|
-
const index = parseIndex(indexContent, indexPath);
|
|
548
|
-
|
|
549
|
-
if (!index) return;
|
|
550
|
-
|
|
551
|
-
for (const file of index.files) {
|
|
552
|
-
if (!file.url) continue;
|
|
553
|
-
|
|
554
|
-
const filePath = path.join(basePath, file.filename);
|
|
555
|
-
|
|
556
|
-
const id = cache.referenceManager.generateId({
|
|
557
|
-
packageName: packageJson.name,
|
|
558
|
-
packageVersion: packageJson.version,
|
|
559
|
-
filePath,
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
cache.referenceManager.set(id, {
|
|
563
|
-
packageName: packageJson.name,
|
|
564
|
-
packageVersion: packageJson.version,
|
|
565
|
-
filePath,
|
|
566
|
-
resourceType: file.resourceType,
|
|
567
|
-
url: file.url,
|
|
568
|
-
version: file.version,
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
const entry: IndexEntry = {
|
|
572
|
-
id,
|
|
573
|
-
resourceType: file.resourceType,
|
|
574
|
-
indexVersion: index["index-version"],
|
|
575
|
-
url: file.url,
|
|
576
|
-
version: file.version,
|
|
577
|
-
kind: file.kind,
|
|
578
|
-
type: file.type,
|
|
579
|
-
package: {
|
|
580
|
-
name: packageJson.name,
|
|
581
|
-
version: packageJson.version,
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
if (!cache.entries[file.url]) {
|
|
586
|
-
cache.entries[file.url] = [];
|
|
587
|
-
}
|
|
588
|
-
const entries = cache.entries[file.url];
|
|
589
|
-
if (entries) {
|
|
590
|
-
entries.push(entry);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
} catch {
|
|
594
|
-
// Silently ignore index processing errors
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
const scanPackage = async (
|
|
599
|
-
packagePath: string,
|
|
600
|
-
cache: ReturnType<typeof createCache>,
|
|
601
|
-
): Promise<void> => {
|
|
602
|
-
try {
|
|
603
|
-
const packageJsonPath = path.join(packagePath, "package.json");
|
|
604
|
-
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
605
|
-
const packageJson: PackageJson = JSON.parse(packageJsonContent);
|
|
606
|
-
|
|
607
|
-
const packageInfo: PackageInfo = {
|
|
608
|
-
id: { name: packageJson.name, version: packageJson.version },
|
|
609
|
-
path: packagePath,
|
|
610
|
-
canonical: packageJson.canonical,
|
|
611
|
-
fhirVersions: packageJson.fhirVersions,
|
|
612
|
-
};
|
|
613
|
-
cache.packages[packageJson.name] = packageInfo;
|
|
614
|
-
|
|
615
|
-
await processIndex(packagePath, packageJson, cache);
|
|
616
|
-
|
|
617
|
-
const examplesPath = path.join(packagePath, "examples");
|
|
618
|
-
if (await fileExists(path.join(examplesPath, ".index.json"))) {
|
|
619
|
-
await processIndex(examplesPath, packageJson, cache);
|
|
620
|
-
}
|
|
621
|
-
} catch {
|
|
622
|
-
// Silently ignore package scan errors
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
const scanDirectory = async (
|
|
627
|
-
dirPath: string,
|
|
628
|
-
cache: ReturnType<typeof createCache>,
|
|
629
|
-
): Promise<void> => {
|
|
630
|
-
try {
|
|
631
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
632
|
-
|
|
633
|
-
for (const entry of entries) {
|
|
634
|
-
if (!entry.isDirectory()) continue;
|
|
635
|
-
|
|
636
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
637
|
-
|
|
638
|
-
if (entry.name.startsWith("@")) {
|
|
639
|
-
const scopedEntries = await fs.readdir(fullPath, {
|
|
640
|
-
withFileTypes: true,
|
|
641
|
-
});
|
|
642
|
-
for (const scopedEntry of scopedEntries) {
|
|
643
|
-
if (!scopedEntry.isDirectory()) continue;
|
|
644
|
-
|
|
645
|
-
const scopedPath = path.join(fullPath, scopedEntry.name);
|
|
646
|
-
if (await isFhirPackage(scopedPath)) {
|
|
647
|
-
await scanPackage(scopedPath, cache);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
} else if (await isFhirPackage(fullPath)) {
|
|
651
|
-
await scanPackage(fullPath, cache);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
} catch {
|
|
655
|
-
// Silently ignore scan errors
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
// Resolution functions
|
|
660
|
-
const resolveWithContext = async (
|
|
661
|
-
url: string,
|
|
662
|
-
context: SourceContext,
|
|
663
|
-
cache: ReturnType<typeof createCache>,
|
|
664
|
-
resolveEntry: (url: string, options?: any) => Promise<IndexEntry>,
|
|
665
|
-
): Promise<IndexEntry | null> => {
|
|
666
|
-
if (context.package) {
|
|
667
|
-
try {
|
|
668
|
-
return await resolveEntry(url, {
|
|
669
|
-
package: context.package.name,
|
|
670
|
-
version: context.package.version,
|
|
671
|
-
});
|
|
672
|
-
} catch {
|
|
673
|
-
// Fall through to global resolution
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
return null;
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
// Default FHIR package registry
|
|
680
|
-
const DEFAULT_REGISTRY = "https://fs.get-ig.org/pkgs/";
|
|
681
|
-
|
|
682
|
-
// Main implementation
|
|
683
|
-
export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
684
|
-
const { packages, workingDir } = config;
|
|
685
|
-
// Ensure registry URL ends with /
|
|
686
|
-
const registry = config.registry
|
|
687
|
-
? config.registry.endsWith('/') ? config.registry : `${config.registry}/`
|
|
688
|
-
: DEFAULT_REGISTRY;
|
|
689
|
-
const nodeModulesPath = path.join(workingDir, "node_modules");
|
|
690
|
-
const cacheDir = path.join(workingDir, ".fcm", "cache");
|
|
691
|
-
|
|
692
|
-
let cache = createCache();
|
|
693
|
-
let initialized = false;
|
|
694
|
-
|
|
695
|
-
const ensureInitialized = (): void => {
|
|
696
|
-
if (!initialized) {
|
|
697
|
-
throw new Error("CanonicalManager not initialized. Call init() first.");
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
const init = async (): Promise<void> => {
|
|
702
|
-
if (initialized) return;
|
|
703
|
-
|
|
704
|
-
// Ensure directories exist
|
|
705
|
-
await ensureDir(workingDir);
|
|
706
|
-
await ensureDir(cacheDir);
|
|
707
|
-
|
|
708
|
-
// Get current package-lock.json hash
|
|
709
|
-
const currentPackageLockHash = await computePackageLockHash(workingDir);
|
|
710
|
-
|
|
711
|
-
// Try to load cache from disk
|
|
712
|
-
const cachedData = await loadCacheFromDisk(cacheDir);
|
|
713
|
-
|
|
714
|
-
// Check if cache is valid (exists and package-lock.json hasn't changed)
|
|
715
|
-
const cacheValid =
|
|
716
|
-
cachedData &&
|
|
717
|
-
cachedData.packageLockHash === currentPackageLockHash &&
|
|
718
|
-
currentPackageLockHash !== null;
|
|
719
|
-
|
|
720
|
-
if (cacheValid) {
|
|
721
|
-
// Restore cache from disk
|
|
722
|
-
cache.entries = cachedData.entries;
|
|
723
|
-
cache.packages = cachedData.packages;
|
|
724
|
-
Object.entries(cachedData.references).forEach(([id, metadata]) => {
|
|
725
|
-
cache.referenceManager.set(id, metadata);
|
|
726
|
-
});
|
|
727
|
-
} else {
|
|
728
|
-
// Cache is invalid or doesn't exist - rebuild it
|
|
729
|
-
if (cachedData && cachedData.packageLockHash !== currentPackageLockHash) {
|
|
730
|
-
console.log("Package dependencies have changed, rebuilding index...");
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Install packages if needed
|
|
734
|
-
await installPackages(packages, workingDir, registry);
|
|
735
|
-
|
|
736
|
-
// Clear cache before scanning
|
|
737
|
-
cache = createCache();
|
|
738
|
-
|
|
739
|
-
// Scan installed packages
|
|
740
|
-
await scanDirectory(nodeModulesPath, cache);
|
|
741
|
-
|
|
742
|
-
// Save cache to disk with current package-lock hash
|
|
743
|
-
await saveCacheToDisk(cache, cacheDir, workingDir);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
initialized = true;
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
const destroy = async (): Promise<void> => {
|
|
750
|
-
cache.entries = {};
|
|
751
|
-
cache.packages = {};
|
|
752
|
-
cache.referenceManager.clear();
|
|
753
|
-
initialized = false;
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
const getPackages = async (): Promise<PackageId[]> => {
|
|
757
|
-
ensureInitialized();
|
|
758
|
-
return Object.values(cache.packages).map((p) => p.id);
|
|
759
|
-
};
|
|
760
|
-
|
|
761
|
-
const resolveEntry = async (
|
|
762
|
-
canonicalUrl: string,
|
|
763
|
-
options?: {
|
|
764
|
-
package?: string;
|
|
765
|
-
version?: string;
|
|
766
|
-
sourceContext?: SourceContext;
|
|
767
|
-
},
|
|
768
|
-
): Promise<IndexEntry> => {
|
|
769
|
-
ensureInitialized();
|
|
770
|
-
|
|
771
|
-
if (options?.sourceContext) {
|
|
772
|
-
const contextResolved = await resolveWithContext(
|
|
773
|
-
canonicalUrl,
|
|
774
|
-
options.sourceContext,
|
|
775
|
-
cache,
|
|
776
|
-
resolveEntry,
|
|
777
|
-
);
|
|
778
|
-
if (contextResolved) {
|
|
779
|
-
return contextResolved;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const entries = cache.entries[canonicalUrl] || [];
|
|
784
|
-
|
|
785
|
-
if (entries.length === 0) {
|
|
786
|
-
throw new Error(`Cannot resolve canonical URL: ${canonicalUrl}`);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
let filtered = [...entries];
|
|
790
|
-
|
|
791
|
-
if (options?.package) {
|
|
792
|
-
filtered = filtered.filter((e) => e.package?.name === options.package);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (options?.version) {
|
|
796
|
-
filtered = filtered.filter((e) => e.version === options.version);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (filtered.length === 0) {
|
|
800
|
-
throw new Error(
|
|
801
|
-
`No matching resource found for ${canonicalUrl} with given options`,
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
return filtered[0]!;
|
|
806
|
-
};
|
|
807
|
-
|
|
808
|
-
const resolve = async (
|
|
809
|
-
canonicalUrl: string,
|
|
810
|
-
options?: {
|
|
811
|
-
package?: string;
|
|
812
|
-
version?: string;
|
|
813
|
-
sourceContext?: SourceContext;
|
|
814
|
-
},
|
|
815
|
-
): Promise<Resource> => {
|
|
816
|
-
const entry = await resolveEntry(canonicalUrl, options);
|
|
817
|
-
return read(entry);
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
const read = async (reference: Reference): Promise<Resource> => {
|
|
821
|
-
ensureInitialized();
|
|
822
|
-
|
|
823
|
-
const metadata = cache.referenceManager.get(reference.id);
|
|
824
|
-
if (!metadata) {
|
|
825
|
-
throw new Error(`Invalid reference ID: ${reference.id}`);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
try {
|
|
829
|
-
const content = await fs.readFile(metadata.filePath, "utf-8");
|
|
830
|
-
const resource = JSON.parse(content);
|
|
831
|
-
|
|
832
|
-
return {
|
|
833
|
-
...resource,
|
|
834
|
-
id: reference.id,
|
|
835
|
-
resourceType: reference.resourceType,
|
|
836
|
-
};
|
|
837
|
-
} catch (err) {
|
|
838
|
-
throw new Error(`Failed to read resource: ${err}`);
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
const searchEntries = async (params: {
|
|
843
|
-
kind?: string;
|
|
844
|
-
url?: string;
|
|
845
|
-
type?: string;
|
|
846
|
-
version?: string;
|
|
847
|
-
package?: PackageId;
|
|
848
|
-
}): Promise<IndexEntry[]> => {
|
|
849
|
-
ensureInitialized();
|
|
850
|
-
|
|
851
|
-
let results: IndexEntry[] = [];
|
|
852
|
-
|
|
853
|
-
if (params.url) {
|
|
854
|
-
results = cache.entries[params.url] || [];
|
|
855
|
-
} else {
|
|
856
|
-
for (const entries of Object.values(cache.entries)) {
|
|
857
|
-
results.push(...entries);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (params.kind !== undefined) {
|
|
862
|
-
results = results.filter((e) => e.kind === params.kind);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (params.type !== undefined) {
|
|
866
|
-
results = results.filter((e) => e.type === params.type);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if (params.version !== undefined) {
|
|
870
|
-
results = results.filter((e) => e.version === params.version);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
if (params.package) {
|
|
874
|
-
const pkg = params.package;
|
|
875
|
-
results = results.filter(
|
|
876
|
-
(e) =>
|
|
877
|
-
e.package?.name === pkg.name && e.package?.version === pkg.version,
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
return results;
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
const search = async (params: {
|
|
885
|
-
kind?: string;
|
|
886
|
-
url?: string;
|
|
887
|
-
type?: string;
|
|
888
|
-
version?: string;
|
|
889
|
-
package?: PackageId;
|
|
890
|
-
}): Promise<Resource[]> => {
|
|
891
|
-
const entries = await searchEntries(params);
|
|
892
|
-
const resources = await Promise.all(entries.map((entry) => read(entry)));
|
|
893
|
-
return resources;
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
const smartSearch = async (
|
|
897
|
-
searchTerms: string[],
|
|
898
|
-
filters?: {
|
|
899
|
-
resourceType?: string;
|
|
900
|
-
type?: string;
|
|
901
|
-
kind?: string;
|
|
902
|
-
package?: PackageId;
|
|
903
|
-
}
|
|
904
|
-
): Promise<IndexEntry[]> => {
|
|
905
|
-
ensureInitialized();
|
|
906
|
-
|
|
907
|
-
// Start with base search using filters
|
|
908
|
-
let results = await searchEntries({
|
|
909
|
-
kind: filters?.kind,
|
|
910
|
-
package: filters?.package,
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
// Apply resourceType filter
|
|
914
|
-
if (filters?.resourceType) {
|
|
915
|
-
results = results.filter(entry => entry.resourceType === filters.resourceType);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// Apply type filter
|
|
919
|
-
if (filters?.type) {
|
|
920
|
-
results = results.filter(entry => entry.type === filters.type);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// Apply smart search terms if provided
|
|
924
|
-
if (searchTerms.length > 0) {
|
|
925
|
-
const terms = searchTerms.map(t => t.toLowerCase());
|
|
926
|
-
|
|
927
|
-
results = results.filter(entry => {
|
|
928
|
-
if (!entry.url) return false;
|
|
929
|
-
const urlLower = entry.url.toLowerCase();
|
|
930
|
-
|
|
931
|
-
// Also check type and resourceType for matching
|
|
932
|
-
const fullText = [
|
|
933
|
-
urlLower,
|
|
934
|
-
entry.type?.toLowerCase() || '',
|
|
935
|
-
entry.resourceType?.toLowerCase() || ''
|
|
936
|
-
].join(' ');
|
|
937
|
-
|
|
938
|
-
// Check if all search terms match
|
|
939
|
-
return terms.every(term => {
|
|
940
|
-
// Split the text into parts (by /, -, _, ., spaces)
|
|
941
|
-
const allParts = fullText.split(/[\/\-_\.\s]+/);
|
|
942
|
-
|
|
943
|
-
// Check if any part starts with the search term
|
|
944
|
-
const directMatch = allParts.some(part => part.startsWith(term));
|
|
945
|
-
if (directMatch) return true;
|
|
946
|
-
|
|
947
|
-
// Smart matching for common abbreviations
|
|
948
|
-
const expandedTerms: Record<string, string[]> = {
|
|
949
|
-
'str': ['structure'],
|
|
950
|
-
'struct': ['structure'],
|
|
951
|
-
'def': ['definition'],
|
|
952
|
-
'pati': ['patient'],
|
|
953
|
-
'obs': ['observation'],
|
|
954
|
-
'org': ['organization'],
|
|
955
|
-
'pract': ['practitioner'],
|
|
956
|
-
'med': ['medication', 'medicinal'],
|
|
957
|
-
'req': ['request'],
|
|
958
|
-
'resp': ['response'],
|
|
959
|
-
'ref': ['reference'],
|
|
960
|
-
'val': ['value'],
|
|
961
|
-
'code': ['codesystem', 'code'],
|
|
962
|
-
'cs': ['codesystem'],
|
|
963
|
-
'vs': ['valueset'],
|
|
964
|
-
'sd': ['structuredefinition'],
|
|
965
|
-
};
|
|
966
|
-
|
|
967
|
-
// Check if the term is an abbreviation
|
|
968
|
-
const expansions = expandedTerms[term] || [];
|
|
969
|
-
for (const expansion of expansions) {
|
|
970
|
-
if (allParts.some(part => part.startsWith(expansion))) {
|
|
971
|
-
return true;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// If still no match, check if the term appears anywhere (substring match)
|
|
976
|
-
return fullText.includes(term);
|
|
977
|
-
});
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
return results;
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
return {
|
|
985
|
-
init,
|
|
986
|
-
destroy,
|
|
987
|
-
packages: getPackages,
|
|
988
|
-
resolveEntry,
|
|
989
|
-
resolve,
|
|
990
|
-
read,
|
|
991
|
-
searchEntries,
|
|
992
|
-
search,
|
|
993
|
-
smartSearch,
|
|
994
|
-
};
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
// Default export
|
|
998
|
-
export default CanonicalManager;
|
|
19
|
+
// Default export for backward compatibility
|
|
20
|
+
import { createCanonicalManager } from './manager';
|
|
21
|
+
export default createCanonicalManager;
|