vet-data-utils-ts 0.3.0 → 0.3.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 CHANGED
@@ -12,3 +12,9 @@ The catalogue retains longitudinal sections that also apply to animal care,
12
12
  with animal-facing labels where needed. Human advance directives
13
13
  (`LOINC|42348-3`) are deliberately excluded because they express the patient's
14
14
  own legal/autonomy decisions, not controller preferences.
15
+
16
+ Digital-twin search input contains one section, one or more resource types
17
+ allowed by that section, display text, `dateFrom` and optional `dateTo`.
18
+ `VeterinaryDigitalTwinSectionSearchProfiles` owns the corresponding
19
+ `code-display` and date claims so browser code never supplies claim names.
20
+ Identifying fields and caller-authored `claim`/`dateClaim` values are rejected.
@@ -3,11 +3,24 @@ export declare const VeterinaryDigitalTwinFormats: Readonly<{
3
3
  readonly R4: "org.hl7.fhir.r4";
4
4
  }>;
5
5
  export type VeterinaryDigitalTwinFormat = typeof VeterinaryDigitalTwinFormats[keyof typeof VeterinaryDigitalTwinFormats];
6
+ export type VeterinaryDigitalTwinResourceSearchProfile = Readonly<{
7
+ resourceType: string;
8
+ codeDisplayClaim: `${string}.code-display`;
9
+ dateClaim: `${string}.${string}`;
10
+ }>;
11
+ /**
12
+ * Searchable flat-claim projections for each veterinary section.
13
+ *
14
+ * Native FHIR resources are converted by GW into these governed claims. The
15
+ * browser selects resource families only; it never supplies claim names.
16
+ */
17
+ export declare const VeterinaryDigitalTwinSectionSearchProfiles: Readonly<Record<string, readonly VeterinaryDigitalTwinResourceSearchProfile[]>>;
6
18
  export type VeterinaryDigitalTwinSearch = Readonly<{
7
19
  section: string;
8
- resourceType: string;
9
- claim: string;
10
- value: string;
20
+ resourceTypes: readonly string[];
21
+ text: string;
22
+ dateFrom: string;
23
+ dateTo?: string;
11
24
  }>;
12
- /** Bounds one section-first pseudonymous search and rejects direct identifiers. */
25
+ /** Bounds one section-first pseudonymous search and rejects direct identifiers and caller-authored claims. */
13
26
  export declare function parseVeterinaryDigitalTwinSearch(input: unknown): VeterinaryDigitalTwinSearch;
@@ -1,24 +1,61 @@
1
- import { isVeterinarySummarySection } from './veterinary-sections.js';
1
+ import { VeterinarySummarySections, isVeterinarySummarySection } from './veterinary-sections.js';
2
2
  export const VeterinaryDigitalTwinFormats = Object.freeze({ Api: 'org.hl7.fhir.api', R4: 'org.hl7.fhir.r4' });
