vet-data-utils-ts 0.4.12 → 0.5.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
@@ -8,6 +8,21 @@ Current surfaces cover the canonical animal-card DID, veterinary summary
8
8
  sections, animal-only emergency data, pseudonymous DigitalTwin search input and
9
9
  veterinary assistant intents.
10
10
 
11
+ FHIR R5 ResearchStudy screens consume the exact 25 resource-specific search
12
+ parameters and the separate associated-party projection from
13
+ `vet-data-utils-ts/research-study`. The complete `associatedParty` object stays
14
+ beside each flat projection so repeating roles, periods and classifiers cannot
15
+ be attributed to the wrong party. A party classifier describes an organization
16
+ category; it never represents Consent or CRUDS authorization. See
17
+ [`docs/research-study-r5-contract.md`](docs/research-study-r5-contract.md).
18
+
19
+ ResearchStudy professional teams use native FHIR R5 Group resources from
20
+ `vet-data-utils-ts/group`: required type and membership codes, the exact twelve
21
+ resource-specific SearchParameters, canonical flat claim projection and
22
+ enumerated PractitionerRole member normalization. Group membership is
23
+ descriptive and never carries authorization, permission or scope. See
24
+ [`docs/group-r5-contract.md`](docs/group-r5-contract.md).
25
+
11
26
  Reusable Communication screens receive immutable workflow presets from
12
27
  `vet-data-utils-ts/communication`. The research-agreement screen is fixed to
