@fuego-systems/core 0.1.4

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.
Files changed (54) hide show
  1. package/dist/constants.d.ts +89 -0
  2. package/dist/constants.d.ts.map +1 -0
  3. package/dist/constants.js +97 -0
  4. package/dist/constants.js.map +1 -0
  5. package/dist/db/types.d.ts +49 -0
  6. package/dist/db/types.d.ts.map +1 -0
  7. package/dist/db/types.js +7 -0
  8. package/dist/db/types.js.map +1 -0
  9. package/dist/fhir/helpers.d.ts +4 -0
  10. package/dist/fhir/helpers.d.ts.map +1 -0
  11. package/dist/fhir/helpers.js +7 -0
  12. package/dist/fhir/helpers.js.map +1 -0
  13. package/dist/fhir/index.d.ts +3 -0
  14. package/dist/fhir/index.d.ts.map +1 -0
  15. package/dist/fhir/index.js +19 -0
  16. package/dist/fhir/index.js.map +1 -0
  17. package/dist/fhir/transaction-utils.d.ts +22 -0
  18. package/dist/fhir/transaction-utils.d.ts.map +1 -0
  19. package/dist/fhir/transaction-utils.js +156 -0
  20. package/dist/fhir/transaction-utils.js.map +1 -0
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +23 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/scheduling/appointment.d.ts +15 -0
  26. package/dist/scheduling/appointment.d.ts.map +1 -0
  27. package/dist/scheduling/appointment.js +338 -0
  28. package/dist/scheduling/appointment.js.map +1 -0
  29. package/dist/scheduling/extension.d.ts +58 -0
  30. package/dist/scheduling/extension.d.ts.map +1 -0
  31. package/dist/scheduling/extension.js +3 -0
  32. package/dist/scheduling/extension.js.map +1 -0
  33. package/dist/scheduling/slot.d.ts +15 -0
  34. package/dist/scheduling/slot.d.ts.map +1 -0
  35. package/dist/scheduling/slot.js +291 -0
  36. package/dist/scheduling/slot.js.map +1 -0
  37. package/dist/scheduling/time-span.d.ts +26 -0
  38. package/dist/scheduling/time-span.d.ts.map +1 -0
  39. package/dist/scheduling/time-span.js +143 -0
  40. package/dist/scheduling/time-span.js.map +1 -0
  41. package/dist/search/alias-builder.d.ts +23 -0
  42. package/dist/search/alias-builder.d.ts.map +1 -0
  43. package/dist/search/alias-builder.js +126 -0
  44. package/dist/search/alias-builder.js.map +1 -0
  45. package/dist/search/normalizer.d.ts +25 -0
  46. package/dist/search/normalizer.d.ts.map +1 -0
  47. package/dist/search/normalizer.js +81 -0
  48. package/dist/search/normalizer.js.map +1 -0
  49. package/dist/test/factories.d.ts +10 -0
  50. package/dist/test/factories.d.ts.map +1 -0
  51. package/dist/test/factories.js +68 -0
  52. package/dist/test/factories.js.map +1 -0
  53. package/dist/tsconfig.tsbuildinfo +1 -0
  54. package/package.json +33 -0
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SEARCH_SERVICE_CODES_SYSTEM = void 0;
4
+ exports.extractSearchAliases = extractSearchAliases;
5
+ exports.buildAliases = buildAliases;
6
+ const constants_1 = require("../constants");
7
+ const normalizer_1 = require("./normalizer");
8
+ /**
9
+ * FHIR identifier system used by the search index to store and extract service codes.
10
+ *
11
+ * Note: this value may differ from SERVICE_CODES_SYSTEM in constants.ts which is
12
+ * used by the admin UI. Use this constant whenever interacting with the search index.
13
+ */
14
+ exports.SEARCH_SERVICE_CODES_SYSTEM = 'http://fuego.health/fhir/service-codes';
15
+ /**
16
+ * Build the list of alias rows to index for a given HealthcareService resource.
17
+ *
18
+ * Each alias carries a `weight` that scales the trigram similarity score
19
+ * during ranking, giving more prominent results for exact name matches
20
+ * vs. code matches vs. abbreviation matches.
21
+ *
22
+ * Weight scale:
23
+ * 1.5 – canonical name (primary display)
24
+ * 1.2 – HS_SEARCH_ALIASES_EXTENSION_URL extension entries (alternative labels)
25
+ * 1.0 – code + name combined phrase (searchable compound)
26
+ * 0.9 – service code identifier alone
27
+ * 0.7 – payer-specific / free-text comment aliases (hs.comment)
28
+ */
29
+ /**
30
+ * Extract the service code from the SEARCH_SERVICE_CODES_SYSTEM identifier.
31
+ * @param hs - HealthcareService resource to read.
32
+ * @returns Service code value or an empty string when missing.
33
+ */
34
+ function extractCode(hs) {
35
+ var _a, _b, _c;
36
+ return (_c = (_b = (_a = hs.identifier) === null || _a === void 0 ? void 0 : _a.find((id) => id.system === exports.SEARCH_SERVICE_CODES_SYSTEM)) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : '';
37
+ }
38
+ /**
39
+ * Extract custom search aliases from the HS_SEARCH_ALIASES_EXTENSION_URL extension.
40
+ * Returns an empty array when none are present.
41
+ * @param hs - HealthcareService resource to read.
42
+ * @returns Non-empty extension alias strings.
43
+ */
44
+ function extractSearchAliases(hs) {
45
+ var _a, _b, _c, _d;
46
+ return ((_d = (_c = (_b = (_a = hs.extension) === null || _a === void 0 ? void 0 : _a.filter((e) => e.url === constants_1.HS_SEARCH_ALIASES_EXTENSION_URL)) === null || _b === void 0 ? void 0 : _b.map((e) => { var _a; return (_a = e.valueString) !== null && _a !== void 0 ? _a : ''; })) === null || _c === void 0 ? void 0 : _c.filter(Boolean)) !== null && _d !== void 0 ? _d : []);
47
+ }
48
+ /**
49
+ * Deduplicate alias rows by normalizedAlias, keeping the one with the highest
50
+ * weight when multiple rows resolve to the same normalised form.
51
+ * @param rows - Candidate alias rows before deduplication.
52
+ * @returns Rows deduplicated by `normalizedAlias` with highest weight kept.
53
+ */
54
+ function dedup(rows) {
55
+ const seen = new Map();
56
+ for (const row of rows) {
57
+ const existing = seen.get(row.normalizedAlias);
58
+ if (!existing || row.weight > existing.weight) {
59
+ seen.set(row.normalizedAlias, row);
60
+ }
61
+ }
62
+ return Array.from(seen.values());
63
+ }
64
+ /**
65
+ * Build all alias rows for a single HealthcareService resource.
66
+ * @param hs - Source HealthcareService resource.
67
+ * @returns Alias rows ready to persist in the search index.
68
+ */
69
+ function buildAliases(hs) {
70
+ var _a;
71
+ const name = (_a = hs.name) !== null && _a !== void 0 ? _a : '';
72
+ const code = extractCode(hs);
73
+ const rows = [];
74
+ // 1. Canonical name (highest weight)
75
+ if (name) {
76
+ rows.push({
77
+ alias: name,
78
+ normalizedAlias: (0, normalizer_1.normalize)(name),
79
+ kind: 'canonical',
80
+ weight: 1.5
81
+ });
82
+ }
83
+ // 2. Custom search aliases from HS_SEARCH_ALIASES_EXTENSION_URL extension
84
+ for (const alt of extractSearchAliases(hs)) {
85
+ if (alt !== name) {
86
+ rows.push({
87
+ alias: alt,
88
+ normalizedAlias: (0, normalizer_1.normalize)(alt),
89
+ kind: 'alias',
90
+ weight: 1.2
91
+ });
92
+ }
93
+ }
94
+ // 3. Code + name combined phrase (helps "RX TORAX" style searches)
95
+ if (code && name) {
96
+ const combined = `${code} ${name}`;
97
+ rows.push({
98
+ alias: combined,
99
+ normalizedAlias: (0, normalizer_1.normalize)(combined),
100
+ kind: 'code-name',
101
+ weight: 1.0
102
+ });
103
+ }
104
+ // 4. Service code alone
105
+ if (code) {
106
+ rows.push({
107
+ alias: code,
108
+ normalizedAlias: (0, normalizer_1.normalize)(code),
109
+ kind: 'code',
110
+ weight: 0.9
111
+ });
112
+ }
113
+ // 5. Comment field – may contain informal / abbreviation labels
114
+ if (hs.comment) {
115
+ rows.push({
116
+ alias: hs.comment,
117
+ normalizedAlias: (0, normalizer_1.normalize)(hs.comment),
118
+ kind: 'alias',
119
+ weight: 0.7
120
+ });
121
+ }
122
+ // Remove empty normalised aliases (e.g. a code that is all punctuation)
123
+ const nonEmpty = rows.filter((r) => r.normalizedAlias.length > 0);
124
+ return dedup(nonEmpty);
125
+ }
126
+ //# sourceMappingURL=alias-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alias-builder.js","sourceRoot":"","sources":["../../src/search/alias-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HealthcareService } from '@medplum/fhirtypes'\nimport type { AliasRow } from '../db/types'\nimport { HS_SEARCH_ALIASES_EXTENSION_URL } from '../constants'\nimport { normalize } from './normalizer'\n\n/**\n * FHIR identifier system used by the search index to store and extract service codes.\n *\n * Note: this value may differ from SERVICE_CODES_SYSTEM in constants.ts which is\n * used by the admin UI. Use this constant whenever interacting with the search index.\n */\nexport const SEARCH_SERVICE_CODES_SYSTEM = 'http://fuego.health/fhir/service-codes'\n\n/**\n * Build the list of alias rows to index for a given HealthcareService resource.\n *\n * Each alias carries a `weight` that scales the trigram similarity score\n * during ranking, giving more prominent results for exact name matches\n * vs. code matches vs. abbreviation matches.\n *\n * Weight scale:\n * 1.5 – canonical name (primary display)\n * 1.2 – HS_SEARCH_ALIASES_EXTENSION_URL extension entries (alternative labels)\n * 1.0 – code + name combined phrase (searchable compound)\n * 0.9 – service code identifier alone\n * 0.7 – payer-specific / free-text comment aliases (hs.comment)\n */\n\n/**\n * Extract the service code from the SEARCH_SERVICE_CODES_SYSTEM identifier.\n * @param hs - HealthcareService resource to read.\n * @returns Service code value or an empty string when missing.\n */\nfunction extractCode (hs: HealthcareService): string {\n return hs.identifier?.find((id) => id.system === SEARCH_SERVICE_CODES_SYSTEM)?.value ?? ''\n}\n\n/**\n * Extract custom search aliases from the HS_SEARCH_ALIASES_EXTENSION_URL extension.\n * Returns an empty array when none are present.\n * @param hs - HealthcareService resource to read.\n * @returns Non-empty extension alias strings.\n */\nexport function extractSearchAliases (hs: HealthcareService): string[] {\n return (\n hs.extension\n ?.filter((e) => e.url === HS_SEARCH_ALIASES_EXTENSION_URL)\n ?.map((e) => e.valueString ?? '')\n ?.filter(Boolean) ?? []\n )\n}\n\n/**\n * Deduplicate alias rows by normalizedAlias, keeping the one with the highest\n * weight when multiple rows resolve to the same normalised form.\n * @param rows - Candidate alias rows before deduplication.\n * @returns Rows deduplicated by `normalizedAlias` with highest weight kept.\n */\nfunction dedup (rows: AliasRow[]): AliasRow[] {\n const seen = new Map<string, AliasRow>()\n for (const row of rows) {\n const existing = seen.get(row.normalizedAlias)\n if (!existing || row.weight > existing.weight) {\n seen.set(row.normalizedAlias, row)\n }\n }\n return Array.from(seen.values())\n}\n\n/**\n * Build all alias rows for a single HealthcareService resource.\n * @param hs - Source HealthcareService resource.\n * @returns Alias rows ready to persist in the search index.\n */\nexport function buildAliases (hs: HealthcareService): AliasRow[] {\n const name = hs.name ?? ''\n const code = extractCode(hs)\n const rows: AliasRow[] = []\n\n // 1. Canonical name (highest weight)\n if (name) {\n rows.push({\n alias: name,\n normalizedAlias: normalize(name),\n kind: 'canonical',\n weight: 1.5\n })\n }\n\n // 2. Custom search aliases from HS_SEARCH_ALIASES_EXTENSION_URL extension\n for (const alt of extractSearchAliases(hs)) {\n if (alt !== name) {\n rows.push({\n alias: alt,\n normalizedAlias: normalize(alt),\n kind: 'alias',\n weight: 1.2\n })\n }\n }\n\n // 3. Code + name combined phrase (helps \"RX TORAX\" style searches)\n if (code && name) {\n const combined = `${code} ${name}`\n rows.push({\n alias: combined,\n normalizedAlias: normalize(combined),\n kind: 'code-name',\n weight: 1.0\n })\n }\n\n // 4. Service code alone\n if (code) {\n rows.push({\n alias: code,\n normalizedAlias: normalize(code),\n kind: 'code',\n weight: 0.9\n })\n }\n\n // 5. Comment field – may contain informal / abbreviation labels\n if (hs.comment) {\n rows.push({\n alias: hs.comment,\n normalizedAlias: normalize(hs.comment),\n kind: 'alias',\n weight: 0.7\n })\n }\n\n // Remove empty normalised aliases (e.g. a code that is all punctuation)\n const nonEmpty = rows.filter((r) => r.normalizedAlias.length > 0)\n return dedup(nonEmpty)\n}\n"]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Text normaliser for the HealthcareService search index.
3
+ *
4
+ * Produces a lower-cased, accent-stripped, abbreviation-expanded string
5
+ * suitable for trigram similarity matching via pg_trgm.
6
+ *
7
+ * The normalisation here must mirror what the Postgres migration achieves with
8
+ * `unaccent()` for query-time comparisons. By pre-normalising at index time we
9
+ * avoid calling `unaccent()` on every row during search, which would prevent
10
+ * the GIN index from being used.
11
+ */
12
+ /**
13
+ * Normalise a text string for storage or query comparison.
14
+ *
15
+ * Steps:
16
+ * 1. NFD decompose + strip combining diacritical marks (accent removal)
17
+ * 2. Lower-case
18
+ * 3. Expand known medical abbreviations
19
+ * 4. Remove non-alphanumeric characters (keep spaces)
20
+ * 5. Collapse whitespace and trim
21
+ * @param text - Input text to normalize.
22
+ * @returns Normalized text suitable for trigram index lookup.
23
+ */
24
+ export declare function normalize(text: string | null | undefined): string;
25
+ //# sourceMappingURL=normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../src/search/normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsCH;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAuBlE"}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * Text normaliser for the HealthcareService search index.
4
+ *
5
+ * Produces a lower-cased, accent-stripped, abbreviation-expanded string
6
+ * suitable for trigram similarity matching via pg_trgm.
7
+ *
8
+ * The normalisation here must mirror what the Postgres migration achieves with
9
+ * `unaccent()` for query-time comparisons. By pre-normalising at index time we
10
+ * avoid calling `unaccent()` on every row during search, which would prevent
11
+ * the GIN index from being used.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.normalize = normalize;
15
+ /** Expanded abbreviations map (Spanish medical/radiological context). */
16
+ const ABBREVIATIONS = [
17
+ [/\brm\b/g, 'resonancia magnetica'],
18
+ [/\brx\b/g, 'radiografia'],
19
+ [/\btac\b/g, 'tomografia computada'],
20
+ [/\btc\b/g, 'tomografia computada'],
21
+ [/\beco\b/g, 'ecografia'],
22
+ [/\bus\b/g, 'ecografia'],
23
+ [/\bder\b/g, 'derecha'],
24
+ [/\bizq\b/g, 'izquierda'],
25
+ [/\bint\b/g, 'interno'],
26
+ [/\bext\b/g, 'externo'],
27
+ [/\bsup\b/g, 'superior'],
28
+ [/\binf\b/g, 'inferior'],
29
+ [/\bant\b/g, 'anterior'],
30
+ [/\bpost\b/g, 'posterior'],
31
+ [/\bmed\b/g, 'medico'],
32
+ [/\bped\b/g, 'pediatrico'],
33
+ [/\bgine\b/g, 'ginecologia'],
34
+ [/\boftal\b/g, 'oftalmologia'],
35
+ [/\btraumat\b/g, 'traumatologia'],
36
+ [/\bneurol\b/g, 'neurologia'],
37
+ [/\bcardio\b/g, 'cardiologia'],
38
+ [/\bderma\b/g, 'dermatologia'],
39
+ [/\boncol\b/g, 'oncologia'],
40
+ [/\burgen\b/g, 'urgencias'],
41
+ [/\bcir\b/g, 'cirugia'],
42
+ [/\bfisio\b/g, 'fisioterapia'],
43
+ [/\bpsico\b/g, 'psicologia'],
44
+ [/\bodont\b/g, 'odontologia'],
45
+ [/\bnutri\b/g, 'nutricion'],
46
+ [/\blab\b/g, 'laboratorio'],
47
+ [/\bpat\b/g, 'patologia'],
48
+ [/\brad\b/g, 'radiologia']
49
+ ];
50
+ /**
51
+ * Normalise a text string for storage or query comparison.
52
+ *
53
+ * Steps:
54
+ * 1. NFD decompose + strip combining diacritical marks (accent removal)
55
+ * 2. Lower-case
56
+ * 3. Expand known medical abbreviations
57
+ * 4. Remove non-alphanumeric characters (keep spaces)
58
+ * 5. Collapse whitespace and trim
59
+ * @param text - Input text to normalize.
60
+ * @returns Normalized text suitable for trigram index lookup.
61
+ */
62
+ function normalize(text) {
63
+ if (!text) {
64
+ return '';
65
+ }
66
+ // Step 1: Remove diacritics (accent stripping via NFD decomposition)
67
+ let result = text
68
+ .normalize('NFD')
69
+ .replace(/[\u0300-\u036f]/g, '');
70
+ // Step 2: Lower-case
71
+ result = result.toLowerCase();
72
+ // Step 3: Expand abbreviations
73
+ for (const [pattern, expansion] of ABBREVIATIONS) {
74
+ result = result.replace(pattern, expansion);
75
+ }
76
+ // Step 4: Remove non-alphanumeric characters (keep spaces)
77
+ result = result.replace(/[^a-z0-9\s]/g, ' ');
78
+ // Step 5: Collapse whitespace and trim
79
+ return result.replace(/\s+/g, ' ').trim();
80
+ }
81
+ //# sourceMappingURL=normalizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizer.js","sourceRoot":"","sources":["../../src/search/normalizer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAkDH,8BAuBC;AAvED,yEAAyE;AACzE,MAAM,aAAa,GAAuB;IACxC,CAAC,SAAS,EAAE,sBAAsB,CAAC;IACnC,CAAC,SAAS,EAAE,aAAa,CAAC;IAC1B,CAAC,UAAU,EAAE,sBAAsB,CAAC;IACpC,CAAC,SAAS,EAAE,sBAAsB,CAAC;IACnC,CAAC,UAAU,EAAE,WAAW,CAAC;IACzB,CAAC,SAAS,EAAE,WAAW,CAAC;IACxB,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,UAAU,EAAE,WAAW,CAAC;IACzB,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,WAAW,EAAE,WAAW,CAAC;IAC1B,CAAC,UAAU,EAAE,QAAQ,CAAC;IACtB,CAAC,UAAU,EAAE,YAAY,CAAC;IAC1B,CAAC,WAAW,EAAE,aAAa,CAAC;IAC5B,CAAC,YAAY,EAAE,cAAc,CAAC;IAC9B,CAAC,cAAc,EAAE,eAAe,CAAC;IACjC,CAAC,aAAa,EAAE,YAAY,CAAC;IAC7B,CAAC,aAAa,EAAE,aAAa,CAAC;IAC9B,CAAC,YAAY,EAAE,cAAc,CAAC;IAC9B,CAAC,YAAY,EAAE,WAAW,CAAC;IAC3B,CAAC,YAAY,EAAE,WAAW,CAAC;IAC3B,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,YAAY,EAAE,cAAc,CAAC;IAC9B,CAAC,YAAY,EAAE,YAAY,CAAC;IAC5B,CAAC,YAAY,EAAE,aAAa,CAAC;IAC7B,CAAC,YAAY,EAAE,WAAW,CAAC;IAC3B,CAAC,UAAU,EAAE,aAAa,CAAC;IAC3B,CAAC,UAAU,EAAE,WAAW,CAAC;IACzB,CAAC,UAAU,EAAE,YAAY,CAAC;CAC3B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,SAAS,CAAE,IAA+B;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,CAAA;IACX,CAAC;IAED,qEAAqE;IACrE,IAAI,MAAM,GAAG,IAAI;SACd,SAAS,CAAC,KAAK,CAAC;SAChB,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;IAElC,qBAAqB;IACrB,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,CAAA;IAE7B,+BAA+B;IAC/B,KAAK,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,aAAa,EAAE,CAAC;QACjD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IAC7C,CAAC;IAED,2DAA2D;IAC3D,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAA;IAE5C,uCAAuC;IACvC,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AAC3C,CAAC","sourcesContent":["/**\n * Text normaliser for the HealthcareService search index.\n *\n * Produces a lower-cased, accent-stripped, abbreviation-expanded string\n * suitable for trigram similarity matching via pg_trgm.\n *\n * The normalisation here must mirror what the Postgres migration achieves with\n * `unaccent()` for query-time comparisons. By pre-normalising at index time we\n * avoid calling `unaccent()` on every row during search, which would prevent\n * the GIN index from being used.\n */\n\n/** Expanded abbreviations map (Spanish medical/radiological context). */\nconst ABBREVIATIONS: [RegExp, string][] = [\n [/\\brm\\b/g, 'resonancia magnetica'],\n [/\\brx\\b/g, 'radiografia'],\n [/\\btac\\b/g, 'tomografia computada'],\n [/\\btc\\b/g, 'tomografia computada'],\n [/\\beco\\b/g, 'ecografia'],\n [/\\bus\\b/g, 'ecografia'],\n [/\\bder\\b/g, 'derecha'],\n [/\\bizq\\b/g, 'izquierda'],\n [/\\bint\\b/g, 'interno'],\n [/\\bext\\b/g, 'externo'],\n [/\\bsup\\b/g, 'superior'],\n [/\\binf\\b/g, 'inferior'],\n [/\\bant\\b/g, 'anterior'],\n [/\\bpost\\b/g, 'posterior'],\n [/\\bmed\\b/g, 'medico'],\n [/\\bped\\b/g, 'pediatrico'],\n [/\\bgine\\b/g, 'ginecologia'],\n [/\\boftal\\b/g, 'oftalmologia'],\n [/\\btraumat\\b/g, 'traumatologia'],\n [/\\bneurol\\b/g, 'neurologia'],\n [/\\bcardio\\b/g, 'cardiologia'],\n [/\\bderma\\b/g, 'dermatologia'],\n [/\\boncol\\b/g, 'oncologia'],\n [/\\burgen\\b/g, 'urgencias'],\n [/\\bcir\\b/g, 'cirugia'],\n [/\\bfisio\\b/g, 'fisioterapia'],\n [/\\bpsico\\b/g, 'psicologia'],\n [/\\bodont\\b/g, 'odontologia'],\n [/\\bnutri\\b/g, 'nutricion'],\n [/\\blab\\b/g, 'laboratorio'],\n [/\\bpat\\b/g, 'patologia'],\n [/\\brad\\b/g, 'radiologia']\n]\n\n/**\n * Normalise a text string for storage or query comparison.\n *\n * Steps:\n * 1. NFD decompose + strip combining diacritical marks (accent removal)\n * 2. Lower-case\n * 3. Expand known medical abbreviations\n * 4. Remove non-alphanumeric characters (keep spaces)\n * 5. Collapse whitespace and trim\n * @param text - Input text to normalize.\n * @returns Normalized text suitable for trigram index lookup.\n */\nexport function normalize (text: string | null | undefined): string {\n if (!text) {\n return ''\n }\n\n // Step 1: Remove diacritics (accent stripping via NFD decomposition)\n let result = text\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n\n // Step 2: Lower-case\n result = result.toLowerCase()\n\n // Step 3: Expand abbreviations\n for (const [pattern, expansion] of ABBREVIATIONS) {\n result = result.replace(pattern, expansion)\n }\n\n // Step 4: Remove non-alphanumeric characters (keep spaces)\n result = result.replace(/[^a-z0-9\\s]/g, ' ')\n\n // Step 5: Collapse whitespace and trim\n return result.replace(/\\s+/g, ' ').trim()\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { MedplumClient, WithId } from '@medplum/core';
2
+ import { ActivityDefinition, Device, Location, Patient, Practitioner, Schedule } from '@medplum/fhirtypes';
3
+ export declare const SERVICE_TYPE_SYSTEM = "http://example.com/test/service-type";
4
+ export declare function createActivityDefinition(medplum: MedplumClient, override?: Partial<ActivityDefinition>): Promise<WithId<ActivityDefinition>>;
5
+ export declare function createLocation(medplum: MedplumClient, override?: Partial<Location>): Promise<WithId<Location>>;
6
+ export declare function createDevice(medplum: MedplumClient, override?: Partial<Device>): Promise<WithId<Device>>;
7
+ export declare function createSchedule(medplum: MedplumClient, override?: Partial<Schedule>): Promise<WithId<Schedule>>;
8
+ export declare function createPractitioner(medplum: MedplumClient, override?: Partial<Practitioner>): Promise<WithId<Practitioner>>;
9
+ export declare function createPatient(medplum: MedplumClient, override?: Partial<Patient>): Promise<WithId<Patient>>;
10
+ //# sourceMappingURL=factories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../src/test/factories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,aAAa,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAE1G,eAAO,MAAM,mBAAmB,yCAAyC,CAAA;AAEzE,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,aAAa,EACtB,QAAQ,GAAE,OAAO,CAAC,kBAAkB,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAiBjF;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,aAAa,EACtB,QAAQ,GAAE,OAAO,CAAC,QAAQ,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAK7D;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,aAAa,EACtB,QAAQ,GAAE,OAAO,CAAC,MAAM,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAKzD;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,aAAa,EACtB,QAAQ,GAAE,OAAO,CAAC,QAAQ,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAa7D;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,aAAa,EACtB,QAAQ,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAKrE;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,QAAQ,GAAE,OAAO,CAAC,OAAO,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAK3D"}
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERVICE_TYPE_SYSTEM = void 0;
4
+ exports.createActivityDefinition = createActivityDefinition;
5
+ exports.createLocation = createLocation;
6
+ exports.createDevice = createDevice;
7
+ exports.createSchedule = createSchedule;
8
+ exports.createPractitioner = createPractitioner;
9
+ exports.createPatient = createPatient;
10
+ const core_1 = require("@medplum/core");
11
+ exports.SERVICE_TYPE_SYSTEM = 'http://example.com/test/service-type';
12
+ async function createActivityDefinition(medplum, override = {}) {
13
+ return medplum.createResource({
14
+ resourceType: 'ActivityDefinition',
15
+ status: 'active',
16
+ url: exports.SERVICE_TYPE_SYSTEM + '/' + (0, core_1.generateId)(),
17
+ code: {
18
+ coding: [{
19
+ system: exports.SERVICE_TYPE_SYSTEM,
20
+ code: 'TEST-PROCEDURE-CODE'
21
+ }]
22
+ },
23
+ timingDuration: {
24
+ value: 30 * 60,
25
+ unit: 's'
26
+ },
27
+ ...override
28
+ });
29
+ }
30
+ async function createLocation(medplum, override = {}) {
31
+ return medplum.createResource({
32
+ resourceType: 'Location',
33
+ ...override
34
+ });
35
+ }
36
+ async function createDevice(medplum, override = {}) {
37
+ return medplum.createResource({
38
+ resourceType: 'Device',
39
+ ...override
40
+ });
41
+ }
42
+ async function createSchedule(medplum, override = {}) {
43
+ return medplum.createResource({
44
+ resourceType: 'Schedule',
45
+ active: true,
46
+ actor: [],
47
+ serviceType: [{
48
+ coding: [{
49
+ system: exports.SERVICE_TYPE_SYSTEM,
50
+ code: 'TEST-PROCEDURE-CODE'
51
+ }]
52
+ }],
53
+ ...override
54
+ });
55
+ }
56
+ async function createPractitioner(medplum, override = {}) {
57
+ return medplum.createResource({
58
+ resourceType: 'Practitioner',
59
+ ...override
60
+ });
61
+ }
62
+ async function createPatient(medplum, override = {}) {
63
+ return medplum.createResource({
64
+ resourceType: 'Patient',
65
+ ...override
66
+ });
67
+ }
68
+ //# sourceMappingURL=factories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factories.js","sourceRoot":"","sources":["../../src/test/factories.ts"],"names":[],"mappings":";;;AAKA,4DAmBC;AAED,wCAOC;AAED,oCAOC;AAED,wCAeC;AAED,gDAOC;AAED,sCAOC;AA7ED,wCAAiE;AAGpD,QAAA,mBAAmB,GAAG,sCAAsC,CAAA;AAElE,KAAK,UAAU,wBAAwB,CAC5C,OAAsB,EACtB,WAAwC,EAAE;IAC1C,OAAO,OAAO,CAAC,cAAc,CAAC;QAC5B,YAAY,EAAE,oBAAoB;QAClC,MAAM,EAAE,QAAQ;QAChB,GAAG,EAAE,2BAAmB,GAAG,GAAG,GAAG,IAAA,iBAAU,GAAE;QAC7C,IAAI,EAAE;YACJ,MAAM,EAAE,CAAC;oBACP,MAAM,EAAE,2BAAmB;oBAC3B,IAAI,EAAE,qBAAqB;iBAC5B,CAAC;SACH;QACD,cAAc,EAAE;YACd,KAAK,EAAE,EAAE,GAAG,EAAE;YACd,IAAI,EAAE,GAAG;SACV;QACD,GAAG,QAAQ;KACZ,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,cAAc,CAClC,OAAsB,EACtB,WAA8B,EAAE;IAChC,OAAO,OAAO,CAAC,cAAc,CAAC;QAC5B,YAAY,EAAE,UAAU;QACxB,GAAG,QAAQ;KACZ,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,OAAsB,EACtB,WAA4B,EAAE;IAC9B,OAAO,OAAO,CAAC,cAAc,CAAC;QAC5B,YAAY,EAAE,QAAQ;QACtB,GAAG,QAAQ;KACZ,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,cAAc,CAClC,OAAsB,EACtB,WAA8B,EAAE;IAChC,OAAO,OAAO,CAAC,cAAc,CAAC;QAC5B,YAAY,EAAE,UAAU;QACxB,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,EAAE;QACT,WAAW,EAAE,CAAC;gBACZ,MAAM,EAAE,CAAC;wBACP,MAAM,EAAE,2BAAmB;wBAC3B,IAAI,EAAE,qBAAqB;qBAC5B,CAAC;aACH,CAAC;QACF,GAAG,QAAQ;KACZ,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,kBAAkB,CACtC,OAAsB,EACtB,WAAkC,EAAE;IACpC,OAAO,OAAO,CAAC,cAAc,CAAC;QAC5B,YAAY,EAAE,cAAc;QAC5B,GAAG,QAAQ;KACZ,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,aAAa,CACjC,OAAsB,EACtB,WAA6B,EAAE;IAC/B,OAAO,OAAO,CAAC,cAAc,CAAC;QAC5B,YAAY,EAAE,SAAS;QACvB,GAAG,QAAQ;KACZ,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { generateId, MedplumClient, WithId } from '@medplum/core'\nimport { ActivityDefinition, Device, Location, Patient, Practitioner, Schedule } from '@medplum/fhirtypes'\n\nexport const SERVICE_TYPE_SYSTEM = 'http://example.com/test/service-type'\n\nexport async function createActivityDefinition (\n medplum: MedplumClient,\n override: Partial<ActivityDefinition> = {}): Promise<WithId<ActivityDefinition>> {\n return medplum.createResource({\n resourceType: 'ActivityDefinition',\n status: 'active',\n url: SERVICE_TYPE_SYSTEM + '/' + generateId(),\n code: {\n coding: [{\n system: SERVICE_TYPE_SYSTEM,\n code: 'TEST-PROCEDURE-CODE'\n }]\n },\n timingDuration: {\n value: 30 * 60,\n unit: 's'\n },\n ...override\n })\n}\n\nexport async function createLocation (\n medplum: MedplumClient,\n override: Partial<Location> = {}): Promise<WithId<Location>> {\n return medplum.createResource({\n resourceType: 'Location',\n ...override\n })\n}\n\nexport async function createDevice (\n medplum: MedplumClient,\n override: Partial<Device> = {}): Promise<WithId<Device>> {\n return medplum.createResource({\n resourceType: 'Device',\n ...override\n })\n}\n\nexport async function createSchedule (\n medplum: MedplumClient,\n override: Partial<Schedule> = {}): Promise<WithId<Schedule>> {\n return medplum.createResource({\n resourceType: 'Schedule',\n active: true,\n actor: [],\n serviceType: [{\n coding: [{\n system: SERVICE_TYPE_SYSTEM,\n code: 'TEST-PROCEDURE-CODE'\n }]\n }],\n ...override\n })\n}\n\nexport async function createPractitioner (\n medplum: MedplumClient,\n override: Partial<Practitioner> = {}): Promise<WithId<Practitioner>> {\n return medplum.createResource({\n resourceType: 'Practitioner',\n ...override\n })\n}\n\nexport async function createPatient (\n medplum: MedplumClient,\n override: Partial<Patient> = {}): Promise<WithId<Patient>> {\n return medplum.createResource({\n resourceType: 'Patient',\n ...override\n })\n}\n"]}