3
- /** Bounds one section-first pseudonymous search and rejects direct identifiers. */
3
+ const profile = (resourceType, dateClaim) => Object.freeze({ resourceType, codeDisplayClaim: `${resourceType}.code-display`, dateClaim });
4
+ /**
5
+ * Searchable flat-claim projections for each veterinary section.
6
+ *
7
+ * Native FHIR resources are converted by GW into these governed claims. The
8
+ * browser selects resource families only; it never supplies claim names.
9
+ */
10
+ export const VeterinaryDigitalTwinSectionSearchProfiles = Object.freeze({
11
+ [VeterinarySummarySections.Problems.value]: Object.freeze([profile('Condition', 'Condition.onset-datetime')]),
12
+ [VeterinarySummarySections.Allergies.value]: Object.freeze([profile('AllergyIntolerance', 'AllergyIntolerance.onset-datetime')]),
13
+ [VeterinarySummarySections.Medications.value]: Object.freeze([profile('MedicationStatement', 'MedicationStatement.effective')]),
14
+ [VeterinarySummarySections.Immunizations.value]: Object.freeze([profile('Immunization', 'Immunization.date')]),
15
+ [VeterinarySummarySections.Results.value]: Object.freeze([
16
+ profile('Observation', 'Observation.date'),
17
+ profile('DiagnosticReport', 'DiagnosticReport.date'),
18
+ ]),
19
+ [VeterinarySummarySections.Procedures.value]: Object.freeze([profile('Procedure', 'Procedure.date')]),
20
+ [VeterinarySummarySections.VitalSigns.value]: Object.freeze([profile('Observation', 'Observation.date')]),
21
+ [VeterinarySummarySections.EnvironmentAndLifestyle.value]: Object.freeze([profile('Observation', 'Observation.date')]),
22
+ [VeterinarySummarySections.FunctionalStatus.value]: Object.freeze([profile('Condition', 'Condition.onset-datetime')]),
23
+ [VeterinarySummarySections.PastIllness.value]: Object.freeze([profile('Condition', 'Condition.onset-datetime')]),
24
+ [VeterinarySummarySections.ReproductiveHistory.value]: Object.freeze([profile('Observation', 'Observation.date')]),
25
+ });
26
+ /** Bounds one section-first pseudonymous search and rejects direct identifiers and caller-authored claims. */
4
27
  export function parseVeterinaryDigitalTwinSearch(input) {
5
28
  const value = (input || {});
6
29
  for (const forbidden of ['name', 'email', 'telephone', 'identifier', 'subjectDid', 'cardNumber']) {
7
30
  if (String(value[forbidden] || '').trim())
8
31
  throw new TypeError('veterinary_digital_twin_identifying_filter_forbidden');
9
32
  }
33
+ if (String(value.claim || '').trim() || String(value.dateClaim || '').trim()) {
34
+ throw new TypeError('veterinary_digital_twin_claim_forbidden');
35
+ }
10
36
  const section = String(value.section || '').trim();
11
- const resourceType = String(value.resourceType || '').trim();
12
- const claim = String(value.claim || '').trim();
13
- const filterValue = String(value.value || '').trim();
14
37
  if (!isVeterinarySummarySection(section))
15
38
  throw new TypeError('veterinary_digital_twin_section_invalid');
16
- if (!/^[A-Z][A-Za-z]{1,63}$/.test(resourceType))
39
+ const allowedProfiles = VeterinaryDigitalTwinSectionSearchProfiles[section] || [];
40
+ if (allowedProfiles.length === 0)
41
+ throw new TypeError('veterinary_digital_twin_section_not_searchable');
42
+ const rawResourceTypes = Array.isArray(value.resourceTypes) ? value.resourceTypes : [value.resourceType];
43
+ const resourceTypes = Array.from(new Set(rawResourceTypes.map(item => String(item || '').trim()).filter(Boolean)));
44
+ if (resourceTypes.length === 0 || resourceTypes.some(resourceType => !allowedProfiles.some(item => item.resourceType === resourceType))) {
17
45
  throw new TypeError('veterinary_digital_twin_resource_invalid');
18
- if (!new RegExp(`^${resourceType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.[A-Za-z0-9][A-Za-z0-9.-]{0,127}$`).test(claim)) {
19
- throw new TypeError('veterinary_digital_twin_claim_invalid');
20
46
  }
21
- if (!filterValue || filterValue.length > 256)
22
- throw new TypeError('veterinary_digital_twin_value_invalid');
23
- return Object.freeze({ section, resourceType, claim, value: filterValue });
47
+ const text = String(value.text || value.value || '').trim();
48
+ if (!text || text.length > 256)
49
+ throw new TypeError('veterinary_digital_twin_text_invalid');
50
+ const dateFrom = parseIsoDate(value.dateFrom, 'date_from');
51
+ const dateTo = String(value.dateTo || '').trim() ? parseIsoDate(value.dateTo, 'date_to') : undefined;
52
+ if (dateTo && Date.parse(dateTo) < Date.parse(dateFrom))
53
+ throw new TypeError('veterinary_digital_twin_date_range_invalid');
54
+ return Object.freeze({ section, resourceTypes: Object.freeze(resourceTypes), text, dateFrom, ...(dateTo ? { dateTo } : {}) });
55
+ }
56
+ function parseIsoDate(value, field) {
57
+ const normalized = String(value || '').trim();
58
+ if (!normalized || Number.isNaN(Date.parse(normalized)))
59
+ throw new TypeError(`veterinary_digital_twin_${field}_invalid`);
60
+ return normalized;
24
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",