13
28
  FHIR `notification` plus HL7 v3 ActReason `HRESCH` and does not expose a topic
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Browser-safe FHIR R5 Group contract for research-team descriptions.
3
+ *
4
+ * Group membership is descriptive. It never grants authorization, permission
5
+ * or a SMART scope; those decisions remain separate Consent and credential
6
+ * contracts.
7
+ *
8
+ * @see https://hl7.org/fhir/R5/group.html
9
+ * @see https://hl7.org/fhir/R5/group-search.html
10
+ */
11
+ export declare const GroupR5Version: "5.0.0";
12
+ export declare const GroupR5TypeSystem: "http://hl7.org/fhir/group-type";
13
+ export declare const GroupR5TypeCodes: readonly ["person", "animal", "practitioner", "device", "careteam", "healthcareservice", "location", "organization", "relatedperson", "specimen"];
14
+ export type GroupR5TypeCode = (typeof GroupR5TypeCodes)[number];
15
+ export declare const GroupR5MembershipSystem: "http://hl7.org/fhir/group-membership-basis";
16
+ export declare const GroupR5MembershipCodes: readonly ["definitional", "enumerated"];
17
+ export type GroupR5MembershipCode = (typeof GroupR5MembershipCodes)[number];
18
+ export declare const GroupR5Bindings: Readonly<{
19
+ readonly type: Readonly<{
20
+ valueSet: "http://hl7.org/fhir/ValueSet/group-type";
21
+ strength: "required";
22
+ }>;
23
+ readonly membership: Readonly<{
24
+ valueSet: "http://hl7.org/fhir/ValueSet/group-membership-basis";
25
+ strength: "required";
26
+ }>;
27
+ }>;
28
+ export type GroupSearchParameterType = 'composite' | 'reference' | 'string' | 'token';
29
+ export type GroupR5SearchParameter = Readonly<{
30
+ code: string;
31
+ type: GroupSearchParameterType;
32
+ expression: string;
33
+ components: readonly string[];
34
+ }>;
35
+ /** Exact resource-specific SearchParameters published by FHIR R5 5.0.0. */
36
+ export declare const GroupR5SearchParameterCatalog: readonly GroupR5SearchParameter[];
37
+ declare const standardSearchClaims: readonly `Group.${string}`[];
38
+ /** Canonical claims derived only from the official resource-specific catalogue. */
39
+ export declare const GroupFlatClaimCatalog: Readonly<{
40
+ standardSearch: readonly `Group.${string}`[];
41
+ all: readonly `Group.${string}`[];
42
+ }>;
43
+ export type GroupR5SearchClaim = (typeof standardSearchClaims)[number];
44
+ export type GroupR5SearchClaims = Readonly<Partial<Record<GroupR5SearchClaim, readonly string[]>>>;
45
+ export type GroupR5Reference = Readonly<{
46
+ reference: string;
47
+ type?: string;
48
+ display?: string;
49
+ }>;
50
+ export type GroupR5Period = Readonly<{
51
+ start?: string;
52
+ end?: string;
53
+ }>;
54
+ export type GroupPractitionerRoleMember = Readonly<{
55
+ entity: Readonly<{
56
+ reference: `PractitionerRole/${string}`;
57
+ type: 'PractitionerRole';
58
+ display?: string;
59
+ }>;
60
+ period?: GroupR5Period;
61
+ inactive?: boolean;
62
+ }>;
63
+ export type GroupR5ResearchTeam = Readonly<Record<string, unknown> & {
64
+ resourceType: 'Group';
65
+ id?: string;
66
+ type: 'practitioner';
67
+ membership: 'enumerated';
68
+ member: readonly GroupPractitionerRoleMember[];
69
+ }>;
70
+ /**
71
+ * Normalizes the members used by an enumerated professional research team.
72
+ * Every entity is a native PractitionerRole reference; a Practitioner or an
73
+ * employee identifier cannot substitute for the registered role assignment.
74
+ */
75
+ export declare function normalizeGroupPractitionerRoleMembers(input: unknown): readonly GroupPractitionerRoleMember[];
76
+ /**
77
+ * Normalizes the Group profile used to describe an explicit professional
78
+ * ResearchStudy team. This data is descriptive and authorization-shaped
79
+ * properties are rejected at every depth.
80
+ */
81
+ export declare function normalizeGroupR5ResearchTeam(input: unknown): GroupR5ResearchTeam;
82
+ /**
83
+ * Projects all populated official resource-specific R5 Group SearchParameters
84
+ * into flat `resource.meta.claims`. A referenced characteristic is indexed by
85
+ * `characteristic-reference`, not as the token-valued `value` parameter.
86
+ *
87
+ * @see https://hl7.org/fhir/R5/group-search.html
88
+ */
89
+ export declare function projectGroupR5SearchClaims(input: unknown): GroupR5SearchClaims;
90
+ export {};
package/dist/group.js ADDED
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Browser-safe FHIR R5 Group contract for research-team descriptions.
3
+ *
4
+ * Group membership is descriptive. It never grants authorization, permission
5
+ * or a SMART scope; those decisions remain separate Consent and credential
6
+ * contracts.
7
+ *
8
+ * @see https://hl7.org/fhir/R5/group.html
9
+ * @see https://hl7.org/fhir/R5/group-search.html
10
+ */
11
+ export const GroupR5Version = '5.0.0';
12
+ export const GroupR5TypeSystem = 'http://hl7.org/fhir/group-type';
13
+ export const GroupR5TypeCodes = Object.freeze([
14
+ 'person',
15
+ 'animal',
16
+ 'practitioner',
17
+ 'device',
18
+ 'careteam',
19
+ 'healthcareservice',
20
+ 'location',
21
+ 'organization',
22
+ 'relatedperson',
23
+ 'specimen',
24
+ ]);
25
+ export const GroupR5MembershipSystem = 'http://hl7.org/fhir/group-membership-basis';
26
+ export const GroupR5MembershipCodes = Object.freeze(['definitional', 'enumerated']);
27
+ export const GroupR5Bindings = Object.freeze({
28
+ type: Object.freeze({
29
+ valueSet: 'http://hl7.org/fhir/ValueSet/group-type',
30
+ strength: 'required',
31
+ }),
32
+ membership: Object.freeze({
33
+ valueSet: 'http://hl7.org/fhir/ValueSet/group-membership-basis',
34
+ strength: 'required',
35
+ }),
36
+ });
37
+ const searchParameter = (code, type, expression, components = []) => Object.freeze({
38
+ code,
39
+ type,
40
+ expression,
41
+ components: Object.freeze([...components]),
42
+ });
43
+ /** Exact resource-specific SearchParameters published by FHIR R5 5.0.0. */
44
+ export const GroupR5SearchParameterCatalog = Object.freeze([
45
+ searchParameter('characteristic', 'token', 'Group.characteristic.code'),
46
+ searchParameter('characteristic-reference', 'reference', '(Group.characteristic.value.ofType(Reference))'),
47
+ searchParameter('characteristic-value', 'composite', 'Group.characteristic', [
48
+ 'http://hl7.org/fhir/SearchParameter/Group-characteristic',
49
+ 'http://hl7.org/fhir/SearchParameter/Group-value',
50
+ ]),
51
+ searchParameter('code', 'token', 'Group.code'),
52
+ searchParameter('exclude', 'token', 'Group.characteristic.exclude'),
53
+ searchParameter('identifier', 'token', 'Group.identifier'),
54
+ searchParameter('managing-entity', 'reference', 'Group.managingEntity'),
55
+ searchParameter('member', 'reference', 'Group.member.entity'),
56
+ searchParameter('membership', 'token', 'Group.membership'),
57
+ searchParameter('name', 'string', 'Group.name'),
58
+ searchParameter('type', 'token', 'Group.type'),
59
+ searchParameter('value', 'token', '(Group.characteristic.value.ofType(CodeableConcept)) | (Group.characteristic.value.ofType(boolean))'),
60
+ ]);
61
+ const standardSearchClaims = Object.freeze(GroupR5SearchParameterCatalog.map(parameter => `Group.${parameter.code}`));
62
+ /** Canonical claims derived only from the official resource-specific catalogue. */
63
+ export const GroupFlatClaimCatalog = Object.freeze({
64
+ standardSearch: standardSearchClaims,
65
+ all: standardSearchClaims,
66
+ });
67
+ /**
68
+ * Normalizes the members used by an enumerated professional research team.
69
+ * Every entity is a native PractitionerRole reference; a Practitioner or an
70
+ * employee identifier cannot substitute for the registered role assignment.
71
+ */
72
+ export function normalizeGroupPractitionerRoleMembers(input) {
73
+ if (!Array.isArray(input))
74
+ throw new TypeError('group_members_invalid');
75
+ return Object.freeze(input.map((value, index) => {
76
+ if (!isRecord(value) || containsAuthorizationProperty(value)) {
77
+ throw new TypeError(containsAuthorizationProperty(value) ? 'group_authorization_property_forbidden' : `group_member_${index}_invalid`);
78
+ }
79
+ if (!isRecord(value.entity))
80
+ throw new TypeError(`group_member_${index}_practitioner_role_reference_invalid`);
81
+ const reference = optionalString(value.entity.reference);
82
+ const declaredType = optionalString(value.entity.type);
83
+ if (!reference || !/^PractitionerRole\/[A-Za-z0-9.-]{1,64}$/.test(reference) || (declaredType && declaredType !== 'PractitionerRole')) {
84
+ throw new TypeError(`group_member_${index}_practitioner_role_reference_invalid`);
85
+ }
86
+ const display = optionalString(value.entity.display);
87
+ const period = value.period === undefined ? undefined : normalizePeriod(value.period, index);
88
+ if (value.inactive !== undefined && typeof value.inactive !== 'boolean') {
89
+ throw new TypeError(`group_member_${index}_inactive_invalid`);
90
+ }
91
+ return Object.freeze({
92
+ entity: Object.freeze({
93
+ reference: reference,
94
+ type: 'PractitionerRole',
95
+ ...(display ? { display } : {}),
96
+ }),
97
+ ...(period ? { period } : {}),
98
+ ...(value.inactive !== undefined ? { inactive: value.inactive } : {}),
99
+ });
100
+ }));
101
+ }
102
+ /**
103
+ * Normalizes the Group profile used to describe an explicit professional
104
+ * ResearchStudy team. This data is descriptive and authorization-shaped
105
+ * properties are rejected at every depth.
106
+ */
107
+ export function normalizeGroupR5ResearchTeam(input) {
108
+ if (!isRecord(input) || input.resourceType !== 'Group')
109
+ throw new TypeError('group_resource_invalid');
110
+ if (containsAuthorizationProperty(input))
111
+ throw new TypeError('group_authorization_property_forbidden');
112
+ if (input.type !== 'practitioner')
113
+ throw new TypeError('group_research_team_type_invalid');
114
+ if (input.membership !== 'enumerated')
115
+ throw new TypeError('group_research_team_membership_invalid');
116
+ const member = normalizeGroupPractitionerRoleMembers(input.member);
117
+ return Object.freeze({ ...input, type: 'practitioner', membership: 'enumerated', member });
118
+ }
119
+ /**
120
+ * Projects all populated official resource-specific R5 Group SearchParameters
121
+ * into flat `resource.meta.claims`. A referenced characteristic is indexed by
122
+ * `characteristic-reference`, not as the token-valued `value` parameter.
123
+ *
124
+ * @see https://hl7.org/fhir/R5/group-search.html
125
+ */
126
+ export function projectGroupR5SearchClaims(input) {
127
+ if (!isRecord(input) || input.resourceType !== 'Group')
128
+ throw new TypeError('group_resource_invalid');
129
+ if (containsAuthorizationProperty(input))
130
+ throw new TypeError('group_authorization_property_forbidden');
131
+ if (!GroupR5TypeCodes.includes(input.type))
132
+ throw new TypeError('group_type_invalid');
133
+ if (!GroupR5MembershipCodes.includes(input.membership)) {
134
+ throw new TypeError('group_membership_invalid');
135
+ }
136
+ const characteristics = normalizeRecords(input.characteristic, 'group_characteristic_invalid');
137
+ const characteristicCodes = characteristics.flatMap(item => conceptTokens(item.code));
138
+ const characteristicValues = characteristics.flatMap(characteristicTokenValues);
139
+ const claims = {};
140
+ addClaim(claims, 'Group.characteristic', characteristicCodes);
141
+ addClaim(claims, 'Group.characteristic-reference', characteristics.flatMap(item => referenceTokens(item.valueReference)));
142
+ addClaim(claims, 'Group.characteristic-value', characteristics.flatMap(item => conceptTokens(item.code).flatMap(code => characteristicTokenValues(item).map(value => `${code}$${value}`))));
143
+ addClaim(claims, 'Group.code', conceptTokens(input.code));
144
+ addClaim(claims, 'Group.exclude', characteristics.flatMap(item => typeof item.exclude === 'boolean' ? [String(item.exclude)] : []));
145
+ addClaim(claims, 'Group.identifier', identifierTokens(input.identifier));
146
+ addClaim(claims, 'Group.managing-entity', referenceTokens(input.managingEntity));
147
+ addClaim(claims, 'Group.member', normalizeRecords(input.member, 'group_members_invalid').flatMap(item => referenceTokens(item.entity)));
148
+ addClaim(claims, 'Group.membership', [input.membership]);
149
+ addClaim(claims, 'Group.name', strings(input.name));
150
+ addClaim(claims, 'Group.type', [input.type]);
151
+ addClaim(claims, 'Group.value', characteristicValues);
152
+ return Object.freeze(claims);
153
+ }
154
+ function addClaim(claims, name, values) {
155
+ const normalized = [...new Set(values.flatMap(value => strings(value)))];
156
+ if (normalized.length > 0)
157
+ claims[name] = Object.freeze(normalized);
158
+ }
159
+ function identifierTokens(value) {
160
+ return normalizeRecords(value, 'group_identifier_invalid').flatMap(identifier => {
161
+ const system = optionalString(identifier.system);
162
+ const identifierValue = optionalString(identifier.value);
163
+ return identifierValue ? [`${system ? `${system}|` : ''}${identifierValue}`] : [];
164
+ });
165
+ }
166
+ function conceptTokens(value) {
167
+ if (!isRecord(value))
168
+ return [];
169
+ const coding = normalizeRecords(value.coding, 'group_coding_invalid').flatMap(item => {
170
+ const system = optionalString(item.system);
171
+ const code = optionalString(item.code);
172
+ return code ? [`${system ? `${system}|` : ''}${code}`] : [];
173
+ });
174
+ return coding.length > 0 ? coding : strings(value.text);
175
+ }
176
+ function characteristicTokenValues(value) {
177
+ if (typeof value.valueBoolean === 'boolean')
178
+ return [String(value.valueBoolean)];
179
+ return conceptTokens(value.valueCodeableConcept);
180
+ }
181
+ function referenceTokens(value) {
182
+ return isRecord(value) ? strings(value.reference) : [];
183
+ }
184
+ function normalizeRecords(value, error) {
185
+ if (value === undefined)
186
+ return [];
187
+ if (!Array.isArray(value) || !value.every(isRecord))
188
+ throw new TypeError(error);
189
+ return value;
190
+ }
191
+ function normalizePeriod(value, index) {
192
+ if (!isRecord(value))
193
+ throw new TypeError(`group_member_${index}_period_invalid`);
194
+ const start = optionalString(value.start);
195
+ const end = optionalString(value.end);
196
+ if (!start && !end)
197
+ throw new TypeError(`group_member_${index}_period_invalid`);
198
+ if ((start && !isFhirDateTime(start)) || (end && !isFhirDateTime(end))) {
199
+ throw new TypeError(`group_member_${index}_period_invalid`);
200
+ }
201
+ if (start && end && Date.parse(end) < Date.parse(start))
202
+ throw new TypeError(`group_member_${index}_period_invalid`);
203
+ return Object.freeze({ ...(start ? { start } : {}), ...(end ? { end } : {}) });
204
+ }
205
+ function containsAuthorizationProperty(value, seen = new WeakSet()) {
206
+ if (!value || typeof value !== 'object')
207
+ return false;
208
+ if (seen.has(value))
209
+ return false;
210
+ seen.add(value);
211
+ if (Array.isArray(value))
212
+ return value.some(item => containsAuthorizationProperty(item, seen));
213
+ return Object.entries(value).some(([name, nested]) => ['authorization', 'permission', 'scope'].includes(name) || containsAuthorizationProperty(nested, seen));
214
+ }
215
+ function strings(value) {
216
+ const normalized = optionalString(value);
217
+ return normalized ? [normalized] : [];
218
+ }
219
+ function optionalString(value) {
220
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
221
+ }
222
+ function isFhirDateTime(value) {
223
+ return /^\d{4}(?:-(?:0[1-9]|1[0-2])(?:-(?:0[1-9]|[12]\d|3[01])(?:T.+)?)?)?$/.test(value);
224
+ }
225
+ function isRecord(value) {
226
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
227
+ }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,9 @@ export * from './clinical-terminology.js';
5
5
  export * from './digital-twin.js';
