@esheet/adapters 0.0.3 → 0.0.4-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/README.md +1 -1
- package/dist/fhir/fhir-adapter.d.ts +21 -0
- package/dist/fhir/fhir-adapter.d.ts.map +1 -0
- package/dist/fhir/fhir-adapter.js +564 -0
- package/dist/fhir/index.d.ts +5 -0
- package/dist/fhir/index.d.ts.map +1 -0
- package/dist/fhir/index.js +9 -0
- package/dist/fhir/types.d.ts +306 -0
- package/dist/fhir/types.d.ts.map +1 -0
- package/dist/fhir/types.js +4 -0
- package/dist/fhir/utils.d.ts +105 -0
- package/dist/fhir/utils.d.ts.map +1 -0
- package/dist/fhir/utils.js +440 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/lib/mcp.d.ts +2 -0
- package/dist/lib/mcp.d.ts.map +1 -1
- package/dist/lib/mcp.js +7 -0
- package/dist/lib/surveyjs-converter.d.ts +10 -0
- package/dist/lib/surveyjs-converter.d.ts.map +1 -1
- package/dist/lib/surveyjs-converter.js +26 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# Adapter
|
|
1
|
+
# Adapter Package
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormDefinition } from '@esheet/core';
|
|
2
|
+
import type { FhirQuestionnaire, FhirQuestionnaireResponse, FhirImportOptions, FhirExportOptions, ResponseImportOptions, ResponseExportOptions } from './types.js';
|
|
3
|
+
import { isFhirQuestionnaire, isFhirQuestionnaireResponse } from './utils.js';
|
|
4
|
+
export { isFhirQuestionnaire, isFhirQuestionnaireResponse };
|
|
5
|
+
/**
|
|
6
|
+
* Convert a FHIR R4 Questionnaire to an eSheet FormDefinition.
|
|
7
|
+
*/
|
|
8
|
+
export declare function importFromFhir(questionnaire: FhirQuestionnaire, options?: FhirImportOptions): FormDefinition;
|
|
9
|
+
/**
|
|
10
|
+
* Convert an eSheet FormDefinition to a FHIR R4 Questionnaire.
|
|
11
|
+
*/
|
|
12
|
+
export declare function exportToFhir(form: FormDefinition, options?: FhirExportOptions): FhirQuestionnaire;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a FHIR QuestionnaireResponse to an eSheet answer map.
|
|
15
|
+
*/
|
|
16
|
+
export declare function importResponseFromFhir(response: FhirQuestionnaireResponse, options?: ResponseImportOptions): Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Convert eSheet answers to a FHIR QuestionnaireResponse.
|
|
19
|
+
*/
|
|
20
|
+
export declare function exportResponseToFhir(form: FormDefinition, answers: Record<string, unknown>, options: ResponseExportOptions): FhirQuestionnaireResponse;
|
|
21
|
+
//# sourceMappingURL=fhir-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fhir-adapter.d.ts","sourceRoot":"","sources":["../../src/fhir/fhir-adapter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EACV,iBAAiB,EAEjB,yBAAyB,EAMzB,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EAKtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAc5B,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,CAAC;AAM5D;;GAEG;AACH,wBAAgB,cAAc,CAC5B,aAAa,EAAE,iBAAiB,EAChC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,cAAc,CA0BhB;AAiSD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,cAAc,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,iBAAiB,CAmCnB;AAsID;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,yBAAyB,EACnC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAuDD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,qBAAqB,GAC7B,yBAAyB,CAa3B"}
|
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// FHIR R4 Adapter - Bidirectional conversion
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { isFhirQuestionnaire, isFhirQuestionnaireResponse, mapFhirTypeToEsheet, mapEsheetTypeToFhir, convertAnswerOptionToFieldOption, convertOptionToFhirAnswerOption, mapFhirOperatorToEsheet, mapEsheetOperatorToFhir, mapEnableBehaviorToLogic, extractEnableWhenAnswer, getExtensionValue, createItemControlExtension, createSignatureRequiredExtension, generateUUID, FHIR_EXT, } from './utils.js';
|
|
5
|
+
// Re-export type guards
|
|
6
|
+
export { isFhirQuestionnaire, isFhirQuestionnaireResponse };
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Import: FHIR Questionnaire → FormDefinition
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Convert a FHIR R4 Questionnaire to an eSheet FormDefinition.
|
|
12
|
+
*/
|
|
13
|
+
export function importFromFhir(questionnaire, options) {
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const formId = options?.formId ?? questionnaire.id ?? questionnaire.name ?? generateUUID();
|
|
16
|
+
const form = {
|
|
17
|
+
id: formId,
|
|
18
|
+
title: questionnaire.title,
|
|
19
|
+
description: questionnaire.description,
|
|
20
|
+
fields: [],
|
|
21
|
+
_sourceData: extractFormMetadata(questionnaire, options),
|
|
22
|
+
};
|
|
23
|
+
if (questionnaire.item) {
|
|
24
|
+
form.fields = questionnaire.item.map((item, index) => convertItemToField(item, `item[${index}]`, warnings, options));
|
|
25
|
+
}
|
|
26
|
+
if (warnings.length > 0) {
|
|
27
|
+
const meta = form._sourceData;
|
|
28
|
+
meta._conversionWarnings =
|
|
29
|
+
warnings;
|
|
30
|
+
}
|
|
31
|
+
return form;
|
|
32
|
+
}
|
|
33
|
+
function extractFormMetadata(q, options) {
|
|
34
|
+
const meta = {};
|
|
35
|
+
const preserve = options?.preserveExtensions !== false;
|
|
36
|
+
if (q.url)
|
|
37
|
+
meta.url = q.url;
|
|
38
|
+
if (q.version)
|
|
39
|
+
meta.version = q.version;
|
|
40
|
+
if (q.name)
|
|
41
|
+
meta.name = q.name;
|
|
42
|
+
if (q.status)
|
|
43
|
+
meta.status = q.status;
|
|
44
|
+
if (q.publisher)
|
|
45
|
+
meta.publisher = q.publisher;
|
|
46
|
+
if (q.date)
|
|
47
|
+
meta.date = q.date;
|
|
48
|
+
if (q.subjectType)
|
|
49
|
+
meta.subjectType = q.subjectType;
|
|
50
|
+
if (q.derivedFrom)
|
|
51
|
+
meta.derivedFrom = q.derivedFrom;
|
|
52
|
+
if (q.code)
|
|
53
|
+
meta.code = q.code;
|
|
54
|
+
if (preserve && q.extension) {
|
|
55
|
+
meta.fhirExtensions =
|
|
56
|
+
q.extension;
|
|
57
|
+
}
|
|
58
|
+
return meta;
|
|
59
|
+
}
|
|
60
|
+
function convertItemToField(item, path, warnings, options) {
|
|
61
|
+
const typeResult = mapFhirTypeToEsheet(item.type, item);
|
|
62
|
+
const existingIds = new Set();
|
|
63
|
+
// Build base field
|
|
64
|
+
const base = {
|
|
65
|
+
id: item.linkId,
|
|
66
|
+
question: item.text,
|
|
67
|
+
required: item.required,
|
|
68
|
+
};
|
|
69
|
+
// Build _sourceData for round-trip
|
|
70
|
+
const fieldMeta = buildFieldMeta(item, typeResult, options);
|
|
71
|
+
const hasFieldMeta = Object.keys(fieldMeta).length > 0;
|
|
72
|
+
// Convert enableWhen to rules
|
|
73
|
+
const rules = convertEnableWhenToRules(item.enableWhen, item.enableBehavior);
|
|
74
|
+
const rulesSpread = rules.length > 0 ? { rules } : {};
|
|
75
|
+
// Handle warnings for lossy conversions
|
|
76
|
+
addConversionWarnings(item, path, typeResult, warnings);
|
|
77
|
+
// Build field based on type
|
|
78
|
+
switch (typeResult.fieldType) {
|
|
79
|
+
case 'section':
|
|
80
|
+
return buildSectionField(base, item, path, warnings, options, hasFieldMeta ? fieldMeta : undefined);
|
|
81
|
+
case 'radio':
|
|
82
|
+
case 'check':
|
|
83
|
+
case 'dropdown':
|
|
84
|
+
case 'multiselectdropdown':
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
...(hasFieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
88
|
+
...rulesSpread,
|
|
89
|
+
fieldType: typeResult.fieldType,
|
|
90
|
+
options: convertAnswerOptions(item.answerOption, existingIds),
|
|
91
|
+
};
|
|
92
|
+
case 'rating':
|
|
93
|
+
case 'slider':
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
...(hasFieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
97
|
+
...rulesSpread,
|
|
98
|
+
fieldType: typeResult.fieldType,
|
|
99
|
+
options: convertAnswerOptions(item.answerOption, existingIds),
|
|
100
|
+
};
|
|
101
|
+
case 'text':
|
|
102
|
+
case 'longtext':
|
|
103
|
+
return {
|
|
104
|
+
...base,
|
|
105
|
+
...(hasFieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
106
|
+
...rulesSpread,
|
|
107
|
+
fieldType: typeResult.fieldType,
|
|
108
|
+
...(typeResult.inputType ? { inputType: typeResult.inputType } : {}),
|
|
109
|
+
};
|
|
110
|
+
case 'display':
|
|
111
|
+
// Display fields have no question — FHIR item.text maps to content (the displayed text)
|
|
112
|
+
return {
|
|
113
|
+
id: base.id,
|
|
114
|
+
...(hasFieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
115
|
+
...rulesSpread,
|
|
116
|
+
fieldType: 'display',
|
|
117
|
+
content: item.text,
|
|
118
|
+
};
|
|
119
|
+
case 'boolean':
|
|
120
|
+
case 'signature':
|
|
121
|
+
case 'diagram':
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
...(hasFieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
125
|
+
...rulesSpread,
|
|
126
|
+
fieldType: typeResult.fieldType,
|
|
127
|
+
};
|
|
128
|
+
default:
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
...(hasFieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
132
|
+
...rulesSpread,
|
|
133
|
+
fieldType: 'text',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function buildSectionField(base, item, path, warnings, options, fieldMeta) {
|
|
138
|
+
const nestedFields = item.item
|
|
139
|
+
? item.item.map((child, i) => convertItemToField(child, `${path}.item[${i}]`, warnings, options))
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
...(fieldMeta ? { _sourceData: fieldMeta } : {}),
|
|
144
|
+
fieldType: 'section',
|
|
145
|
+
title: item.text,
|
|
146
|
+
fields: nestedFields,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function buildFieldMeta(item, typeResult, options) {
|
|
150
|
+
const meta = {};
|
|
151
|
+
const preserve = options?.preserveExtensions !== false;
|
|
152
|
+
if (item.definition)
|
|
153
|
+
meta.definition = item.definition;
|
|
154
|
+
if (item.code)
|
|
155
|
+
meta.code = item.code;
|
|
156
|
+
if (item.prefix)
|
|
157
|
+
meta.prefix = item.prefix;
|
|
158
|
+
if (item.readOnly)
|
|
159
|
+
meta.readOnly = item.readOnly;
|
|
160
|
+
if (item.repeats)
|
|
161
|
+
meta.repeats = item.repeats;
|
|
162
|
+
// Store original FHIR type for export
|
|
163
|
+
meta.fhirItemType = item.type;
|
|
164
|
+
// Extract validation extensions
|
|
165
|
+
const minVal = getExtensionValue(item.extension, FHIR_EXT.MIN_VALUE);
|
|
166
|
+
const maxVal = getExtensionValue(item.extension, FHIR_EXT.MAX_VALUE);
|
|
167
|
+
const minLen = getExtensionValue(item.extension, FHIR_EXT.MIN_LENGTH);
|
|
168
|
+
const regex = getExtensionValue(item.extension, FHIR_EXT.REGEX);
|
|
169
|
+
if (minVal !== undefined)
|
|
170
|
+
meta.minValue = minVal;
|
|
171
|
+
if (maxVal !== undefined)
|
|
172
|
+
meta.maxValue = maxVal;
|
|
173
|
+
if (minLen !== undefined)
|
|
174
|
+
meta.minLength = minLen;
|
|
175
|
+
if (regex !== undefined)
|
|
176
|
+
meta.regex = regex;
|
|
177
|
+
// Preserve unknown extensions
|
|
178
|
+
if (preserve && item.extension) {
|
|
179
|
+
meta.fhirExtensions =
|
|
180
|
+
item.extension;
|
|
181
|
+
}
|
|
182
|
+
// Preserve answerValueSet URL for client-side resolution
|
|
183
|
+
if (item.answerValueSet) {
|
|
184
|
+
meta.answerValueSet = item.answerValueSet;
|
|
185
|
+
}
|
|
186
|
+
return meta;
|
|
187
|
+
}
|
|
188
|
+
function convertAnswerOptions(answerOptions, existingIds) {
|
|
189
|
+
if (!answerOptions)
|
|
190
|
+
return [];
|
|
191
|
+
return answerOptions.map((opt, index) => convertAnswerOptionToFieldOption(opt, existingIds, index));
|
|
192
|
+
}
|
|
193
|
+
function convertEnableWhenToRules(enableWhen, enableBehavior) {
|
|
194
|
+
if (!enableWhen || enableWhen.length === 0)
|
|
195
|
+
return [];
|
|
196
|
+
const conditions = [];
|
|
197
|
+
for (const ew of enableWhen) {
|
|
198
|
+
const operator = mapFhirOperatorToEsheet(ew.operator, ew.answerBoolean);
|
|
199
|
+
if (!operator)
|
|
200
|
+
continue;
|
|
201
|
+
conditions.push({
|
|
202
|
+
conditionType: 'field',
|
|
203
|
+
targetId: ew.question,
|
|
204
|
+
operator,
|
|
205
|
+
expected: extractEnableWhenAnswer(ew),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (conditions.length === 0)
|
|
209
|
+
return [];
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
effect: 'visible',
|
|
213
|
+
logic: mapEnableBehaviorToLogic(enableBehavior),
|
|
214
|
+
conditions,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
function addConversionWarnings(item, path, typeResult, warnings) {
|
|
219
|
+
// Reference type warning
|
|
220
|
+
if (typeResult.subType === 'reference') {
|
|
221
|
+
warnings.push({
|
|
222
|
+
path,
|
|
223
|
+
code: 'UNSUPPORTED_TYPE',
|
|
224
|
+
message: `FHIR reference type converted to text field. Type safety lost.`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// Quantity type warning
|
|
228
|
+
if (typeResult.subType === 'quantity') {
|
|
229
|
+
warnings.push({
|
|
230
|
+
path,
|
|
231
|
+
code: 'UNSUPPORTED_TYPE',
|
|
232
|
+
message: `FHIR quantity type converted to number field. Unit choices lost.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// ValueSet reference (informational)
|
|
236
|
+
if (item.answerValueSet) {
|
|
237
|
+
warnings.push({
|
|
238
|
+
path,
|
|
239
|
+
code: 'VALUESET_NOT_EXPANDED',
|
|
240
|
+
severity: 'info',
|
|
241
|
+
message: `ValueSet "${item.answerValueSet}" requires expansion. URL preserved in _sourceData.answerValueSet for client-side $expand.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Export: FormDefinition → FHIR Questionnaire
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
/**
|
|
249
|
+
* Convert an eSheet FormDefinition to a FHIR R4 Questionnaire.
|
|
250
|
+
*/
|
|
251
|
+
export function exportToFhir(form, options) {
|
|
252
|
+
const sourceMeta = form._sourceData;
|
|
253
|
+
const questionnaire = {
|
|
254
|
+
resourceType: 'Questionnaire',
|
|
255
|
+
id: options?.resourceId ?? form.id,
|
|
256
|
+
url: options?.canonicalUrl
|
|
257
|
+
? `${options.canonicalUrl}/Questionnaire/${form.id}`
|
|
258
|
+
: sourceMeta?.url ?? `urn:uuid:${generateUUID()}`,
|
|
259
|
+
status: options?.status ?? sourceMeta?.status ?? 'draft',
|
|
260
|
+
title: form.title,
|
|
261
|
+
description: form.description,
|
|
262
|
+
...(sourceMeta?.version ? { version: sourceMeta.version } : {}),
|
|
263
|
+
...(options?.publisher ?? sourceMeta?.publisher
|
|
264
|
+
? { publisher: options?.publisher ?? sourceMeta?.publisher }
|
|
265
|
+
: {}),
|
|
266
|
+
...(sourceMeta?.date ? { date: sourceMeta.date } : {}),
|
|
267
|
+
item: form.fields.map(convertFieldToItem),
|
|
268
|
+
};
|
|
269
|
+
// DTR compliance
|
|
270
|
+
if (options?.dtrCompliant) {
|
|
271
|
+
questionnaire.subjectType = [
|
|
272
|
+
'Patient',
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
// Restore preserved extensions
|
|
276
|
+
if (sourceMeta?.fhirExtensions) {
|
|
277
|
+
questionnaire.extension = sourceMeta.fhirExtensions;
|
|
278
|
+
}
|
|
279
|
+
return questionnaire;
|
|
280
|
+
}
|
|
281
|
+
function convertFieldToItem(field) {
|
|
282
|
+
const fieldMeta = field._sourceData;
|
|
283
|
+
const inputType = 'inputType' in field ? field.inputType : undefined;
|
|
284
|
+
const typeResult = mapEsheetTypeToFhir(field.fieldType, inputType);
|
|
285
|
+
// For sections, use title if question is not set
|
|
286
|
+
const text = field.question ??
|
|
287
|
+
(field.fieldType === 'section' && 'title' in field
|
|
288
|
+
? field.title
|
|
289
|
+
: undefined);
|
|
290
|
+
const item = {
|
|
291
|
+
linkId: field.id,
|
|
292
|
+
text,
|
|
293
|
+
type: typeResult.type,
|
|
294
|
+
...(field.required ? { required: true } : {}),
|
|
295
|
+
...(typeResult.repeats ? { repeats: true } : {}),
|
|
296
|
+
...(fieldMeta?.readOnly ? { readOnly: true } : {}),
|
|
297
|
+
};
|
|
298
|
+
// Build extensions array
|
|
299
|
+
const extensions = [];
|
|
300
|
+
// Add itemControl extension
|
|
301
|
+
if (typeResult.itemControl) {
|
|
302
|
+
extensions.push(createItemControlExtension(typeResult.itemControl));
|
|
303
|
+
}
|
|
304
|
+
// Add signature extension for signature fields
|
|
305
|
+
if (field.fieldType === 'signature') {
|
|
306
|
+
extensions.push(createSignatureRequiredExtension());
|
|
307
|
+
}
|
|
308
|
+
// Add preserved extensions
|
|
309
|
+
if (fieldMeta?.fhirExtensions) {
|
|
310
|
+
// Filter out extensions we're generating ourselves
|
|
311
|
+
const preserved = fieldMeta.fhirExtensions.filter((e) => e.url !== FHIR_EXT.ITEM_CONTROL && e.url !== FHIR_EXT.SIGNATURE_REQUIRED);
|
|
312
|
+
extensions.push(...preserved);
|
|
313
|
+
}
|
|
314
|
+
if (extensions.length > 0) {
|
|
315
|
+
item.extension = extensions;
|
|
316
|
+
}
|
|
317
|
+
// Add answer options
|
|
318
|
+
if ('options' in field && field.options && field.options.length > 0) {
|
|
319
|
+
item.answerOption =
|
|
320
|
+
field.options.map(convertOptionToFhirAnswerOption);
|
|
321
|
+
}
|
|
322
|
+
// Add enableWhen for visibility rules
|
|
323
|
+
const enableWhen = convertRulesToEnableWhen(field.rules);
|
|
324
|
+
if (enableWhen.length > 0) {
|
|
325
|
+
item.enableWhen =
|
|
326
|
+
enableWhen;
|
|
327
|
+
if (enableWhen.length > 1) {
|
|
328
|
+
// Check original logic mode
|
|
329
|
+
const visibleRule = field.rules?.find((r) => r.effect === 'visible');
|
|
330
|
+
item.enableBehavior =
|
|
331
|
+
visibleRule?.logic === 'OR' ? 'any' : 'all';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Handle nested fields for sections
|
|
335
|
+
if (field.fieldType === 'section' && 'fields' in field && field.fields) {
|
|
336
|
+
item.item =
|
|
337
|
+
field.fields.map(convertFieldToItem);
|
|
338
|
+
}
|
|
339
|
+
// Restore definition, code, prefix from metadata
|
|
340
|
+
if (fieldMeta?.definition)
|
|
341
|
+
item.definition = fieldMeta.definition;
|
|
342
|
+
if (fieldMeta?.code)
|
|
343
|
+
item.code = fieldMeta.code;
|
|
344
|
+
if (fieldMeta?.prefix)
|
|
345
|
+
item.prefix = fieldMeta.prefix;
|
|
346
|
+
return item;
|
|
347
|
+
}
|
|
348
|
+
function convertRulesToEnableWhen(rules) {
|
|
349
|
+
if (!rules)
|
|
350
|
+
return [];
|
|
351
|
+
// Only convert 'visible' rules to enableWhen
|
|
352
|
+
const visibleRule = rules.find((r) => r.effect === 'visible');
|
|
353
|
+
if (!visibleRule)
|
|
354
|
+
return [];
|
|
355
|
+
return visibleRule.conditions
|
|
356
|
+
.map((cond) => {
|
|
357
|
+
if (cond.conditionType === 'expression')
|
|
358
|
+
return null;
|
|
359
|
+
if (!cond.targetId || !cond.operator)
|
|
360
|
+
return null;
|
|
361
|
+
const fhirOp = mapEsheetOperatorToFhir(cond.operator);
|
|
362
|
+
if (!fhirOp)
|
|
363
|
+
return null;
|
|
364
|
+
const ew = {
|
|
365
|
+
question: cond.targetId,
|
|
366
|
+
operator: fhirOp.operator,
|
|
367
|
+
};
|
|
368
|
+
// Set the answer value
|
|
369
|
+
if (fhirOp.answerBoolean !== undefined) {
|
|
370
|
+
ew.answerBoolean = fhirOp.answerBoolean;
|
|
371
|
+
}
|
|
372
|
+
else if (cond.expected !== undefined) {
|
|
373
|
+
// Determine answer type based on expected value
|
|
374
|
+
if (cond.expected === 'true' || cond.expected === 'false') {
|
|
375
|
+
ew.answerBoolean =
|
|
376
|
+
cond.expected === 'true';
|
|
377
|
+
}
|
|
378
|
+
else if (!isNaN(Number(cond.expected))) {
|
|
379
|
+
ew.answerDecimal = Number(cond.expected);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
ew.answerString = cond.expected;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return ew;
|
|
386
|
+
})
|
|
387
|
+
.filter((ew) => ew !== null);
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Import: FHIR QuestionnaireResponse → Answers
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
/**
|
|
393
|
+
* Convert a FHIR QuestionnaireResponse to an eSheet answer map.
|
|
394
|
+
*/
|
|
395
|
+
export function importResponseFromFhir(response, options) {
|
|
396
|
+
const answers = {};
|
|
397
|
+
if (response.item) {
|
|
398
|
+
collectResponseAnswers(response.item, answers);
|
|
399
|
+
}
|
|
400
|
+
return answers;
|
|
401
|
+
}
|
|
402
|
+
function collectResponseAnswers(items, answers) {
|
|
403
|
+
for (const item of items) {
|
|
404
|
+
if (item.answer && item.answer.length > 0) {
|
|
405
|
+
answers[item.linkId] = extractAnswerValue(item.answer);
|
|
406
|
+
}
|
|
407
|
+
// Recurse into nested items
|
|
408
|
+
if (item.item) {
|
|
409
|
+
collectResponseAnswers(item.item, answers);
|
|
410
|
+
}
|
|
411
|
+
// Also recurse into answer-nested items
|
|
412
|
+
for (const ans of item.answer ?? []) {
|
|
413
|
+
if (ans.item) {
|
|
414
|
+
collectResponseAnswers(ans.item, answers);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function extractAnswerValue(answers) {
|
|
420
|
+
// Multiple answers → array (for repeating items)
|
|
421
|
+
if (answers.length > 1) {
|
|
422
|
+
return answers.map(extractSingleAnswerValue);
|
|
423
|
+
}
|
|
424
|
+
return extractSingleAnswerValue(answers[0]);
|
|
425
|
+
}
|
|
426
|
+
function extractSingleAnswerValue(answer) {
|
|
427
|
+
if (answer.valueBoolean !== undefined)
|
|
428
|
+
return answer.valueBoolean;
|
|
429
|
+
if (answer.valueDecimal !== undefined)
|
|
430
|
+
return answer.valueDecimal;
|
|
431
|
+
if (answer.valueInteger !== undefined)
|
|
432
|
+
return answer.valueInteger;
|
|
433
|
+
if (answer.valueDate !== undefined)
|
|
434
|
+
return answer.valueDate;
|
|
435
|
+
if (answer.valueDateTime !== undefined)
|
|
436
|
+
return answer.valueDateTime;
|
|
437
|
+
if (answer.valueTime !== undefined)
|
|
438
|
+
return answer.valueTime;
|
|
439
|
+
if (answer.valueString !== undefined)
|
|
440
|
+
return answer.valueString;
|
|
441
|
+
if (answer.valueUri !== undefined)
|
|
442
|
+
return answer.valueUri;
|
|
443
|
+
if (answer.valueCoding)
|
|
444
|
+
return answer.valueCoding.code;
|
|
445
|
+
if (answer.valueQuantity)
|
|
446
|
+
return answer.valueQuantity.value;
|
|
447
|
+
if (answer.valueAttachment)
|
|
448
|
+
return answer.valueAttachment.data;
|
|
449
|
+
if (answer.valueReference)
|
|
450
|
+
return answer.valueReference.reference;
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Export: Answers → FHIR QuestionnaireResponse
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
/**
|
|
457
|
+
* Convert eSheet answers to a FHIR QuestionnaireResponse.
|
|
458
|
+
*/
|
|
459
|
+
export function exportResponseToFhir(form, answers, options) {
|
|
460
|
+
const response = {
|
|
461
|
+
resourceType: 'QuestionnaireResponse',
|
|
462
|
+
...(options.resourceId ? { id: options.resourceId } : {}),
|
|
463
|
+
questionnaire: options.questionnaireUrl,
|
|
464
|
+
status: options.status ?? 'completed',
|
|
465
|
+
...(options.subject ? { subject: options.subject } : {}),
|
|
466
|
+
...(options.author ? { author: options.author } : {}),
|
|
467
|
+
authored: new Date().toISOString(),
|
|
468
|
+
item: convertFieldsToResponseItems(form.fields, answers),
|
|
469
|
+
};
|
|
470
|
+
return response;
|
|
471
|
+
}
|
|
472
|
+
function convertFieldsToResponseItems(fields, answers) {
|
|
473
|
+
const items = [];
|
|
474
|
+
for (const field of fields) {
|
|
475
|
+
const item = convertFieldToResponseItem(field, answers);
|
|
476
|
+
if (item) {
|
|
477
|
+
items.push(item);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return items;
|
|
481
|
+
}
|
|
482
|
+
function convertFieldToResponseItem(field, answers) {
|
|
483
|
+
const value = answers[field.id];
|
|
484
|
+
// Handle sections recursively
|
|
485
|
+
if (field.fieldType === 'section' && 'fields' in field && field.fields) {
|
|
486
|
+
const nestedItems = convertFieldsToResponseItems(field.fields, answers);
|
|
487
|
+
if (nestedItems.length === 0)
|
|
488
|
+
return null;
|
|
489
|
+
return {
|
|
490
|
+
linkId: field.id,
|
|
491
|
+
text: field.question,
|
|
492
|
+
item: nestedItems,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// Skip fields with no answer
|
|
496
|
+
if (value === undefined || value === null || value === '') {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const item = {
|
|
500
|
+
linkId: field.id,
|
|
501
|
+
text: field.question,
|
|
502
|
+
answer: convertValueToAnswers(field, value),
|
|
503
|
+
};
|
|
504
|
+
return item;
|
|
505
|
+
}
|
|
506
|
+
function convertValueToAnswers(field, value) {
|
|
507
|
+
const inputType = 'inputType' in field ? field.inputType : undefined;
|
|
508
|
+
switch (field.fieldType) {
|
|
509
|
+
case 'text':
|
|
510
|
+
case 'longtext':
|
|
511
|
+
return [convertTextAnswer(value, inputType)];
|
|
512
|
+
case 'boolean':
|
|
513
|
+
return [{ valueBoolean: Boolean(value) }];
|
|
514
|
+
case 'radio':
|
|
515
|
+
case 'dropdown':
|
|
516
|
+
return [createCodingAnswer(field, value)];
|
|
517
|
+
case 'check':
|
|
518
|
+
case 'multiselectdropdown': {
|
|
519
|
+
const selected = Array.isArray(value) ? value : [value];
|
|
520
|
+
return selected.map((v) => createCodingAnswer(field, v));
|
|
521
|
+
}
|
|
522
|
+
case 'rating':
|
|
523
|
+
case 'slider':
|
|
524
|
+
return [{ valueInteger: Number(value) }];
|
|
525
|
+
case 'signature':
|
|
526
|
+
case 'diagram':
|
|
527
|
+
return [
|
|
528
|
+
{
|
|
529
|
+
valueAttachment: {
|
|
530
|
+
contentType: 'image/png',
|
|
531
|
+
data: value,
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
];
|
|
535
|
+
default:
|
|
536
|
+
return [{ valueString: String(value) }];
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function convertTextAnswer(value, inputType) {
|
|
540
|
+
switch (inputType) {
|
|
541
|
+
case 'number':
|
|
542
|
+
return { valueDecimal: Number(value) };
|
|
543
|
+
case 'date':
|
|
544
|
+
return { valueDate: value };
|
|
545
|
+
case 'datetime-local':
|
|
546
|
+
return { valueDateTime: value };
|
|
547
|
+
case 'time':
|
|
548
|
+
return { valueTime: value };
|
|
549
|
+
case 'url':
|
|
550
|
+
return { valueUri: value };
|
|
551
|
+
default:
|
|
552
|
+
return { valueString: String(value) };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function createCodingAnswer(field, selectedId) {
|
|
556
|
+
const options = 'options' in field ? field.options : undefined;
|
|
557
|
+
const option = options?.find((o) => o.id === selectedId);
|
|
558
|
+
return {
|
|
559
|
+
valueCoding: {
|
|
560
|
+
code: option?.value ?? selectedId,
|
|
561
|
+
display: option?.text,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { FhirCoding, FhirCodeableConcept, FhirReference, FhirIdentifier, FhirPeriod, FhirAttachment, FhirQuantity, FhirExpression, FhirExpressionLanguage, FhirExtension, FhirMeta, FhirContactDetail, FhirContactPoint, FhirUsageContext, FhirRange, FhirQuestionnaireStatus, FhirQuestionnaireItemType, FhirEnableWhenOperator, FhirEnableBehavior, FhirEnableWhen, FhirAnswerOption, FhirInitialValue, FhirQuestionnaireItem, FhirQuestionnaire, FhirResponseStatus, FhirResponseAnswer, FhirQuestionnaireResponseItem, FhirQuestionnaireResponse, FhirImportOptions, FhirExportOptions, ResponseImportOptions, ResponseExportOptions, ImportWarning, ImportWarningCode, ImportWarningSeverity, FhirFieldMeta, FhirFormMeta, } from './types.js';
|
|
2
|
+
export { isFhirQuestionnaire, isFhirQuestionnaireResponse } from './utils.js';
|
|
3
|
+
export { importFromFhir, exportToFhir, importResponseFromFhir, exportResponseToFhir, } from './fhir-adapter.js';
|
|
4
|
+
export { mapFhirTypeToEsheet, mapEsheetTypeToFhir, convertAnswerOptionToFieldOption, convertOptionToFhirAnswerOption, mapFhirOperatorToEsheet, mapEsheetOperatorToFhir, getExtensionValue, createItemControlExtension, FHIR_EXT, ITEM_CONTROL_SYSTEM, } from './utils.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fhir/index.ts"],"names":[],"mappings":"AAKA,YAAY,EAEV,UAAU,EACV,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,UAAU,EACV,cAAc,EACd,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,aAAa,EACb,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,EAET,uBAAuB,EACvB,yBAAyB,EACzB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,EAEjB,kBAAkB,EAClB,kBAAkB,EAClB,6BAA6B,EAC7B,yBAAyB,EAEzB,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EAErB,aAAa,EACb,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gCAAgC,EAChC,+BAA+B,EAC/B,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,0BAA0B,EAC1B,QAAQ,EACR,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// FHIR Adapter - Public API
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Type guards
|
|
5
|
+
export { isFhirQuestionnaire, isFhirQuestionnaireResponse } from './utils.js';
|
|
6
|
+
// Adapter functions
|
|
7
|
+
export { importFromFhir, exportToFhir, importResponseFromFhir, exportResponseToFhir, } from './fhir-adapter.js';
|
|
8
|
+
// Utility functions (for advanced users)
|
|
9
|
+
export { mapFhirTypeToEsheet, mapEsheetTypeToFhir, convertAnswerOptionToFieldOption, convertOptionToFhirAnswerOption, mapFhirOperatorToEsheet, mapEsheetOperatorToFhir, getExtensionValue, createItemControlExtension, FHIR_EXT, ITEM_CONTROL_SYSTEM, } from './utils.js';
|