@fhiron/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ const { postJson } = require('./http.js');
4
+ const errors = require('./errors.js');
5
+ const linter = require('./_linter/rules.js');
6
+ const pkg = require('../package.json');
7
+
8
+ const DEFAULT_BASE_URL = 'https://fhiron.cl';
9
+ const DEFAULT_TIMEOUT = 10_000;
10
+ const DEFAULT_LANGUAGE = 'es';
11
+
12
+ /**
13
+ * Cliente del SDK de Fhiron. Embebe la validación FHIR contra CL Core en tu
14
+ * aplicación: misma API, misma cuota y mismo motor que @fhiron/cli y
15
+ * @fhiron/mcp-connector.
16
+ *
17
+ * @example
18
+ * const { Fhiron } = require('@fhiron/sdk');
19
+ * const fhiron = new Fhiron({ apiKey: process.env.FHIRON_API_KEY });
20
+ * const result = await fhiron.validate(patient);
21
+ * if (!result.ok) result.errors.forEach(e => console.log(e.path, e.message));
22
+ */
23
+ class Fhiron {
24
+ /**
25
+ * @param {object} [options]
26
+ * @param {string} [options.apiKey] default: process.env.FHIRON_API_KEY
27
+ * @param {string} [options.baseUrl] default: https://fhiron.cl (o FHIRON_API_URL)
28
+ * @param {number} [options.timeout] ms, default 10000
29
+ * @param {string} [options.clCoreVersion] ej. "1.9.4" — fija la versión de CL Core
30
+ * @param {string} [options.language] "es" | "en", default "es"
31
+ */
32
+ constructor(options = {}) {
33
+ this.apiKey = options.apiKey || process.env.FHIRON_API_KEY || null;
34
+ this.baseUrl = stripTrailingSlash(
35
+ options.baseUrl || process.env.FHIRON_API_URL || DEFAULT_BASE_URL,
36
+ );
37
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
38
+ this.clCoreVersion = normalizeVersion(options.clCoreVersion) || null;
39
+ this.language = options.language || DEFAULT_LANGUAGE;
40
+ }
41
+
42
+ /**
43
+ * Valida un recurso FHIR contra CL Core en el motor remoto (HAPI + perfiles).
44
+ * Descuenta 1 de la cuota mensual del tenant. Funciona también con Bundles
45
+ * (basta con que el recurso tenga `resourceType: "Bundle"`).
46
+ *
47
+ * @param {object} resource recurso FHIR con `resourceType` en la raíz
48
+ * @param {object} [opts]
49
+ * @param {string} [opts.profile] "hl7.fhir.cl.clcore@1.9.4" o "1.9.4"
50
+ * @param {string} [opts.clCoreVersion] override puntual de la versión
51
+ * @param {number} [opts.timeout]
52
+ * @param {AbortSignal} [opts.signal] (reservado; el timeout interno aplica)
53
+ * @returns {Promise<ValidateResult>}
54
+ */
55
+ async validate(resource, opts = {}) {
56
+ if (!this.apiKey) {
57
+ throw new errors.FhironAuthError(
58
+ 'Falta la API key. Pásala a new Fhiron({ apiKey }) o exporta FHIRON_API_KEY.',
59
+ );
60
+ }
61
+ assertResource(resource);
62
+
63
+ const version =
64
+ normalizeVersion(opts.clCoreVersion) ||
65
+ versionFromProfile(opts.profile) ||
66
+ this.clCoreVersion;
67
+
68
+ const raw = await postJson(`${this.baseUrl}/api/validate`, {
69
+ apiKey: this.apiKey,
70
+ body: resource,
71
+ timeout: opts.timeout || this.timeout,
72
+ userAgent: `${pkg.name}/${pkg.version}`,
73
+ headers: {
74
+ 'Accept-Language': this.language,
75
+ ...(version ? { 'X-Fhiron-CL-Core-Version': version } : {}),
76
+ },
77
+ });
78
+
79
+ return normalizeResult(raw);
80
+ }
81
+
82
+ /**
83
+ * Lint estructural OFFLINE (no toca la red, no descuenta cuota). Usa el
84
+ * mismo linter embebido que el MCP y la CLI. Útil para feedback inmediato
85
+ * antes de gastar una validación remota.
86
+ *
87
+ * @param {object} resource
88
+ * @returns {Issue[]}
89
+ */
90
+ lint(resource) {
91
+ assertResource(resource, { allowMissingType: true });
92
+ const issues = linter.lintResource(resource) || [];
93
+ return issues.map(normalizeIssue);
94
+ }
95
+
96
+ /**
97
+ * Conveniencia: lint local + validación remota en un solo llamado.
98
+ * Devuelve el resultado remoto con los issues locales adjuntos en
99
+ * `localIssues` (no se duplican con los del servidor, que ya los incluye).
100
+ * @param {object} resource
101
+ * @param {object} [opts]
102
+ * @returns {Promise<ValidateResult & { localIssues: Issue[] }>}
103
+ */
104
+ async check(resource, opts = {}) {
105
+ const localIssues = this.lint(resource);
106
+ const result = await this.validate(resource, opts);
107
+ return { ...result, localIssues };
108
+ }
109
+ }
110
+
111
+ // ── Helpers ───────────────────────────────────────────────────────────────
112
+
113
+ function assertResource(resource, { allowMissingType = false } = {}) {
114
+ if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
115
+ throw new errors.FhironRequestError(
116
+ 'El recurso debe ser un objeto FHIR (no string, array ni null).',
117
+ );
118
+ }
119
+ if (!allowMissingType && typeof resource.resourceType !== 'string') {
120
+ throw new errors.FhironRequestError(
121
+ 'El recurso no tiene `resourceType` en la raíz. Todo recurso FHIR debe declararlo (ej. "Patient").',
122
+ );
123
+ }
124
+ }
125
+
126
+ function normalizeResult(raw) {
127
+ const data = raw && typeof raw === 'object' ? raw : {};
128
+ const issues = Array.isArray(data.issues) ? data.issues.map(normalizeIssue) : [];
129
+ const engineDegraded = data.engineDegraded === true;
130
+ const errorIssues = issues.filter((i) => i.severity === 'error');
131
+ // `valid` se deriva de los issues (locales + servidor), NO del flag crudo del
132
+ // API: ese flag solo cuenta errores de HAPI e ignora los del linter local,
133
+ // así que podía venir `valid:true` con un error en `issues`. Derivarlo aquí
134
+ // garantiza que valid/ok/errors sean siempre coherentes entre sí. El flag
135
+ // crudo del servidor sigue disponible en `raw.valid`.
136
+ const valid = errorIssues.length === 0;
137
+ return {
138
+ valid,
139
+ ok: valid && !engineDegraded,
140
+ engineDegraded,
141
+ resourceType: data.resourceType || null,
142
+ profile: data.profile || null,
143
+ issues,
144
+ errors: errorIssues,
145
+ warnings: issues.filter((i) => i.severity === 'warning'),
146
+ information: issues.filter((i) => i.severity === 'information' || i.severity === 'info'),
147
+ messages: {
148
+ errors: Array.isArray(data.errors) ? data.errors : [],
149
+ warnings: Array.isArray(data.warnings) ? data.warnings : [],
150
+ },
151
+ raw: data,
152
+ };
153
+ }
154
+
155
+ function normalizeIssue(i) {
156
+ if (!i || typeof i !== 'object') {
157
+ return { severity: 'error', code: 'unknown', path: '', message: String(i) };
158
+ }
159
+ return {
160
+ severity: i.severity || i.level || 'warning',
161
+ code: i.code || i.rule || 'unknown',
162
+ path: i.path || i.location || '',
163
+ message: i.message || i.text || '',
164
+ why: i.why,
165
+ profileUrl: i.profileUrl || i.profile,
166
+ docsUrl: i.docsUrl,
167
+ suggestion: i.suggestion || (i.quickFix && i.quickFix.title) || undefined,
168
+ quickFix: i.quickFix,
169
+ };
170
+ }
171
+
172
+ function versionFromProfile(profile) {
173
+ if (!profile || typeof profile !== 'string') return null;
174
+ const at = profile.lastIndexOf('@');
175
+ const candidate = at !== -1 ? profile.slice(at + 1) : profile;
176
+ return /^\d+\.\d+\.\d+$/.test(candidate) ? candidate : null;
177
+ }
178
+
179
+ function normalizeVersion(v) {
180
+ if (!v || typeof v !== 'string') return null;
181
+ return /^\d+\.\d+\.\d+$/.test(v) ? v : null;
182
+ }
183
+
184
+ function stripTrailingSlash(url) {
185
+ return typeof url === 'string' ? url.replace(/\/+$/, '') : url;
186
+ }
187
+
188
+ module.exports = {
189
+ Fhiron,
190
+ // Cliente por defecto opcional: usa FHIRON_API_KEY de la env.
191
+ ...errors,
192
+ versionFromProfile,
193
+ VERSION: pkg.version,
194
+ };
package/src/index.mjs ADDED
@@ -0,0 +1,16 @@
1
+ // ESM wrapper sobre el módulo CommonJS. Permite `import { Fhiron } from '@fhiron/sdk'`
2
+ // en proyectos ESM/TypeScript sin duplicar la lógica.
3
+ import mod from './index.js';
4
+
5
+ export const Fhiron = mod.Fhiron;
6
+ export const FhironError = mod.FhironError;
7
+ export const FhironAuthError = mod.FhironAuthError;
8
+ export const FhironQuotaError = mod.FhironQuotaError;
9
+ export const FhironRequestError = mod.FhironRequestError;
10
+ export const FhironServerError = mod.FhironServerError;
11
+ export const FhironTimeoutError = mod.FhironTimeoutError;
12
+ export const FhironNetworkError = mod.FhironNetworkError;
13
+ export const versionFromProfile = mod.versionFromProfile;
14
+ export const VERSION = mod.VERSION;
15
+
16
+ export default mod;