@petrarca/sonnet-core 0.8.0 → 0.9.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/README.md +18 -5
- package/dist/{chunk-XAEHYRXC.js → chunk-LOWDCP73.js} +1 -1
- package/dist/chunk-ZM3U3HR2.js +98 -0
- package/dist/chunk-ZM3U3HR2.js.map +1 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/entityOptions/index.d.ts +2 -2
- package/dist/hooks/index.d.ts +100 -3
- package/dist/hooks/index.js +123 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +35 -11
- package/dist/index.js.map +1 -1
- package/dist/lazyTree-C0oThxCS.d.ts +139 -0
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +13 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/schema/index.d.ts +106 -407
- package/dist/schema/index.js +408 -41
- package/dist/schema/index.js.map +1 -1
- package/dist/search/index.d.ts +18 -1
- package/dist/search/index.js +26 -1
- package/dist/search/index.js.map +1 -1
- package/dist/terminology/index.d.ts +161 -0
- package/dist/terminology/index.js +178 -0
- package/dist/terminology/index.js.map +1 -0
- package/dist/{types-DJ4hb97y.d.ts → types-BiFVnfqr.d.ts} +1 -1
- package/dist/types-HhzIKd1R.d.ts +582 -0
- package/package.json +18 -10
- /package/dist/{chunk-XAEHYRXC.js.map → chunk-LOWDCP73.js.map} +0 -0
package/dist/search/index.d.ts
CHANGED
|
@@ -59,6 +59,16 @@ declare const EMPTY_FILTER: FilterExpr;
|
|
|
59
59
|
* Ignores the boolean structure (AND / OR / NOT / GROUP).
|
|
60
60
|
*/
|
|
61
61
|
declare function collectTerms(expr: FilterExpr): FilterTerm[];
|
|
62
|
+
/**
|
|
63
|
+
* The value of the first term with `key`, or undefined -- for a facet used once.
|
|
64
|
+
* Ignores the boolean structure, like collectTerms.
|
|
65
|
+
*/
|
|
66
|
+
declare function termValue(expr: FilterExpr, key: string): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The values of every term with `key`, in order -- for a repeatable facet.
|
|
69
|
+
* Ignores the boolean structure, like collectTerms.
|
|
70
|
+
*/
|
|
71
|
+
declare function termValues(expr: FilterExpr, key: string): string[];
|
|
62
72
|
/**
|
|
63
73
|
* Collect all term nodes from a FilterExpr tree as flat key/value pairs.
|
|
64
74
|
*
|
|
@@ -115,5 +125,12 @@ declare function termsToFilterExpr(terms: {
|
|
|
115
125
|
key: string;
|
|
116
126
|
value: string;
|
|
117
127
|
}[]): FilterExpr;
|
|
128
|
+
/**
|
|
129
|
+
* A free text as a filter: the text in any of the fields -- one term per
|
|
130
|
+
* field, OR-ed -- or, without fields, the server's own `search` term. Empty
|
|
131
|
+
* text is no filter. What a search box sends for what is typed in it.
|
|
132
|
+
*/
|
|
133
|
+
declare function textSearchExpr(text: string, fields?: string[]): FilterExpr;
|
|
118
134
|
|
|
119
|
-
export { EMPTY_FILTER,
|
|
135
|
+
export { EMPTY_FILTER, buildListParams, collectTerms, flattenFilterExpr, isEmptyFilterExpr, serializeFilterExpr, termValue, termValues, termsToFilterExpr, textSearchExpr };
|
|
136
|
+
export type { FilterAnd, FilterExpr, FilterGroup, FilterNot, FilterOr, FilterTerm };
|
package/dist/search/index.js
CHANGED
|
@@ -22,6 +22,12 @@ function collectTermNodes(expr, out) {
|
|
|
22
22
|
break;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
function termValue(expr, key) {
|
|
26
|
+
return collectTerms(expr).find((t) => t.key === key)?.value;
|
|
27
|
+
}
|
|
28
|
+
function termValues(expr, key) {
|
|
29
|
+
return collectTerms(expr).filter((t) => t.key === key).map((t) => t.value);
|
|
30
|
+
}
|
|
25
31
|
function flattenFilterExpr(expr) {
|
|
26
32
|
return collectTerms(expr).map(({ key, value }) => ({ key, value }));
|
|
27
33
|
}
|
|
@@ -70,6 +76,22 @@ function termsToFilterExpr(terms) {
|
|
|
70
76
|
}))
|
|
71
77
|
};
|
|
72
78
|
}
|
|
79
|
+
function textSearchExpr(text, fields) {
|
|
80
|
+
const trimmed = text.trim();
|
|
81
|
+
if (!trimmed) return { type: "and", children: [] };
|
|
82
|
+
if (!fields?.length)
|
|
83
|
+
return {
|
|
84
|
+
type: "and",
|
|
85
|
+
children: [{ type: "term", key: "search", value: trimmed }]
|
|
86
|
+
};
|
|
87
|
+
const terms = fields.map(
|
|
88
|
+
(key) => ({ type: "term", key, value: trimmed })
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
type: "and",
|
|
92
|
+
children: terms.length === 1 ? terms : [{ type: "or", children: terms }]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
73
95
|
export {
|
|
74
96
|
EMPTY_FILTER,
|
|
75
97
|
buildListParams,
|
|
@@ -77,6 +99,9 @@ export {
|
|
|
77
99
|
flattenFilterExpr,
|
|
78
100
|
isEmptyFilterExpr,
|
|
79
101
|
serializeFilterExpr,
|
|
80
|
-
|
|
102
|
+
termValue,
|
|
103
|
+
termValues,
|
|
104
|
+
termsToFilterExpr,
|
|
105
|
+
textSearchExpr
|
|
81
106
|
};
|
|
82
107
|
//# sourceMappingURL=index.js.map
|
package/dist/search/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/search/types.ts","../../src/search/filterExpr.ts"],"sourcesContent":["/**\n * FilterExpr -- lightweight boolean expression tree for search and filtering.\n *\n * Designed to start simple (a flat AND list of terms) and grow to support\n * full boolean queries (AND, OR, NOT, grouping) without breaking callers.\n *\n * The component always emits a FilterExpr. Callers that talk to a flat\n * server API today use flattenFilterExpr() to extract the terms. When the\n * server gains AST support, callers drop flattenFilterExpr and pass the\n * expression directly -- no component changes required.\n *\n * Extractable to @petrarca/sonnet-core: no app-specific imports.\n */\n\n/** A single field/value filter term -- the leaf node of a FilterExpr. */\nexport interface FilterTerm {\n type: \"term\";\n /** Schema property key -- used as the query param name sent to the server. */\n key: string;\n /** The filter value. */\n value: string;\n}\n\n/** Conjunction: all children must match. */\nexport interface FilterAnd {\n type: \"and\";\n children: FilterExpr[];\n}\n\n/** Disjunction: at least one child must match. */\nexport interface FilterOr {\n type: \"or\";\n children: FilterExpr[];\n}\n\n/** Negation: child must not match. */\nexport interface FilterNot {\n type: \"not\";\n child: FilterExpr;\n}\n\n/**\n * Explicit grouping (parentheses). Semantically identical to its child --\n * used to preserve user intent when round-tripping the expression.\n */\nexport interface FilterGroup {\n type: \"group\";\n child: FilterExpr;\n}\n\n/** The full expression type. */\nexport type FilterExpr =\n | FilterTerm\n | FilterAnd\n | FilterOr\n | FilterNot\n | FilterGroup;\n\n/** An empty expression -- no filters active. */\nexport const EMPTY_FILTER: FilterExpr = { type: \"and\", children: [] };\n","/**\n * Utilities for working with FilterExpr trees.\n *\n * Extractable to @petrarca/sonnet-core: no app-specific imports.\n */\n\nimport type { FilterExpr, FilterTerm } from \"./types\";\n\n/**\n * Return all term nodes from the expression as FilterTerm objects, depth-first.\n *\n * Ignores the boolean structure (AND / OR / NOT / GROUP).\n */\nexport function collectTerms(expr: FilterExpr): FilterTerm[] {\n const out: FilterTerm[] = [];\n collectTermNodes(expr, out);\n return out;\n}\n\nfunction collectTermNodes(expr: FilterExpr, out: FilterTerm[]): void {\n switch (expr.type) {\n case \"term\":\n out.push(expr);\n break;\n case \"and\":\n case \"or\":\n for (const child of expr.children) collectTermNodes(child, out);\n break;\n case \"not\":\n case \"group\":\n collectTermNodes(expr.child, out);\n break;\n }\n}\n\n/**\n * Collect all term nodes from a FilterExpr tree as flat key/value pairs.\n *\n * Use this when talking to a server API that accepts only flat key/value\n * filter params. When the server gains AST support, pass the raw FilterExpr\n * instead and drop this call.\n *\n * @example\n * const terms = flattenFilterExpr(expr);\n * // [{ key: \"name\", value: \"acme\" }, { key: \"status\", value: \"active\" }]\n * const params = Object.fromEntries(terms.map(t => [t.key, t.value]));\n */\nexport function flattenFilterExpr(\n expr: FilterExpr,\n): { key: string; value: string }[] {\n return collectTerms(expr).map(({ key, value }) => ({ key, value }));\n}\n\n/**\n * Return true when the expression contains no term nodes.\n */\nexport function isEmptyFilterExpr(expr: FilterExpr): boolean {\n return collectTerms(expr).length === 0;\n}\n\n/**\n * Check whether a FilterExpr is a flat AND of terms only (no OR/NOT/Group).\n * When true, the expression can be serialized as simple key=value params.\n * When false, it must be serialized as a `q=` expression string.\n */\nfunction isFlatAnd(expr: FilterExpr): boolean {\n if (expr.type === \"term\") return true;\n if (expr.type === \"and\") return expr.children.every((c) => c.type === \"term\");\n return false;\n}\n\n/**\n * Serialize a FilterExpr to the server's q= expression syntax.\n *\n * Produces strings like `code ~ \"aspirin\" OR display ~ \"aspirin\"`.\n * All terms use the `~` (LIKE / substring) operator -- the FilterTerm\n * type carries no operator field; callers that need exact-match or other\n * operators should construct the q= string directly.\n */\nexport function serializeFilterExpr(expr: FilterExpr): string {\n switch (expr.type) {\n case \"term\": {\n const escaped = expr.value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n return `${expr.key} ~ \"${escaped}\"`;\n }\n case \"and\":\n return expr.children.map((c) => serializeFilterExpr(c)).join(\" AND \");\n case \"or\":\n return expr.children.map((c) => serializeFilterExpr(c)).join(\" OR \");\n case \"not\":\n return `NOT (${serializeFilterExpr(expr.child)})`;\n case \"group\":\n return `(${serializeFilterExpr(expr.child)})`;\n }\n}\n\n/**\n * Build a typed API params object from a FilterExpr and pagination state.\n *\n * Used by list and expansion pages to convert Zustand store state into\n * query params for the server. The base object carries pagination keys\n * (limit / offset); filter terms from the expression are merged in.\n *\n * When the expression is a flat AND of terms, each term becomes a flat\n * query param (backwards compatible with existing server endpoints).\n * When the expression contains OR/NOT/Group nodes, it is serialized\n * as a `q=` string using the server's query expression syntax.\n *\n * @example\n * const params = buildListParams<ValueSetListParams>(\n * filterExpr,\n * { limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize },\n * );\n */\nexport function buildListParams<T extends object>(\n expr: FilterExpr,\n base: T,\n): T {\n if (isEmptyFilterExpr(expr)) return base;\n\n // Flat AND of terms: serialize as individual key=value params.\n if (isFlatAnd(expr)) {\n const pairs = flattenFilterExpr(expr);\n const extra = Object.fromEntries(\n pairs\n .filter(({ key }) => !(key in base))\n .map(({ key, value }) => [key, value]),\n );\n return { ...base, ...extra } as T;\n }\n\n // Complex expression: serialize as q= string.\n return { ...base, q: serializeFilterExpr(expr) } as T;\n}\n\n/**\n * Build a flat AND expression from an array of key/value pairs.\n * Convenience for constructing the initial state from existing filter params.\n */\nexport function termsToFilterExpr(\n terms: { key: string; value: string }[],\n): FilterExpr {\n return {\n type: \"and\",\n children: terms.map((t) => ({\n type: \"term\",\n key: t.key,\n value: t.value,\n })),\n };\n}\n"],"mappings":";AA2DO,IAAM,eAA2B,EAAE,MAAM,OAAO,UAAU,CAAC,EAAE;;;AC9C7D,SAAS,aAAa,MAAgC;AAC3D,QAAM,MAAoB,CAAC;AAC3B,mBAAiB,MAAM,GAAG;AAC1B,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAkB,KAAyB;AACnE,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,UAAI,KAAK,IAAI;AACb;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,iBAAW,SAAS,KAAK,SAAU,kBAAiB,OAAO,GAAG;AAC9D;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,uBAAiB,KAAK,OAAO,GAAG;AAChC;AAAA,EACJ;AACF;AAcO,SAAS,kBACd,MACkC;AAClC,SAAO,aAAa,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,MAAM,OAAO,EAAE,KAAK,MAAM,EAAE;AACpE;AAKO,SAAS,kBAAkB,MAA2B;AAC3D,SAAO,aAAa,IAAI,EAAE,WAAW;AACvC;AAOA,SAAS,UAAU,MAA2B;AAC5C,MAAI,KAAK,SAAS,OAAQ,QAAO;AACjC,MAAI,KAAK,SAAS,MAAO,QAAO,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM;AAC5E,SAAO;AACT;AAUO,SAAS,oBAAoB,MAA0B;AAC5D,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,QAAQ;AACX,YAAM,UAAU,KAAK,MAAM,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACrE,aAAO,GAAG,KAAK,GAAG,OAAO,OAAO;AAAA,IAClC;AAAA,IACA,KAAK;AACH,aAAO,KAAK,SAAS,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,OAAO;AAAA,IACtE,KAAK;AACH,aAAO,KAAK,SAAS,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,MAAM;AAAA,IACrE,KAAK;AACH,aAAO,QAAQ,oBAAoB,KAAK,KAAK,CAAC;AAAA,IAChD,KAAK;AACH,aAAO,IAAI,oBAAoB,KAAK,KAAK,CAAC;AAAA,EAC9C;AACF;AAoBO,SAAS,gBACd,MACA,MACG;AACH,MAAI,kBAAkB,IAAI,EAAG,QAAO;AAGpC,MAAI,UAAU,IAAI,GAAG;AACnB,UAAM,QAAQ,kBAAkB,IAAI;AACpC,UAAM,QAAQ,OAAO;AAAA,MACnB,MACG,OAAO,CAAC,EAAE,IAAI,MAAM,EAAE,OAAO,KAAK,EAClC,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,CAAC;AAAA,IACzC;AACA,WAAO,EAAE,GAAG,MAAM,GAAG,MAAM;AAAA,EAC7B;AAGA,SAAO,EAAE,GAAG,MAAM,GAAG,oBAAoB,IAAI,EAAE;AACjD;AAMO,SAAS,kBACd,OACY;AACZ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,MAAM,IAAI,CAAC,OAAO;AAAA,MAC1B,MAAM;AAAA,MACN,KAAK,EAAE;AAAA,MACP,OAAO,EAAE;AAAA,IACX,EAAE;AAAA,EACJ;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/search/types.ts","../../src/search/filterExpr.ts"],"sourcesContent":["/**\n * FilterExpr -- lightweight boolean expression tree for search and filtering.\n *\n * Designed to start simple (a flat AND list of terms) and grow to support\n * full boolean queries (AND, OR, NOT, grouping) without breaking callers.\n *\n * The component always emits a FilterExpr. Callers that talk to a flat\n * server API today use flattenFilterExpr() to extract the terms. When the\n * server gains AST support, callers drop flattenFilterExpr and pass the\n * expression directly -- no component changes required.\n *\n * Extractable to @petrarca/sonnet-core: no app-specific imports.\n */\n\n/** A single field/value filter term -- the leaf node of a FilterExpr. */\nexport interface FilterTerm {\n type: \"term\";\n /** Schema property key -- used as the query param name sent to the server. */\n key: string;\n /** The filter value. */\n value: string;\n}\n\n/** Conjunction: all children must match. */\nexport interface FilterAnd {\n type: \"and\";\n children: FilterExpr[];\n}\n\n/** Disjunction: at least one child must match. */\nexport interface FilterOr {\n type: \"or\";\n children: FilterExpr[];\n}\n\n/** Negation: child must not match. */\nexport interface FilterNot {\n type: \"not\";\n child: FilterExpr;\n}\n\n/**\n * Explicit grouping (parentheses). Semantically identical to its child --\n * used to preserve user intent when round-tripping the expression.\n */\nexport interface FilterGroup {\n type: \"group\";\n child: FilterExpr;\n}\n\n/** The full expression type. */\nexport type FilterExpr =\n | FilterTerm\n | FilterAnd\n | FilterOr\n | FilterNot\n | FilterGroup;\n\n/** An empty expression -- no filters active. */\nexport const EMPTY_FILTER: FilterExpr = { type: \"and\", children: [] };\n","/**\n * Utilities for working with FilterExpr trees.\n *\n * Extractable to @petrarca/sonnet-core: no app-specific imports.\n */\n\nimport type { FilterExpr, FilterTerm } from \"./types\";\n\n/**\n * Return all term nodes from the expression as FilterTerm objects, depth-first.\n *\n * Ignores the boolean structure (AND / OR / NOT / GROUP).\n */\nexport function collectTerms(expr: FilterExpr): FilterTerm[] {\n const out: FilterTerm[] = [];\n collectTermNodes(expr, out);\n return out;\n}\n\nfunction collectTermNodes(expr: FilterExpr, out: FilterTerm[]): void {\n switch (expr.type) {\n case \"term\":\n out.push(expr);\n break;\n case \"and\":\n case \"or\":\n for (const child of expr.children) collectTermNodes(child, out);\n break;\n case \"not\":\n case \"group\":\n collectTermNodes(expr.child, out);\n break;\n }\n}\n\n/**\n * The value of the first term with `key`, or undefined -- for a facet used once.\n * Ignores the boolean structure, like collectTerms.\n */\nexport function termValue(expr: FilterExpr, key: string): string | undefined {\n return collectTerms(expr).find((t) => t.key === key)?.value;\n}\n\n/**\n * The values of every term with `key`, in order -- for a repeatable facet.\n * Ignores the boolean structure, like collectTerms.\n */\nexport function termValues(expr: FilterExpr, key: string): string[] {\n return collectTerms(expr)\n .filter((t) => t.key === key)\n .map((t) => t.value);\n}\n\n/**\n * Collect all term nodes from a FilterExpr tree as flat key/value pairs.\n *\n * Use this when talking to a server API that accepts only flat key/value\n * filter params. When the server gains AST support, pass the raw FilterExpr\n * instead and drop this call.\n *\n * @example\n * const terms = flattenFilterExpr(expr);\n * // [{ key: \"name\", value: \"acme\" }, { key: \"status\", value: \"active\" }]\n * const params = Object.fromEntries(terms.map(t => [t.key, t.value]));\n */\nexport function flattenFilterExpr(\n expr: FilterExpr,\n): { key: string; value: string }[] {\n return collectTerms(expr).map(({ key, value }) => ({ key, value }));\n}\n\n/**\n * Return true when the expression contains no term nodes.\n */\nexport function isEmptyFilterExpr(expr: FilterExpr): boolean {\n return collectTerms(expr).length === 0;\n}\n\n/**\n * Check whether a FilterExpr is a flat AND of terms only (no OR/NOT/Group).\n * When true, the expression can be serialized as simple key=value params.\n * When false, it must be serialized as a `q=` expression string.\n */\nfunction isFlatAnd(expr: FilterExpr): boolean {\n if (expr.type === \"term\") return true;\n if (expr.type === \"and\") return expr.children.every((c) => c.type === \"term\");\n return false;\n}\n\n/**\n * Serialize a FilterExpr to the server's q= expression syntax.\n *\n * Produces strings like `code ~ \"aspirin\" OR display ~ \"aspirin\"`.\n * All terms use the `~` (LIKE / substring) operator -- the FilterTerm\n * type carries no operator field; callers that need exact-match or other\n * operators should construct the q= string directly.\n */\nexport function serializeFilterExpr(expr: FilterExpr): string {\n switch (expr.type) {\n case \"term\": {\n const escaped = expr.value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n return `${expr.key} ~ \"${escaped}\"`;\n }\n case \"and\":\n return expr.children.map((c) => serializeFilterExpr(c)).join(\" AND \");\n case \"or\":\n return expr.children.map((c) => serializeFilterExpr(c)).join(\" OR \");\n case \"not\":\n return `NOT (${serializeFilterExpr(expr.child)})`;\n case \"group\":\n return `(${serializeFilterExpr(expr.child)})`;\n }\n}\n\n/**\n * Build a typed API params object from a FilterExpr and pagination state.\n *\n * Used by list and expansion pages to convert Zustand store state into\n * query params for the server. The base object carries pagination keys\n * (limit / offset); filter terms from the expression are merged in.\n *\n * When the expression is a flat AND of terms, each term becomes a flat\n * query param (backwards compatible with existing server endpoints).\n * When the expression contains OR/NOT/Group nodes, it is serialized\n * as a `q=` string using the server's query expression syntax.\n *\n * @example\n * const params = buildListParams<ValueSetListParams>(\n * filterExpr,\n * { limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize },\n * );\n */\nexport function buildListParams<T extends object>(\n expr: FilterExpr,\n base: T,\n): T {\n if (isEmptyFilterExpr(expr)) return base;\n\n // Flat AND of terms: serialize as individual key=value params.\n if (isFlatAnd(expr)) {\n const pairs = flattenFilterExpr(expr);\n const extra = Object.fromEntries(\n pairs\n .filter(({ key }) => !(key in base))\n .map(({ key, value }) => [key, value]),\n );\n return { ...base, ...extra } as T;\n }\n\n // Complex expression: serialize as q= string.\n return { ...base, q: serializeFilterExpr(expr) } as T;\n}\n\n/**\n * Build a flat AND expression from an array of key/value pairs.\n * Convenience for constructing the initial state from existing filter params.\n */\nexport function termsToFilterExpr(\n terms: { key: string; value: string }[],\n): FilterExpr {\n return {\n type: \"and\",\n children: terms.map((t) => ({\n type: \"term\",\n key: t.key,\n value: t.value,\n })),\n };\n}\n\n/**\n * A free text as a filter: the text in any of the fields -- one term per\n * field, OR-ed -- or, without fields, the server's own `search` term. Empty\n * text is no filter. What a search box sends for what is typed in it.\n */\nexport function textSearchExpr(text: string, fields?: string[]): FilterExpr {\n const trimmed = text.trim();\n if (!trimmed) return { type: \"and\", children: [] };\n if (!fields?.length)\n return {\n type: \"and\",\n children: [{ type: \"term\", key: \"search\", value: trimmed }],\n };\n const terms = fields.map(\n (key): FilterExpr => ({ type: \"term\", key, value: trimmed }),\n );\n return {\n type: \"and\",\n children: terms.length === 1 ? terms : [{ type: \"or\", children: terms }],\n };\n}\n"],"mappings":";AA2DO,IAAM,eAA2B,EAAE,MAAM,OAAO,UAAU,CAAC,EAAE;;;AC9C7D,SAAS,aAAa,MAAgC;AAC3D,QAAM,MAAoB,CAAC;AAC3B,mBAAiB,MAAM,GAAG;AAC1B,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAkB,KAAyB;AACnE,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,UAAI,KAAK,IAAI;AACb;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,iBAAW,SAAS,KAAK,SAAU,kBAAiB,OAAO,GAAG;AAC9D;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,uBAAiB,KAAK,OAAO,GAAG;AAChC;AAAA,EACJ;AACF;AAMO,SAAS,UAAU,MAAkB,KAAiC;AAC3E,SAAO,aAAa,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG,GAAG;AACxD;AAMO,SAAS,WAAW,MAAkB,KAAuB;AAClE,SAAO,aAAa,IAAI,EACrB,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,EAC3B,IAAI,CAAC,MAAM,EAAE,KAAK;AACvB;AAcO,SAAS,kBACd,MACkC;AAClC,SAAO,aAAa,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,MAAM,OAAO,EAAE,KAAK,MAAM,EAAE;AACpE;AAKO,SAAS,kBAAkB,MAA2B;AAC3D,SAAO,aAAa,IAAI,EAAE,WAAW;AACvC;AAOA,SAAS,UAAU,MAA2B;AAC5C,MAAI,KAAK,SAAS,OAAQ,QAAO;AACjC,MAAI,KAAK,SAAS,MAAO,QAAO,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM;AAC5E,SAAO;AACT;AAUO,SAAS,oBAAoB,MAA0B;AAC5D,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,QAAQ;AACX,YAAM,UAAU,KAAK,MAAM,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACrE,aAAO,GAAG,KAAK,GAAG,OAAO,OAAO;AAAA,IAClC;AAAA,IACA,KAAK;AACH,aAAO,KAAK,SAAS,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,OAAO;AAAA,IACtE,KAAK;AACH,aAAO,KAAK,SAAS,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,MAAM;AAAA,IACrE,KAAK;AACH,aAAO,QAAQ,oBAAoB,KAAK,KAAK,CAAC;AAAA,IAChD,KAAK;AACH,aAAO,IAAI,oBAAoB,KAAK,KAAK,CAAC;AAAA,EAC9C;AACF;AAoBO,SAAS,gBACd,MACA,MACG;AACH,MAAI,kBAAkB,IAAI,EAAG,QAAO;AAGpC,MAAI,UAAU,IAAI,GAAG;AACnB,UAAM,QAAQ,kBAAkB,IAAI;AACpC,UAAM,QAAQ,OAAO;AAAA,MACnB,MACG,OAAO,CAAC,EAAE,IAAI,MAAM,EAAE,OAAO,KAAK,EAClC,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,CAAC;AAAA,IACzC;AACA,WAAO,EAAE,GAAG,MAAM,GAAG,MAAM;AAAA,EAC7B;AAGA,SAAO,EAAE,GAAG,MAAM,GAAG,oBAAoB,IAAI,EAAE;AACjD;AAMO,SAAS,kBACd,OACY;AACZ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,MAAM,IAAI,CAAC,OAAO;AAAA,MAC1B,MAAM;AAAA,MACN,KAAK,EAAE;AAAA,MACP,OAAO,EAAE;AAAA,IACX,EAAE;AAAA,EACJ;AACF;AAOO,SAAS,eAAe,MAAc,QAA+B;AAC1E,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,CAAC,QAAS,QAAO,EAAE,MAAM,OAAO,UAAU,CAAC,EAAE;AACjD,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,CAAC,EAAE,MAAM,QAAQ,KAAK,UAAU,OAAO,QAAQ,CAAC;AAAA,IAC5D;AACF,QAAM,QAAQ,OAAO;AAAA,IACnB,CAAC,SAAqB,EAAE,MAAM,QAAQ,KAAK,OAAO,QAAQ;AAAA,EAC5D;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,MAAM,WAAW,IAAI,QAAQ,CAAC,EAAE,MAAM,MAAM,UAAU,MAAM,CAAC;AAAA,EACzE;AACF;","names":[]}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Coding, CodingMatch } from '../schema/index.js';
|
|
2
|
+
import React__default from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The terminology service -- what a coded-value field asks of the application
|
|
6
|
+
* that serves its codes: search a list, look a code up, validate one. Transport-
|
|
7
|
+
* neutral: sonnet defines it, an application supplies it (a terminology server,
|
|
8
|
+
* lists of its own), and nothing in sonnet knows where codes come from. See
|
|
9
|
+
* docs/design/coded-values.md (workspace).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Which list: a value set, or a code system -- identifiers the service resolves. */
|
|
13
|
+
interface ListRef {
|
|
14
|
+
valueSet?: string;
|
|
15
|
+
system?: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A search hit: a coding, and where it sits -- its ancestors' labels, root
|
|
20
|
+
* first. A service may return properties of its own beside them (a concept's
|
|
21
|
+
* key): a field stores one with `x-ui-options.store`, a coding keeps them with
|
|
22
|
+
* `keep`.
|
|
23
|
+
*/
|
|
24
|
+
interface CodingHit extends Coding {
|
|
25
|
+
[property: string]: unknown;
|
|
26
|
+
path?: string[];
|
|
27
|
+
}
|
|
28
|
+
/** One page of a search: the hits, and how many there are in all. */
|
|
29
|
+
interface SearchResult {
|
|
30
|
+
codings: CodingHit[];
|
|
31
|
+
total?: number;
|
|
32
|
+
}
|
|
33
|
+
/** What a validation says of a code: in the list -- and, when the service can say, known elsewhere. */
|
|
34
|
+
interface ValidationResult {
|
|
35
|
+
inList: boolean;
|
|
36
|
+
/** Whether the code is known outside the list; none when the service cannot say. */
|
|
37
|
+
known?: boolean;
|
|
38
|
+
display?: string;
|
|
39
|
+
message?: string;
|
|
40
|
+
}
|
|
41
|
+
interface TerminologyService {
|
|
42
|
+
/** Codes of a list matching a text -- typeahead; no text: the list itself, paged. */
|
|
43
|
+
search(query: ListRef & {
|
|
44
|
+
text?: string;
|
|
45
|
+
limit?: number;
|
|
46
|
+
offset?: number;
|
|
47
|
+
}): Promise<SearchResult>;
|
|
48
|
+
/** One code's label and version -- to show a stored code. */
|
|
49
|
+
lookup(coding: LookupQuery): Promise<Coding | undefined>;
|
|
50
|
+
/** Whether a code is in a list -- to check a stored code. */
|
|
51
|
+
validate?(query: ValidateQuery): Promise<ValidationResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Several lookups in one request, answered in their order; optional. The
|
|
54
|
+
* provider batches the lookups of a moment into it (FHIR: a batch Bundle).
|
|
55
|
+
*/
|
|
56
|
+
lookupMany?(codings: LookupQuery[]): Promise<(Coding | undefined)[]>;
|
|
57
|
+
/** Several validations in one request, answered in their order; optional. */
|
|
58
|
+
validateMany?(queries: ValidateQuery[]): Promise<ValidationResult[]>;
|
|
59
|
+
/**
|
|
60
|
+
* A code in other lists, and how each relates to it -- FHIR `$translate`
|
|
61
|
+
* over a concept map; optional. A codeable concept's form asks it for its
|
|
62
|
+
* further lists once a code is picked in its first, and shows what it
|
|
63
|
+
* answers as suggestions. The mappings are the service's.
|
|
64
|
+
*/
|
|
65
|
+
translate?(query: TranslateQuery): Promise<Translation[]>;
|
|
66
|
+
}
|
|
67
|
+
/** What a lookup asks: one code, by its system and version. */
|
|
68
|
+
interface LookupQuery {
|
|
69
|
+
system: string;
|
|
70
|
+
code: string;
|
|
71
|
+
version?: string;
|
|
72
|
+
}
|
|
73
|
+
/** What a validation asks: whether a code is in a list. */
|
|
74
|
+
type ValidateQuery = ListRef & {
|
|
75
|
+
code: string;
|
|
76
|
+
};
|
|
77
|
+
/** What a translation asks: one code, and the lists to find it in. */
|
|
78
|
+
interface TranslateQuery {
|
|
79
|
+
coding: LookupQuery;
|
|
80
|
+
targets: ListRef[];
|
|
81
|
+
}
|
|
82
|
+
/** A code a translation found: its coding, and how it relates to the code asked -- the same, broader, narrower. */
|
|
83
|
+
interface Translation {
|
|
84
|
+
coding: Coding;
|
|
85
|
+
match?: CodingMatch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A terminology service over lists given inline -- as a code list given as
|
|
90
|
+
* data (genericode, a CSV) -- searched in memory, and over concept maps given
|
|
91
|
+
* inline, translated in memory. A small list needs no server; the playground
|
|
92
|
+
* and tests use it.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/** A code list: its identifier (a value set's or a code system's), its codes. */
|
|
96
|
+
interface CodeList {
|
|
97
|
+
/** The identifier a binding names: the value set, or the code system itself. */
|
|
98
|
+
id: string;
|
|
99
|
+
/** The code system of its codes, when the list is a value set of one system. */
|
|
100
|
+
system?: string;
|
|
101
|
+
/** Its codes: each a coding, its system the list's unless it names one; a path and properties of its own optional. */
|
|
102
|
+
codes: (Omit<Coding, "system"> & {
|
|
103
|
+
[property: string]: unknown;
|
|
104
|
+
system?: string;
|
|
105
|
+
path?: string[];
|
|
106
|
+
})[];
|
|
107
|
+
}
|
|
108
|
+
/** A concept map: the codes of one code system as codes of another, and how each relates (FHIR `ConceptMap`). */
|
|
109
|
+
interface ConceptMap {
|
|
110
|
+
/** The code system mapped from. */
|
|
111
|
+
source: string;
|
|
112
|
+
/** The code system mapped to. */
|
|
113
|
+
target: string;
|
|
114
|
+
elements: {
|
|
115
|
+
code: string;
|
|
116
|
+
target: string;
|
|
117
|
+
match?: CodingMatch;
|
|
118
|
+
}[];
|
|
119
|
+
}
|
|
120
|
+
/** A terminology service over code lists -- and concept maps between them -- given inline. */
|
|
121
|
+
declare function staticTerminology(lists: CodeList[], maps?: ConceptMap[]): TerminologyService;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A terminology service that remembers and batches -- what TerminologyProvider
|
|
125
|
+
* puts in front of an application's service, so that a form of many coded
|
|
126
|
+
* fields does not flood it. Lookups and validations are kept, per list and
|
|
127
|
+
* code, for the session or a time; one already asked is not asked again while
|
|
128
|
+
* it is answered; those of a moment go as one request when the service offers
|
|
129
|
+
* the batch operations. Translations are kept the same way, per code and the
|
|
130
|
+
* lists asked. A failure is not kept: it is asked again next time. Searches
|
|
131
|
+
* pass through: their texts rarely repeat.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/** How the provider remembers and batches. */
|
|
135
|
+
interface TerminologyCacheOptions {
|
|
136
|
+
/** How long an answer counts, in milliseconds; default: the session. */
|
|
137
|
+
ttlMs?: number;
|
|
138
|
+
/** How long requests are collected into one batch, in milliseconds; default 10. */
|
|
139
|
+
batchWindowMs?: number;
|
|
140
|
+
}
|
|
141
|
+
/** The service, its lookups and validations remembered and batched. */
|
|
142
|
+
declare function cachingTerminology(service: TerminologyService, options?: TerminologyCacheOptions): TerminologyService;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Supply the terminology service to the coded-value fields below -- its
|
|
146
|
+
* lookups and validations remembered and batched (`cache`), so that a form of
|
|
147
|
+
* many coded fields does not flood it; `cache={false}` hands the service on as
|
|
148
|
+
* it is, for one that caches itself.
|
|
149
|
+
*/
|
|
150
|
+
declare function TerminologyProvider({ service, cache, children, }: {
|
|
151
|
+
service: TerminologyService;
|
|
152
|
+
/** How lookups and validations are remembered and batched; false: not at all. */
|
|
153
|
+
cache?: TerminologyCacheOptions | false;
|
|
154
|
+
children: React__default.ReactNode;
|
|
155
|
+
}): React__default.ReactElement;
|
|
156
|
+
|
|
157
|
+
/** The terminology service supplied; null when the application supplies none. */
|
|
158
|
+
declare function useTerminology(): TerminologyService | null;
|
|
159
|
+
|
|
160
|
+
export { TerminologyProvider, cachingTerminology, staticTerminology, useTerminology };
|
|
161
|
+
export type { CodeList, CodingHit, ConceptMap, ListRef, LookupQuery, SearchResult, TerminologyCacheOptions, TerminologyService, TranslateQuery, Translation, ValidateQuery, ValidationResult };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/terminology/staticTerminology.ts
|
|
2
|
+
function codingsOf(list) {
|
|
3
|
+
return list.codes.map((c) => ({
|
|
4
|
+
...c,
|
|
5
|
+
system: c.system ?? list.system ?? list.id
|
|
6
|
+
}));
|
|
7
|
+
}
|
|
8
|
+
function matches(c, text) {
|
|
9
|
+
const t = text.toLowerCase();
|
|
10
|
+
return c.code.toLowerCase().includes(t) || (c.display ?? "").toLowerCase().includes(t);
|
|
11
|
+
}
|
|
12
|
+
function staticTerminology(lists, maps = []) {
|
|
13
|
+
const byRef = (ref) => lists.find((l) => l.id === (ref.valueSet ?? ref.system));
|
|
14
|
+
const all = lists.flatMap(codingsOf);
|
|
15
|
+
const lookupOne = ({ system, code }) => {
|
|
16
|
+
const found = all.find((c) => c.system === system && c.code === code);
|
|
17
|
+
if (!found) return void 0;
|
|
18
|
+
const { path: _path, ...coding } = found;
|
|
19
|
+
return coding;
|
|
20
|
+
};
|
|
21
|
+
const validateOne = ({ code, ...ref }) => {
|
|
22
|
+
const list = byRef(ref);
|
|
23
|
+
const inList = list ? codingsOf(list).find((c) => c.code === code) : void 0;
|
|
24
|
+
const known = all.find((c) => c.code === code);
|
|
25
|
+
return {
|
|
26
|
+
inList: !!inList,
|
|
27
|
+
known: !!known,
|
|
28
|
+
display: (inList ?? known)?.display
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const translateTo = (coding, ref) => {
|
|
32
|
+
const list = byRef(ref);
|
|
33
|
+
const inList = list ? codingsOf(list) : [];
|
|
34
|
+
const system = ref.system ?? inList[0]?.system;
|
|
35
|
+
return maps.filter((m) => m.source === coding.system && m.target === system).flatMap((m) => m.elements.filter((e) => e.code === coding.code)).flatMap((e) => {
|
|
36
|
+
const found = inList.find((c) => c.code === e.target);
|
|
37
|
+
if (!found) return [];
|
|
38
|
+
const { path: _path, ...target } = found;
|
|
39
|
+
return [{ coding: target, ...e.match ? { match: e.match } : {} }];
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
search({ text, limit = 50, offset = 0, ...ref }) {
|
|
44
|
+
const list = byRef(ref);
|
|
45
|
+
const codings = (list ? codingsOf(list) : []).filter(
|
|
46
|
+
(c) => !text || matches(c, text)
|
|
47
|
+
);
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
codings: codings.slice(offset, offset + limit),
|
|
50
|
+
total: codings.length
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
lookup: (q) => Promise.resolve(lookupOne(q)),
|
|
54
|
+
validate: (q) => Promise.resolve(validateOne(q)),
|
|
55
|
+
// The batch operations: what a service answering several in one request offers.
|
|
56
|
+
lookupMany: (qs) => Promise.resolve(qs.map(lookupOne)),
|
|
57
|
+
validateMany: (qs) => Promise.resolve(qs.map(validateOne)),
|
|
58
|
+
translate: ({ coding, targets }) => Promise.resolve(targets.flatMap((t) => translateTo(coding, t)))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/terminology/cachingTerminology.ts
|
|
63
|
+
function batcher(windowMs, one, many) {
|
|
64
|
+
if (!many) return one;
|
|
65
|
+
let pending = [];
|
|
66
|
+
let timer;
|
|
67
|
+
const flush = () => {
|
|
68
|
+
const batch = pending;
|
|
69
|
+
pending = [];
|
|
70
|
+
timer = void 0;
|
|
71
|
+
many(batch.map((b) => b.q)).then(
|
|
72
|
+
(answers) => {
|
|
73
|
+
if (answers.length !== batch.length) {
|
|
74
|
+
const e = new Error(
|
|
75
|
+
`a batch of ${batch.length} was answered with ${answers.length}`
|
|
76
|
+
);
|
|
77
|
+
return batch.forEach((b) => b.reject(e));
|
|
78
|
+
}
|
|
79
|
+
batch.forEach((b, i) => b.resolve(answers[i]));
|
|
80
|
+
},
|
|
81
|
+
(e) => batch.forEach((b) => b.reject(e))
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
return (q) => new Promise((resolve, reject) => {
|
|
85
|
+
pending.push({ q, resolve, reject });
|
|
86
|
+
timer ??= setTimeout(flush, windowMs);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function remembering(key, ask, ttlMs) {
|
|
90
|
+
const kept = /* @__PURE__ */ new Map();
|
|
91
|
+
return (q) => {
|
|
92
|
+
const k = key(q);
|
|
93
|
+
const hit = kept.get(k);
|
|
94
|
+
if (hit && (ttlMs === void 0 || Date.now() - hit.at < ttlMs))
|
|
95
|
+
return hit.answer;
|
|
96
|
+
const answer = ask(q);
|
|
97
|
+
kept.set(k, { answer, at: Date.now() });
|
|
98
|
+
answer.catch(() => kept.delete(k));
|
|
99
|
+
return answer;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var lookupKey = (q) => [q.system, q.version ?? "", q.code].join("|");
|
|
103
|
+
var validateKey = (q) => [q.valueSet ?? "", q.system ?? "", q.version ?? "", q.code].join("|");
|
|
104
|
+
var listKey = (l) => [l.valueSet ?? "", l.system ?? "", l.version ?? ""].join("|");
|
|
105
|
+
var translateKey = (q) => [lookupKey(q.coding), ...q.targets.map(listKey)].join("#");
|
|
106
|
+
function cachingTerminology(service, options = {}) {
|
|
107
|
+
const windowMs = options.batchWindowMs ?? 10;
|
|
108
|
+
const lookup = remembering(
|
|
109
|
+
lookupKey,
|
|
110
|
+
batcher(
|
|
111
|
+
windowMs,
|
|
112
|
+
(q) => service.lookup(q),
|
|
113
|
+
service.lookupMany && ((qs) => service.lookupMany(qs))
|
|
114
|
+
),
|
|
115
|
+
options.ttlMs
|
|
116
|
+
);
|
|
117
|
+
const validateOne = service.validate;
|
|
118
|
+
const validate = validateOne && remembering(
|
|
119
|
+
validateKey,
|
|
120
|
+
batcher(
|
|
121
|
+
windowMs,
|
|
122
|
+
(q) => validateOne.call(service, q),
|
|
123
|
+
service.validateMany && ((qs) => service.validateMany(qs))
|
|
124
|
+
),
|
|
125
|
+
options.ttlMs
|
|
126
|
+
);
|
|
127
|
+
const translateOne = service.translate;
|
|
128
|
+
const translate = translateOne && remembering(
|
|
129
|
+
translateKey,
|
|
130
|
+
(q) => translateOne.call(service, q),
|
|
131
|
+
options.ttlMs
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
search: (query) => service.search(query),
|
|
135
|
+
lookup,
|
|
136
|
+
...validate ? { validate } : {},
|
|
137
|
+
...translate ? { translate } : {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/terminology/TerminologyProvider.tsx
|
|
142
|
+
import { useMemo } from "react";
|
|
143
|
+
|
|
144
|
+
// src/terminology/terminologyContext.ts
|
|
145
|
+
import { createContext } from "react";
|
|
146
|
+
var TerminologyContext = createContext(
|
|
147
|
+
null
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// src/terminology/TerminologyProvider.tsx
|
|
151
|
+
import { jsx } from "react/jsx-runtime";
|
|
152
|
+
function TerminologyProvider({
|
|
153
|
+
service,
|
|
154
|
+
cache,
|
|
155
|
+
children
|
|
156
|
+
}) {
|
|
157
|
+
const ttlMs = cache ? cache.ttlMs : void 0;
|
|
158
|
+
const batchWindowMs = cache ? cache.batchWindowMs : void 0;
|
|
159
|
+
const off = cache === false;
|
|
160
|
+
const value = useMemo(
|
|
161
|
+
() => off ? service : cachingTerminology(service, { ttlMs, batchWindowMs }),
|
|
162
|
+
[service, off, ttlMs, batchWindowMs]
|
|
163
|
+
);
|
|
164
|
+
return /* @__PURE__ */ jsx(TerminologyContext.Provider, { value, children });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/terminology/useTerminology.ts
|
|
168
|
+
import { useContext } from "react";
|
|
169
|
+
function useTerminology() {
|
|
170
|
+
return useContext(TerminologyContext);
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
TerminologyProvider,
|
|
174
|
+
cachingTerminology,
|
|
175
|
+
staticTerminology,
|
|
176
|
+
useTerminology
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/terminology/staticTerminology.ts","../../src/terminology/cachingTerminology.ts","../../src/terminology/TerminologyProvider.tsx","../../src/terminology/terminologyContext.ts","../../src/terminology/useTerminology.ts"],"sourcesContent":["/**\n * A terminology service over lists given inline -- as a code list given as\n * data (genericode, a CSV) -- searched in memory, and over concept maps given\n * inline, translated in memory. A small list needs no server; the playground\n * and tests use it.\n */\n\nimport type { Coding, CodingMatch } from \"../schema/coreTypes\";\nimport type {\n CodingHit,\n ListRef,\n LookupQuery,\n TerminologyService,\n TranslateQuery,\n Translation,\n ValidateQuery,\n ValidationResult,\n} from \"./types\";\n\n/** A code list: its identifier (a value set's or a code system's), its codes. */\nexport interface CodeList {\n /** The identifier a binding names: the value set, or the code system itself. */\n id: string;\n /** The code system of its codes, when the list is a value set of one system. */\n system?: string;\n /** Its codes: each a coding, its system the list's unless it names one; a path and properties of its own optional. */\n codes: (Omit<Coding, \"system\"> & {\n [property: string]: unknown;\n system?: string;\n path?: string[];\n })[];\n}\n\n/** A concept map: the codes of one code system as codes of another, and how each relates (FHIR `ConceptMap`). */\nexport interface ConceptMap {\n /** The code system mapped from. */\n source: string;\n /** The code system mapped to. */\n target: string;\n elements: { code: string; target: string; match?: CodingMatch }[];\n}\n\n// A list's codes as codings: each its own system, or the list's.\nfunction codingsOf(list: CodeList): CodingHit[] {\n return list.codes.map((c) => ({\n ...c,\n system: c.system ?? list.system ?? list.id,\n }));\n}\n\n// Whether a coding matches a search text: its label or its code.\nfunction matches(c: Coding, text: string): boolean {\n const t = text.toLowerCase();\n return (\n c.code.toLowerCase().includes(t) ||\n (c.display ?? \"\").toLowerCase().includes(t)\n );\n}\n\n/** A terminology service over code lists -- and concept maps between them -- given inline. */\nexport function staticTerminology(\n lists: CodeList[],\n maps: ConceptMap[] = [],\n): TerminologyService {\n const byRef = (ref: ListRef) =>\n lists.find((l) => l.id === (ref.valueSet ?? ref.system));\n const all = lists.flatMap(codingsOf);\n\n // One code's label, without its path.\n const lookupOne = ({ system, code }: LookupQuery): Coding | undefined => {\n const found = all.find((c) => c.system === system && c.code === code);\n if (!found) return undefined;\n const { path: _path, ...coding } = found;\n return coding;\n };\n\n // Whether a code is in a list, known elsewhere, and its label.\n const validateOne = ({ code, ...ref }: ValidateQuery): ValidationResult => {\n const list = byRef(ref);\n const inList = list\n ? codingsOf(list).find((c) => c.code === code)\n : undefined;\n const known = all.find((c) => c.code === code);\n return {\n inList: !!inList,\n known: !!known,\n display: (inList ?? known)?.display,\n };\n };\n\n // A code in one target list: what the maps from its system say, kept to the list's codes.\n const translateTo = (coding: LookupQuery, ref: ListRef): Translation[] => {\n const list = byRef(ref);\n const inList = list ? codingsOf(list) : [];\n const system = ref.system ?? inList[0]?.system;\n return maps\n .filter((m) => m.source === coding.system && m.target === system)\n .flatMap((m) => m.elements.filter((e) => e.code === coding.code))\n .flatMap((e) => {\n const found = inList.find((c) => c.code === e.target);\n if (!found) return [];\n const { path: _path, ...target } = found;\n return [{ coding: target, ...(e.match ? { match: e.match } : {}) }];\n });\n };\n\n return {\n search({ text, limit = 50, offset = 0, ...ref }) {\n const list = byRef(ref);\n const codings = (list ? codingsOf(list) : []).filter(\n (c) => !text || matches(c, text),\n );\n return Promise.resolve({\n codings: codings.slice(offset, offset + limit),\n total: codings.length,\n });\n },\n lookup: (q) => Promise.resolve(lookupOne(q)),\n validate: (q) => Promise.resolve(validateOne(q)),\n // The batch operations: what a service answering several in one request offers.\n lookupMany: (qs) => Promise.resolve(qs.map(lookupOne)),\n validateMany: (qs) => Promise.resolve(qs.map(validateOne)),\n translate: ({ coding, targets }: TranslateQuery) =>\n Promise.resolve(targets.flatMap((t) => translateTo(coding, t))),\n };\n}\n","/**\n * A terminology service that remembers and batches -- what TerminologyProvider\n * puts in front of an application's service, so that a form of many coded\n * fields does not flood it. Lookups and validations are kept, per list and\n * code, for the session or a time; one already asked is not asked again while\n * it is answered; those of a moment go as one request when the service offers\n * the batch operations. Translations are kept the same way, per code and the\n * lists asked. A failure is not kept: it is asked again next time. Searches\n * pass through: their texts rarely repeat.\n */\n\nimport type { Coding } from \"../schema/coreTypes\";\nimport type {\n ListRef,\n LookupQuery,\n TerminologyService,\n TranslateQuery,\n Translation,\n ValidateQuery,\n ValidationResult,\n} from \"./types\";\n\n/** How the provider remembers and batches. */\nexport interface TerminologyCacheOptions {\n /** How long an answer counts, in milliseconds; default: the session. */\n ttlMs?: number;\n /** How long requests are collected into one batch, in milliseconds; default 10. */\n batchWindowMs?: number;\n}\n\ninterface Kept<R> {\n answer: Promise<R>;\n at: number;\n}\n\n// Requests of a moment, sent together: through `many` when the service has it, else one by one.\nfunction batcher<Q, R>(\n windowMs: number,\n one: (q: Q) => Promise<R>,\n many: ((qs: Q[]) => Promise<R[]>) | undefined,\n): (q: Q) => Promise<R> {\n if (!many) return one;\n let pending: {\n q: Q;\n resolve: (r: R) => void;\n reject: (e: unknown) => void;\n }[] = [];\n let timer: ReturnType<typeof setTimeout> | undefined;\n const flush = () => {\n const batch = pending;\n pending = [];\n timer = undefined;\n many(batch.map((b) => b.q)).then(\n (answers) => {\n // One answer per request, in their order: any other count is no answer to trust.\n if (answers.length !== batch.length) {\n const e = new Error(\n `a batch of ${batch.length} was answered with ${answers.length}`,\n );\n return batch.forEach((b) => b.reject(e));\n }\n batch.forEach((b, i) => b.resolve(answers[i]));\n },\n (e) => batch.forEach((b) => b.reject(e)),\n );\n };\n return (q) =>\n new Promise<R>((resolve, reject) => {\n pending.push({ q, resolve, reject });\n timer ??= setTimeout(flush, windowMs);\n });\n}\n\n// Answers kept by key, for a time; an answer asked for while it is answered is shared.\nfunction remembering<Q, R>(\n key: (q: Q) => string,\n ask: (q: Q) => Promise<R>,\n ttlMs: number | undefined,\n): (q: Q) => Promise<R> {\n const kept = new Map<string, Kept<R>>();\n return (q) => {\n const k = key(q);\n const hit = kept.get(k);\n if (hit && (ttlMs === undefined || Date.now() - hit.at < ttlMs))\n return hit.answer;\n const answer = ask(q);\n kept.set(k, { answer, at: Date.now() });\n // A failure is not kept.\n answer.catch(() => kept.delete(k));\n return answer;\n };\n}\n\nconst lookupKey = (q: LookupQuery) =>\n [q.system, q.version ?? \"\", q.code].join(\"|\");\nconst validateKey = (q: ValidateQuery) =>\n [q.valueSet ?? \"\", q.system ?? \"\", q.version ?? \"\", q.code].join(\"|\");\nconst listKey = (l: ListRef) =>\n [l.valueSet ?? \"\", l.system ?? \"\", l.version ?? \"\"].join(\"|\");\nconst translateKey = (q: TranslateQuery) =>\n [lookupKey(q.coding), ...q.targets.map(listKey)].join(\"#\");\n\n/** The service, its lookups and validations remembered and batched. */\nexport function cachingTerminology(\n service: TerminologyService,\n options: TerminologyCacheOptions = {},\n): TerminologyService {\n const windowMs = options.batchWindowMs ?? 10;\n const lookup = remembering<LookupQuery, Coding | undefined>(\n lookupKey,\n batcher(\n windowMs,\n (q) => service.lookup(q),\n service.lookupMany && ((qs) => service.lookupMany!(qs)),\n ),\n options.ttlMs,\n );\n const validateOne = service.validate;\n const validate =\n validateOne &&\n remembering<ValidateQuery, ValidationResult>(\n validateKey,\n batcher(\n windowMs,\n (q) => validateOne.call(service, q),\n service.validateMany && ((qs) => service.validateMany!(qs)),\n ),\n options.ttlMs,\n );\n const translateOne = service.translate;\n const translate =\n translateOne &&\n remembering<TranslateQuery, Translation[]>(\n translateKey,\n (q) => translateOne.call(service, q),\n options.ttlMs,\n );\n return {\n search: (query) => service.search(query),\n lookup,\n ...(validate ? { validate } : {}),\n ...(translate ? { translate } : {}),\n };\n}\n","import React, { useMemo } from \"react\";\nimport {\n cachingTerminology,\n type TerminologyCacheOptions,\n} from \"./cachingTerminology\";\nimport { TerminologyContext } from \"./terminologyContext\";\nimport type { TerminologyService } from \"./types\";\n\n/**\n * Supply the terminology service to the coded-value fields below -- its\n * lookups and validations remembered and batched (`cache`), so that a form of\n * many coded fields does not flood it; `cache={false}` hands the service on as\n * it is, for one that caches itself.\n */\nexport function TerminologyProvider({\n service,\n cache,\n children,\n}: {\n service: TerminologyService;\n /** How lookups and validations are remembered and batched; false: not at all. */\n cache?: TerminologyCacheOptions | false;\n children: React.ReactNode;\n}): React.ReactElement {\n const ttlMs = cache ? cache.ttlMs : undefined;\n const batchWindowMs = cache ? cache.batchWindowMs : undefined;\n const off = cache === false;\n // One cache per service and settings: the options' identity does not matter.\n const value = useMemo(\n () =>\n off ? service : cachingTerminology(service, { ttlMs, batchWindowMs }),\n [service, off, ttlMs, batchWindowMs],\n );\n return (\n <TerminologyContext.Provider value={value}>\n {children}\n </TerminologyContext.Provider>\n );\n}\n","/**\n * The terminology service an application supplies -- by context, as the entity\n * fetcher is. Without one, a coded-value field says so and shows stored codes\n * with their stored label: nothing is lost.\n */\n\nimport { createContext } from \"react\";\nimport type { TerminologyService } from \"./types\";\n\nexport const TerminologyContext = createContext<TerminologyService | null>(\n null,\n);\n","import { useContext } from \"react\";\nimport { TerminologyContext } from \"./terminologyContext\";\nimport type { TerminologyService } from \"./types\";\n\n/** The terminology service supplied; null when the application supplies none. */\nexport function useTerminology(): TerminologyService | null {\n return useContext(TerminologyContext);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -73,4 +73,4 @@ interface GraphqlFetcherConfig {
|
|
|
73
73
|
*/
|
|
74
74
|
type EntityFetcherFactory = (options: RestFetcherConfig | GraphqlFetcherConfig) => EntityOptionFetcher;
|
|
75
75
|
|
|
76
|
-
export type {
|
|
76
|
+
export type { EntityFetcherFactory as E, GraphqlFetcherConfig as G, RestFetcherConfig as R, EntityOption as a, EntityOptionFetcher as b, EntityFetchParams as c };
|