6
6
  export * from './emergency.js';
7
7
  export * from './financial.js';
8
+ export * from './group.js';
8
9
  export * from './iso-jurisdictions.js';
9
10
  export * from './organization-application.js';
11
+ export * from './research-study.js';
10
12
  export * from './sectors.js';
11
13
  export * from './veterinary-sections.js';
package/dist/index.js CHANGED
@@ -5,7 +5,9 @@ export * from './clinical-terminology.js';
5
5
  export * from './digital-twin.js';
6
6
  export * from './emergency.js';
7
7
  export * from './financial.js';
8
+ export * from './group.js';
8
9
  export * from './iso-jurisdictions.js';
9
10
  export * from './organization-application.js';
11
+ export * from './research-study.js';
10
12
  export * from './sectors.js';
11
13
  export * from './veterinary-sections.js';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Browser-safe FHIR R5 ResearchStudy flat-claim contract.
3
+ *
4
+ * The standard catalogue is copied from the R5 SearchParameter definitions,
5
+ * not inferred from similarly named resource elements. In particular, the
6
+ * `date` SearchParameter targets `ResearchStudy.period`, not the publication
7
+ * date element. Associated-party projections are indexed separately because
8
+ * base R5 defines no ResearchStudy party SearchParameter.
9
+ *
10
+ * @see https://hl7.org/fhir/R5/researchstudy.html
11
+ * @see https://hl7.org/fhir/R5/researchstudy-search.html
12
+ */
13
+ export declare const ResearchStudyR5Version: "5.0.0";
14
+ /**
15
+ * Immutable codes defined by the FHIR R5 5.0.0 ResearchStudy party-role code
16
+ * system. The element binding remains extensible; this catalogue is the closed
17
+ * set a portal may offer by default, not a validator for every conformant
18
+ * external Coding.
19
+ *
20
+ * @see https://hl7.org/fhir/R5/codesystem-research-study-party-role.html
21
+ */
22
+ export declare const ResearchStudyR5PartyRoleSystem: "http://hl7.org/fhir/research-study-party-role";
23
+ export declare const ResearchStudyR5PartyRoleCodes: readonly ["sponsor", "lead-sponsor", "sponsor-investigator", "primary-investigator", "collaborator", "funding-source", "general-contact", "recruitment-contact", "sub-investigator", "study-director", "study-chair", "irb"];
24
+ export type ResearchStudyR5PartyRoleCode = (typeof ResearchStudyR5PartyRoleCodes)[number];
25
+ export declare const ResearchStudyAssociatedPartyBindings: Readonly<{
26
+ readonly role: Readonly<{
27
+ valueSet: "http://hl7.org/fhir/ValueSet/research-study-party-role";
28
+ strength: "extensible";
29
+ }>;
30
+ readonly classifier: Readonly<{
31
+ valueSet: "http://hl7.org/fhir/ValueSet/research-study-party-organization-type";
32
+ strength: "example";
33
+ }>;
34
+ }>;
35
+ export type ResearchStudySearchParameterType = 'composite' | 'date' | 'number' | 'reference' | 'string' | 'token';
36
+ export type ResearchStudyR5SearchParameter = Readonly<{
37
+ code: string;
38
+ type: ResearchStudySearchParameterType;
39
+ expression: string;
40
+ components: readonly string[];
41
+ }>;
42
+ /** Exact resource-specific SearchParameters published by FHIR R5 5.0.0. */
43
+ export declare const ResearchStudyR5SearchParameterCatalog: readonly ResearchStudyR5SearchParameter[];
44
+ declare const standardSearchClaims: readonly `ResearchStudy.${string}`[];
45
+ declare const associatedPartyClaims: readonly ["ResearchStudy.associated-party-name", "ResearchStudy.associated-party-role", "ResearchStudy.associated-party-period-start", "ResearchStudy.associated-party-period-end", "ResearchStudy.associated-party-classifier", "ResearchStudy.associated-party-party"];
46
+ /**
47
+ * Governed flat claims for ResearchStudy.
48
+ *
49
+ * `standardSearch` contains only official R5 SearchParameters. The
50
+ * `associatedParty` list is an explicit operational projection of the R5
51
+ * backbone element; none of those names is advertised as a standard search
52
+ * parameter and `classifier` never carries CRUD or authorization policy.
53
+ */
54
+ export declare const ResearchStudyFlatClaimCatalog: Readonly<{
55
+ standardSearch: readonly `ResearchStudy.${string}`[];
56
+ associatedParty: readonly ["ResearchStudy.associated-party-name", "ResearchStudy.associated-party-role", "ResearchStudy.associated-party-period-start", "ResearchStudy.associated-party-period-end", "ResearchStudy.associated-party-classifier", "ResearchStudy.associated-party-party"];
57
+ all: readonly `ResearchStudy.${string}`[];
58
+ }>;
59
+ export type ResearchStudyCoding = Readonly<{
60
+ system?: string;
61
+ code?: string;
62
+ display?: string;
63
+ }>;
64
+ export type ResearchStudyCodeableConcept = Readonly<{
65
+ coding?: readonly ResearchStudyCoding[];
66
+ text?: string;
67
+ }>;
68
+ export type ResearchStudyPeriod = Readonly<{
69
+ start?: string;
70
+ end?: string;
71
+ }>;
72
+ export type ResearchStudyAssociatedPartyReferenceType = 'Organization' | 'Practitioner' | 'PractitionerRole';
73
+ export type ResearchStudyAssociatedPartyReference = Readonly<{
74
+ reference: string;
75
+ type?: ResearchStudyAssociatedPartyReferenceType;
76
+ display?: string;
77
+ }>;
78
+ export type ResearchStudyAssociatedParty = Readonly<{
79
+ name?: string;
80
+ role: ResearchStudyCodeableConcept;
81
+ period?: readonly ResearchStudyPeriod[];
82
+ classifier?: readonly ResearchStudyCodeableConcept[];
83
+ party?: ResearchStudyAssociatedPartyReference;
84
+ }>;
85
+ export type ResearchStudyAssociatedPartyClaimProjection = Readonly<{
86
+ /** The complete party is retained beside its derived claims to preserve correlation. */
87
+ associatedParty: ResearchStudyAssociatedParty;
88
+ claims: Readonly<Record<(typeof associatedPartyClaims)[number], readonly string[]>>;
89
+ }>;
90
+ export type ResearchStudyR5SearchClaim = (typeof standardSearchClaims)[number];
91
+ export type ResearchStudyR5SearchClaims = Readonly<Partial<Record<ResearchStudyR5SearchClaim, readonly string[]>>>;
92
+ /**
93
+ * Projects the official resource-specific R5 SearchParameters from one native
94
+ * ResearchStudy into flat `resource.meta.claims`. Composite values use FHIR's
95
+ * `$` component separator; associatedParty remains a separate projection and
96
+ * never becomes authorization data.
97
+ *
98
+ * @see https://hl7.org/fhir/R5/researchstudy-search.html
99
+ */
100
+ export declare function projectResearchStudyR5SearchClaims(input: unknown): ResearchStudyR5SearchClaims;
101
+ /**
102
+ * Normalizes the R5 `associatedParty[]` backbone without flattening parties
103
+ * into unrelated parallel arrays. This function validates data shape only; it
104
+ * does not grant study access or interpret classifier values as permissions.
105
+ */
106
+ export declare function normalizeResearchStudyAssociatedParties(input: unknown): readonly ResearchStudyAssociatedParty[];
107
+ /**
108
+ * Produces one flat-claim map per associated party. Keeping the normalized
109
+ * party beside each map preserves the party/role/period/classifier boundary
110
+ * required when a later Bundle PATCH changes one party.
111
+ */
112
+ export declare function projectResearchStudyAssociatedPartyClaims(input: unknown): readonly ResearchStudyAssociatedPartyClaimProjection[];
113
+ export {};
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Browser-safe FHIR R5 ResearchStudy flat-claim contract.
3
+ *
4
+ * The standard catalogue is copied from the R5 SearchParameter definitions,
5
+ * not inferred from similarly named resource elements. In particular, the
6
+ * `date` SearchParameter targets `ResearchStudy.period`, not the publication
7
+ * date element. Associated-party projections are indexed separately because
8
+ * base R5 defines no ResearchStudy party SearchParameter.
9
+ *
10
+ * @see https://hl7.org/fhir/R5/researchstudy.html
11
+ * @see https://hl7.org/fhir/R5/researchstudy-search.html
12
+ */
13
+ export const ResearchStudyR5Version = '5.0.0';
14
+ /**
15
+ * Immutable codes defined by the FHIR R5 5.0.0 ResearchStudy party-role code
16
+ * system. The element binding remains extensible; this catalogue is the closed
17
+ * set a portal may offer by default, not a validator for every conformant
18
+ * external Coding.
19
+ *
20
+ * @see https://hl7.org/fhir/R5/codesystem-research-study-party-role.html
21
+ */
22
+ export const ResearchStudyR5PartyRoleSystem = 'http://hl7.org/fhir/research-study-party-role';
23
+ export const ResearchStudyR5PartyRoleCodes = Object.freeze([
24
+ 'sponsor',
25
+ 'lead-sponsor',
26
+ 'sponsor-investigator',
27
+ 'primary-investigator',
28
+ 'collaborator',
29
+ 'funding-source',
30
+ 'general-contact',
31
+ 'recruitment-contact',
32
+ 'sub-investigator',
33
+ 'study-director',
34
+ 'study-chair',
35
+ 'irb',
36
+ ]);
37
+ export const ResearchStudyAssociatedPartyBindings = Object.freeze({
38
+ role: Object.freeze({
39
+ valueSet: 'http://hl7.org/fhir/ValueSet/research-study-party-role',
40
+ strength: 'extensible',
41
+ }),
42
+ classifier: Object.freeze({
43
+ valueSet: 'http://hl7.org/fhir/ValueSet/research-study-party-organization-type',
44
+ strength: 'example',
45
+ }),
46
+ });
47
+ const searchParameter = (code, type, expression, components = []) => Object.freeze({
48
+ code,
49
+ type,
50
+ expression,
51
+ components: Object.freeze([...components]),
52
+ });
53
+ /** Exact resource-specific SearchParameters published by FHIR R5 5.0.0. */
54
+ export const ResearchStudyR5SearchParameterCatalog = Object.freeze([
55
+ searchParameter('classifier', 'token', 'ResearchStudy.classifier'),
56
+ searchParameter('condition', 'token', 'ResearchStudy.condition'),
57
+ searchParameter('date', 'date', 'ResearchStudy.period'),
58
+ searchParameter('description', 'string', 'ResearchStudy.description'),
59
+ searchParameter('eligibility', 'reference', 'ResearchStudy.recruitment.eligibility'),
60
+ searchParameter('focus-code', 'token', 'ResearchStudy.focus.concept'),
61
+ searchParameter('focus-reference', 'reference', 'ResearchStudy.focus.reference'),
62
+ searchParameter('identifier', 'token', 'ResearchStudy.identifier'),
63
+ searchParameter('keyword', 'token', 'ResearchStudy.keyword'),
64
+ searchParameter('name', 'string', 'ResearchStudy.name'),
65
+ searchParameter('objective-description', 'string', 'ResearchStudy.objective.description'),
66
+ searchParameter('objective-type', 'token', 'ResearchStudy.objective.type'),
67
+ searchParameter('part-of', 'reference', 'ResearchStudy.partOf'),
68
+ searchParameter('phase', 'token', 'ResearchStudy.phase'),
69
+ searchParameter('progress-status-state-actual', 'composite', 'ResearchStudy.progressStatus', [
70
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-state',
71
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-actual',
72
+ ]),
73
+ searchParameter('progress-status-state-period', 'composite', 'ResearchStudy.progressStatus', [
74
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-state',
75
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-period',
76
+ ]),
77
+ searchParameter('progress-status-state-period-actual', 'composite', 'ResearchStudy.progressStatus', [
78
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-state',
79
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-period',
80
+ 'http://hl7.org/fhir/SearchParameter/ResearchStudy-actual',
81
+ ]),
82
+ searchParameter('protocol', 'reference', 'ResearchStudy.protocol'),
83
+ searchParameter('recruitment-actual', 'number', 'ResearchStudy.recruitment.actualNumber'),
84
+ searchParameter('recruitment-target', 'number', 'ResearchStudy.recruitment.targetNumber'),
85
+ searchParameter('region', 'token', 'ResearchStudy.region'),
86
+ searchParameter('site', 'reference', 'ResearchStudy.site'),
87
+ searchParameter('status', 'token', 'ResearchStudy.status'),
88
+ searchParameter('study-design', 'token', 'ResearchStudy.studyDesign'),
89
+ searchParameter('title', 'string', 'ResearchStudy.title'),
90
+ ]);
91
+ const standardSearchClaims = Object.freeze(ResearchStudyR5SearchParameterCatalog.map(parameter => `ResearchStudy.${parameter.code}`));
92
+ const associatedPartyClaims = Object.freeze([
93
+ 'ResearchStudy.associated-party-name',
94
+ 'ResearchStudy.associated-party-role',
95
+ 'ResearchStudy.associated-party-period-start',
96
+ 'ResearchStudy.associated-party-period-end',
97
+ 'ResearchStudy.associated-party-classifier',
98
+ 'ResearchStudy.associated-party-party',
99
+ ]);
100
+ /**
101
+ * Governed flat claims for ResearchStudy.
102
+ *
103
+ * `standardSearch` contains only official R5 SearchParameters. The
104
+ * `associatedParty` list is an explicit operational projection of the R5
105
+ * backbone element; none of those names is advertised as a standard search
106
+ * parameter and `classifier` never carries CRUD or authorization policy.
107
+ */
108
+ export const ResearchStudyFlatClaimCatalog = Object.freeze({
109
+ standardSearch: standardSearchClaims,
110
+ associatedParty: associatedPartyClaims,
111
+ all: Object.freeze([...standardSearchClaims, ...associatedPartyClaims]),
112
+ });
113
+ /**
114
+ * Projects the official resource-specific R5 SearchParameters from one native
115
+ * ResearchStudy into flat `resource.meta.claims`. Composite values use FHIR's
116
+ * `$` component separator; associatedParty remains a separate projection and
117
+ * never becomes authorization data.
118
+ *
119
+ * @see https://hl7.org/fhir/R5/researchstudy-search.html
120
+ */
121
+ export function projectResearchStudyR5SearchClaims(input) {
122
+ if (!isRecord(input) || input.resourceType !== 'ResearchStudy') {
123
+ throw new TypeError('research_study_resource_invalid');
124
+ }
125
+ const claims = {};
126
+ addClaim(claims, 'ResearchStudy.classifier', conceptsValue(input.classifier, 'research_study_classifier_invalid'));
127
+ addClaim(claims, 'ResearchStudy.condition', conceptsValue(input.condition, 'research_study_condition_invalid'));
128
+ addClaim(claims, 'ResearchStudy.date', periodValues(input.period, 'research_study_period_invalid'));
129
+ addClaim(claims, 'ResearchStudy.description', strings(input.description));
130
+ addClaim(claims, 'ResearchStudy.eligibility', references(input.recruitment, 'eligibility'));
131
+ addClaim(claims, 'ResearchStudy.focus-code', codeableReferences(input.focus, 'concept'));
132
+ addClaim(claims, 'ResearchStudy.focus-reference', codeableReferences(input.focus, 'reference'));
133
+ addClaim(claims, 'ResearchStudy.identifier', identifiers(input.identifier));
134
+ addClaim(claims, 'ResearchStudy.keyword', conceptsValue(input.keyword, 'research_study_keyword_invalid'));
135
+ addClaim(claims, 'ResearchStudy.name', strings(input.name));
136
+ addClaim(claims, 'ResearchStudy.objective-description', backboneStrings(input.objective, 'description'));
137
+ addClaim(claims, 'ResearchStudy.objective-type', backboneConcepts(input.objective, 'type'));
138
+ addClaim(claims, 'ResearchStudy.part-of', references(input.partOf));
139
+ addClaim(claims, 'ResearchStudy.phase', conceptsValue(input.phase, 'research_study_phase_invalid'));
140
+ const progress = progressStatusValues(input.progressStatus);
141
+ addClaim(claims, 'ResearchStudy.progress-status-state-actual', progress.stateActual);
142
+ addClaim(claims, 'ResearchStudy.progress-status-state-period', progress.statePeriod);
143
+ addClaim(claims, 'ResearchStudy.progress-status-state-period-actual', progress.statePeriodActual);
144
+ addClaim(claims, 'ResearchStudy.protocol', references(input.protocol));
145
+ addClaim(claims, 'ResearchStudy.recruitment-actual', nestedNumber(input.recruitment, 'actualNumber'));
146
+ addClaim(claims, 'ResearchStudy.recruitment-target', nestedNumber(input.recruitment, 'targetNumber'));
147
+ addClaim(claims, 'ResearchStudy.region', conceptsValue(input.region, 'research_study_region_invalid'));
148
+ addClaim(claims, 'ResearchStudy.site', references(input.site));
149
+ addClaim(claims, 'ResearchStudy.status', strings(input.status));
150
+ addClaim(claims, 'ResearchStudy.study-design', conceptsValue(input.studyDesign, 'research_study_design_invalid'));
151
+ addClaim(claims, 'ResearchStudy.title', strings(input.title));
152
+ return Object.freeze(claims);
153
+ }
154
+ /**
155
+ * Normalizes the R5 `associatedParty[]` backbone without flattening parties
156
+ * into unrelated parallel arrays. This function validates data shape only; it
157
+ * does not grant study access or interpret classifier values as permissions.
158
+ */
159
+ export function normalizeResearchStudyAssociatedParties(input) {
160
+ if (!Array.isArray(input))
161
+ throw new TypeError('research_study_associated_parties_invalid');
162
+ return Object.freeze(input.map((value, index) => normalizeAssociatedParty(value, index)));
163
+ }
164
+ /**
165
+ * Produces one flat-claim map per associated party. Keeping the normalized
166
+ * party beside each map preserves the party/role/period/classifier boundary
167
+ * required when a later Bundle PATCH changes one party.
168
+ */
169
+ export function projectResearchStudyAssociatedPartyClaims(input) {
170
+ const parties = normalizeResearchStudyAssociatedParties(input);
171
+ return Object.freeze(parties.map(party => Object.freeze({
172
+ associatedParty: party,
173
+ claims: Object.freeze({
174
+ 'ResearchStudy.associated-party-name': Object.freeze(party.name ? [party.name] : []),
175
+ 'ResearchStudy.associated-party-role': Object.freeze(conceptValues([party.role])),
176
+ 'ResearchStudy.associated-party-period-start': Object.freeze((party.period || []).flatMap(period => period.start ? [period.start] : [])),
177
+ 'ResearchStudy.associated-party-period-end': Object.freeze((party.period || []).flatMap(period => period.end ? [period.end] : [])),
178
+ 'ResearchStudy.associated-party-classifier': Object.freeze(conceptValues(party.classifier || [])),
179
+ 'ResearchStudy.associated-party-party': Object.freeze(party.party ? [party.party.reference] : []),
180
+ }),
181
+ })));
182
+ }
183
+ function normalizeAssociatedParty(value, index) {
184
+ if (!isRecord(value))
185
+ throw new TypeError(`research_study_associated_party_${index}_invalid`);
186
+ const role = normalizeConcept(value.role, `research_study_associated_party_${index}_role_required`);
187
+ const name = optionalString(value.name);
188
+ const period = value.period === undefined
189
+ ? undefined
190
+ : normalizePeriods(value.period, index);
191
+ const classifier = value.classifier === undefined
192
+ ? undefined
193
+ : normalizeConcepts(value.classifier, `research_study_associated_party_${index}_classifier_invalid`);
194
+ const party = value.party === undefined ? undefined : normalizePartyReference(value.party, index);
195
+ return Object.freeze({
196
+ ...(name ? { name } : {}),
197
+ role,
198
+ ...(period ? { period } : {}),
199
+ ...(classifier ? { classifier } : {}),
200
+ ...(party ? { party } : {}),
201
+ });
202
+ }
203
+ function normalizeConcepts(value, error) {
204
+ if (!Array.isArray(value))
205
+ throw new TypeError(error);
206
+ return Object.freeze(value.map(item => normalizeConcept(item, error)));
207
+ }
208
+ function normalizeConcept(value, error) {
209
+ if (!isRecord(value))
210
+ throw new TypeError(error);
211
+ const text = optionalString(value.text);
212
+ let coding;
213
+ if (value.coding !== undefined) {
214
+ if (!Array.isArray(value.coding))
215
+ throw new TypeError(error);
216
+ coding = Object.freeze(value.coding.map(item => normalizeCoding(item, error)));
217
+ }
218
+ if (!text && (!coding || coding.length === 0 || !coding.some(item => item.code)))
219
+ throw new TypeError(error);
220
+ return Object.freeze({ ...(coding ? { coding } : {}), ...(text ? { text } : {}) });
221
+ }
222
+ function normalizeCoding(value, error) {
223
+ if (!isRecord(value))
224
+ throw new TypeError(error);
225
+ const system = optionalString(value.system);
226
+ const code = optionalString(value.code);
227
+ const display = optionalString(value.display);
228
+ if (!code && !display)
229
+ throw new TypeError(error);
230
+ return Object.freeze({ ...(system ? { system } : {}), ...(code ? { code } : {}), ...(display ? { display } : {}) });
231
+ }
232
+ function normalizePeriods(value, index) {
233
+ if (!Array.isArray(value))
234
+ throw new TypeError(`research_study_associated_party_${index}_period_invalid`);
235
+ return Object.freeze(value.map(item => {
236
+ if (!isRecord(item))
237
+ throw new TypeError(`research_study_associated_party_${index}_period_invalid`);
238
+ const start = optionalString(item.start);
239
+ const end = optionalString(item.end);
240
+ if (!start && !end)
241
+ throw new TypeError(`research_study_associated_party_${index}_period_invalid`);
242
+ if ((start && !isFhirDateTime(start)) || (end && !isFhirDateTime(end))) {
243
+ throw new TypeError(`research_study_associated_party_${index}_period_invalid`);
244
+ }
245
+ if (start && end && Date.parse(end) < Date.parse(start)) {
246
+ throw new TypeError(`research_study_associated_party_${index}_period_invalid`);
247
+ }
248
+ return Object.freeze({ ...(start ? { start } : {}), ...(end ? { end } : {}) });
249
+ }));
250
+ }
251
+ function normalizePartyReference(value, index) {
252
+ const error = `research_study_associated_party_${index}_party_reference_invalid`;
253
+ if (!isRecord(value))
254
+ throw new TypeError(error);
255
+ const reference = optionalString(value.reference);
256
+ const type = optionalString(value.type);
257
+ const display = optionalString(value.display);
258
+ if (!reference)
259
+ throw new TypeError(error);
260
+ const allowed = ['Organization', 'Practitioner', 'PractitionerRole'];
261
+ if (type && !allowed.includes(type))
262
+ throw new TypeError(error);
263
+ const relativeType = reference.match(/^(Organization|Practitioner|PractitionerRole|[A-Z][A-Za-z]+)\//)?.[1];
264
+ if (relativeType && !allowed.includes(relativeType))
265
+ throw new TypeError(error);
266
+ return Object.freeze({
267
+ reference,
268
+ ...(type ? { type: type } : {}),
269
+ ...(display ? { display } : {}),
270
+ });
271
+ }
272
+ function conceptValues(concepts) {
273
+ return concepts.flatMap(concept => {
274
+ const coded = (concept.coding || []).flatMap(coding => coding.code
275
+ ? [`${coding.system ? `${coding.system}|` : ''}${coding.code}`]
276
+ : []);
277
+ return coded.length > 0 ? coded : concept.text ? [concept.text] : [];
278
+ });
279
+ }
280
+ function addClaim(claims, name, values) {
281
+ if (values.length > 0)
282
+ claims[name] = Object.freeze([...values]);
283
+ }
284
+ function strings(value) {
285
+ const result = optionalString(value);
286
+ return result ? [result] : [];
287
+ }
288
+ function conceptsValue(value, error) {
289
+ if (value === undefined)
290
+ return [];
291
+ const concepts = Array.isArray(value)
292
+ ? normalizeConcepts(value, error)
293
+ : Object.freeze([normalizeConcept(value, error)]);
294
+ return conceptValues(concepts);
295
+ }
296
+ function references(value, nestedKey) {
297
+ let candidate = value;
298
+ if (nestedKey) {
299
+ if (!isRecord(value))
300
+ return [];
301
+ candidate = value[nestedKey];
302
+ }
303
+ if (candidate === undefined)
304
+ return [];
305
+ const items = Array.isArray(candidate) ? candidate : [candidate];
306
+ return items.flatMap(item => isRecord(item) && optionalString(item.reference) ? [String(item.reference).trim()] : []);
307
+ }
308
+ function identifiers(value) {
309
+ if (value === undefined)
310
+ return [];
311
+ if (!Array.isArray(value))
312
+ throw new TypeError('research_study_identifier_invalid');
313
+ return value.flatMap(item => {
314
+ if (!isRecord(item))
315
+ throw new TypeError('research_study_identifier_invalid');
316
+ const system = optionalString(item.system);
317
+ const identifier = optionalString(item.value);
318
+ return identifier ? [`${system ? `${system}|` : ''}${identifier}`] : [];
319
+ });
320
+ }
321
+ function periodValues(value, error) {
322
+ if (value === undefined)
323
+ return [];
324
+ if (!isRecord(value))
325
+ throw new TypeError(error);
326
+ const start = optionalString(value.start);
327
+ const end = optionalString(value.end);
328
+ if ((start && !isFhirDateTime(start)) || (end && !isFhirDateTime(end)))
329
+ throw new TypeError(error);
330
+ return [start, end].filter((item) => Boolean(item));
331
+ }
332
+ function codeableReferences(value, key) {
333
+ if (value === undefined)
334
+ return [];
335
+ if (!Array.isArray(value))
336
+ throw new TypeError('research_study_focus_invalid');
337
+ return value.flatMap(item => {
338
+ if (!isRecord(item))
339
+ throw new TypeError('research_study_focus_invalid');
340
+ return key === 'concept'
341
+ ? conceptsValue(item.concept, 'research_study_focus_invalid')
342
+ : references(item.reference);
343
+ });
344
+ }
345
+ function backboneStrings(value, key) {
346
+ if (value === undefined)
347
+ return [];
348
+ if (!Array.isArray(value))
349
+ throw new TypeError('research_study_backbone_invalid');
350
+ return value.flatMap(item => isRecord(item) ? strings(item[key]) : []);
351
+ }
352
+ function backboneConcepts(value, key) {
353
+ if (value === undefined)
354
+ return [];
355
+ if (!Array.isArray(value))
356
+ throw new TypeError('research_study_backbone_invalid');
357
+ return value.flatMap(item => isRecord(item) ? conceptsValue(item[key], 'research_study_backbone_invalid') : []);
358
+ }
359
+ function nestedNumber(value, key) {
360
+ if (!isRecord(value) || value[key] === undefined)
361
+ return [];
362
+ const number = Number(value[key]);
363
+ if (!Number.isInteger(number) || number < 0)
364
+ throw new TypeError('research_study_recruitment_invalid');
365
+ return [String(number)];
366
+ }
367
+ function progressStatusValues(value) {
368
+ const result = { stateActual: [], statePeriod: [], statePeriodActual: [] };
369
+ if (value === undefined)
370
+ return result;
371
+ if (!Array.isArray(value))
372
+ throw new TypeError('research_study_progress_status_invalid');
373
+ for (const item of value) {
374
+ if (!isRecord(item))
375
+ throw new TypeError('research_study_progress_status_invalid');
376
+ const states = conceptsValue(item.state, 'research_study_progress_status_invalid');
377
+ const dates = periodValues(item.period, 'research_study_progress_status_invalid');
378
+ const actual = typeof item.actual === 'boolean' ? String(item.actual) : undefined;
379
+ for (const state of states) {
380
+ if (actual)
381
+ result.stateActual.push(`${state}$${actual}`);
382
+ for (const date of dates) {
383
+ result.statePeriod.push(`${state}$${date}`);
384
+ if (actual)
385
+ result.statePeriodActual.push(`${state}$${date}$${actual}`);
386
+ }
387
+ }
388
+ }
389
+ return result;
390
+ }
391
+ function optionalString(value) {
392
+ const normalized = typeof value === 'string' ? value.trim() : '';
393
+ return normalized || undefined;
394
+ }
395
+ function isFhirDateTime(value) {
396
+ return /^\d{4}(?:-\d{2}(?:-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))?)?)?$/.test(value)
397
+ && !Number.isNaN(Date.parse(value));
398
+ }
399
+ function isRecord(value) {
400
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
401
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.4.12",
3
+ "version": "0.5.1",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/financial.d.ts",
37
37
  "default": "./dist/financial.js"
38
38
  },
39
+ "./group": {
40
+ "types": "./dist/group.d.ts",
41
+ "default": "./dist/group.js"
42
+ },
39
43
  "./iso-jurisdictions": {
40
44
  "types": "./dist/iso-jurisdictions.d.ts",
41
45
  "default": "./dist/iso-jurisdictions.js"
@@ -44,6 +48,10 @@
44
48
  "types": "./dist/organization-application.d.ts",
45
49
  "default": "./dist/organization-application.js"
46
50
  },
51
+ "./research-study": {
52
+ "types": "./dist/research-study.d.ts",
53
+ "default": "./dist/research-study.js"
54
+ },
47
55
  "./sectors": {
48
56
  "types": "./dist/sectors.d.ts",
49
57
  "default": "./dist/sectors.js"