vet-sdk-core-ts 0.4.13 → 0.4.15
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 +28 -22
- package/dist/research-study.d.ts +55 -111
- package/dist/research-study.js +207 -190
- package/docs/101-RESEARCH-STUDY-INVITATIONS.md +42 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,12 +8,13 @@ owns gateway request construction. GW VET remains the policy authority.
|
|
|
8
8
|
|
|
9
9
|
## Research studies
|
|
10
10
|
|
|
11
|
-
`buildVeterinaryResearchStudyCreateEntry`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
`buildVeterinaryResearchStudyCreateEntry` accepts ResearchStudy business data
|
|
12
|
+
and projects it into the canonical JSON:API resource object: `resourceType`,
|
|
13
|
+
`id` and flat `resource.meta.claims`. It does not send native `status`, `title`,
|
|
14
|
+
`associatedParty` or `recruitment` objects to GW. Associated parties use one
|
|
15
|
+
independent flat-claim PATCH entry per party, so references, roles, periods and
|
|
16
|
+
classifiers cannot be mixed across employees. Native FHIR JSON is produced
|
|
17
|
+
only by an explicit later export/projection boundary.
|
|
17
18
|
|
|
18
19
|
Study participation does not itself authorize access. The separate
|
|
19
20
|
`buildVeterinaryResearchStudySmartAuthorization` request is limited to create,
|
|
@@ -33,26 +34,31 @@ separate JSON:API `Consent/_batch`. Every POST is fixed to `active`, `permit`,
|
|
|
33
34
|
ResearchStudy scope. The API does not accept caller-authored actions, so it
|
|
34
35
|
cannot be broadened to delete.
|
|
35
36
|
|
|
36
|
-
`buildVeterinaryResearchStudyPartyInvitations` creates exactly one
|
|
37
|
-
`Communication` per associated `PractitionerRole`.
|
|
38
|
-
an `application/
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
`buildVeterinaryResearchStudyPartyInvitations` creates exactly one claims-only
|
|
38
|
+
`Communication` per associated `PractitionerRole`. Its canonical attachment
|
|
39
|
+
claims carry an `application/vnd.api+json` primary document containing one
|
|
40
|
+
JSON:API resource object with `type`, `id` and flat Consent claims in
|
|
41
|
+
`attributes`; `resource`, `request` and FHIR Bundle shapes are rejected.
|
|
42
|
+
`Consent.source-reference` pins the ResearchStudy
|
|
43
|
+
and `Consent.resource-type` pins `ResearchSubject`. The draft requests only
|
|
44
|
+
create, read, update and search; it contains no delete permission and never
|
|
45
|
+
authorizes SMART access. Native FHIR Communication, Consent or Bundle objects
|
|
46
|
+
are accepted only by explicit import/projection/export adapters.
|
|
43
47
|
|
|
44
48
|
`buildVeterinaryResearchStudyGroupCreateEntry` and
|
|
45
|
-
`buildVeterinaryResearchStudyGroupMemberPatchEntries` build
|
|
49
|
+
`buildVeterinaryResearchStudyGroupMemberPatchEntries` build flat-claim Group
|
|
46
50
|
POST/PATCH entries for an enumerated professional review team. Every member is
|
|
47
51
|
a registered `PractitionerRole`; the exact twelve Group search claims are
|
|
48
|
-
derived from `vet-data-utils-ts/group
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
`ResearchStudy.recruitment.eligibility
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
derived from `vet-data-utils-ts/group`, and each member has its own PATCH entry.
|
|
53
|
+
The Group stays independent: individual professionals are related to the study
|
|
54
|
+
through separate `ResearchStudy.associatedParty` PATCH entries. It must not use
|
|
55
|
+
`ResearchStudy.recruitment.eligibility`, which represents research-subject
|
|
56
|
+
cohorts or eligibility criteria. Group grants no authorization, permission or
|
|
57
|
+
SMART scope; DCR and active Consent remain separate.
|
|
58
|
+
The Group create builder requires the ResearchStudy UUID and derives a
|
|
59
|
+
governed `Group.identifier`. Its exported search helper re-derives
|
|
60
|
+
the same deterministic UUIDv5 review-team identifier after refresh without
|
|
61
|
+
using eligibility or granting authority.
|
|
56
62
|
|
|
57
63
|
The complete controller-to-professional contract and a browser-safe snippet
|
|
58
64
|
are in
|
package/dist/research-study.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ResearchStudyAssociatedParty } from 'vet-data-utils-ts/research-study';
|
|
2
|
-
import { type GroupR5ResearchTeam } from 'vet-data-utils-ts/group';
|
|
1
|
+
import { type ResearchStudyAssociatedParty, type ResearchStudyR5FlatClaimsResource } from 'vet-data-utils-ts/research-study';
|
|
2
|
+
import { type GroupR5FlatClaimsResource, type GroupR5ResearchTeam } from 'vet-data-utils-ts/group';
|
|
3
3
|
import { HealthcareConsentPurposes } from 'gdc-common-utils-ts/constants/healthcare';
|
|
4
4
|
import { ResourceTypesFhirR5 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
|
|
5
5
|
import { ClaimConsent, ConsentStatuses } from 'gdc-common-utils-ts/models/consent-rule';
|
|
@@ -14,72 +14,23 @@ export declare const VeterinaryResearchStudyInvitationClaimNames: Readonly<{
|
|
|
14
14
|
readonly Action: ClaimConsent.action;
|
|
15
15
|
readonly ResourceType: ClaimConsent.resourceType;
|
|
16
16
|
}>;
|
|
17
|
-
type FhirReference = Readonly<{
|
|
18
|
-
reference: string;
|
|
19
|
-
type?: string;
|
|
20
|
-
}>;
|
|
21
|
-
type FhirCoding = Readonly<{
|
|
22
|
-
system: string;
|
|
23
|
-
code: string;
|
|
24
|
-
}>;
|
|
25
|
-
type FhirCodeableConcept = Readonly<{
|
|
26
|
-
coding: readonly FhirCoding[];
|
|
27
|
-
}>;
|
|
28
17
|
export type VeterinaryResearchStudyDraftConsent = Readonly<{
|
|
29
|
-
|
|
18
|
+
type: 'Consent-v5.0.0';
|
|
30
19
|
id: string;
|
|
31
|
-
|
|
32
|
-
grantor: readonly FhirReference[];
|
|
33
|
-
controller: readonly FhirReference[];
|
|
34
|
-
grantee: readonly FhirReference[];
|
|
35
|
-
decision: 'permit';
|
|
36
|
-
provision: readonly Readonly<{
|
|
37
|
-
actor: readonly Readonly<{
|
|
38
|
-
reference: FhirReference;
|
|
39
|
-
}>[];
|
|
40
|
-
action: readonly FhirCodeableConcept[];
|
|
41
|
-
purpose: readonly FhirCoding[];
|
|
42
|
-
resourceType: readonly FhirCoding[];
|
|
43
|
-
data: readonly Readonly<{
|
|
44
|
-
meaning: 'related';
|
|
45
|
-
reference: FhirReference;
|
|
46
|
-
}>[];
|
|
47
|
-
}>[];
|
|
48
|
-
meta: Readonly<{
|
|
49
|
-
claims: Readonly<Record<string, string>>;
|
|
50
|
-
}>;
|
|
20
|
+
attributes: Readonly<Record<string, string>>;
|
|
51
21
|
}>;
|
|
52
|
-
export type
|
|
53
|
-
|
|
54
|
-
type: 'batch';
|
|
55
|
-
entry: readonly Readonly<{
|
|
56
|
-
fullUrl: string;
|
|
57
|
-
resource: VeterinaryResearchStudyDraftConsent;
|
|
58
|
-
request: Readonly<{
|
|
59
|
-
method: 'POST';
|
|
60
|
-
url: 'Consent';
|
|
61
|
-
}>;
|
|
62
|
-
}>[];
|
|
22
|
+
export type VeterinaryResearchStudyInvitationDocument = Readonly<{
|
|
23
|
+
data: readonly VeterinaryResearchStudyDraftConsent[];
|
|
63
24
|
}>;
|
|
64
25
|
export type VeterinaryResearchStudyPartyInvitation = Readonly<{
|
|
65
26
|
resourceType: 'Communication';
|
|
66
27
|
id: string;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
about: readonly FhirReference[];
|
|
71
|
-
recipient: readonly FhirReference[];
|
|
72
|
-
sender: FhirReference;
|
|
73
|
-
sent?: string;
|
|
74
|
-
payload: readonly Readonly<{
|
|
75
|
-
contentAttachment: Readonly<{
|
|
76
|
-
contentType: 'application/fhir+json';
|
|
77
|
-
title: 'research-study-draft-consent.json';
|
|
78
|
-
data: string;
|
|
79
|
-
}>;
|
|
80
|
-
}>[];
|
|
28
|
+
meta: Readonly<{
|
|
29
|
+
claims: Readonly<Record<string, string>>;
|
|
30
|
+
}>;
|
|
81
31
|
}>;
|
|
82
|
-
|
|
32
|
+
/** Native-shaped builder input used only to derive the persisted flat claims. */
|
|
33
|
+
export type VeterinaryResearchStudySourceResource = Readonly<{
|
|
83
34
|
resourceType: 'ResearchStudy';
|
|
84
35
|
id: string;
|
|
85
36
|
status?: 'draft' | 'active' | 'retired' | 'unknown';
|
|
@@ -87,9 +38,10 @@ export type VeterinaryResearchStudyResource = Readonly<{
|
|
|
87
38
|
meta?: Readonly<Record<string, unknown>>;
|
|
88
39
|
[key: string]: unknown;
|
|
89
40
|
}>;
|
|
90
|
-
export type VeterinaryResearchStudyCreateResource =
|
|
91
|
-
status: NonNullable<
|
|
41
|
+
export type VeterinaryResearchStudyCreateResource = VeterinaryResearchStudySourceResource & Readonly<{
|
|
42
|
+
status: NonNullable<VeterinaryResearchStudySourceResource['status']>;
|
|
92
43
|
}>;
|
|
44
|
+
export type VeterinaryResearchStudyResource = ResearchStudyR5FlatClaimsResource;
|
|
93
45
|
export type VeterinaryResearchStudyBatchEntry = Readonly<{
|
|
94
46
|
type: 'ResearchStudy-v5.0.0';
|
|
95
47
|
resource: VeterinaryResearchStudyResource;
|
|
@@ -101,13 +53,7 @@ export type VeterinaryResearchStudyBatchEntry = Readonly<{
|
|
|
101
53
|
export type VeterinaryResearchStudyBatch = Readonly<{
|
|
102
54
|
data: readonly VeterinaryResearchStudyBatchEntry[];
|
|
103
55
|
}>;
|
|
104
|
-
export type VeterinaryResearchStudyGroupResource =
|
|
105
|
-
id: string;
|
|
106
|
-
meta: Readonly<{
|
|
107
|
-
claims: Readonly<Record<string, readonly string[]>>;
|
|
108
|
-
[key: string]: unknown;
|
|
109
|
-
}>;
|
|
110
|
-
}>;
|
|
56
|
+
export type VeterinaryResearchStudyGroupResource = GroupR5FlatClaimsResource;
|
|
111
57
|
export type VeterinaryResearchStudyGroupBatchEntry = Readonly<{
|
|
112
58
|
type: 'Group-v5.0.0';
|
|
113
59
|
resource: VeterinaryResearchStudyGroupResource;
|
|
@@ -119,6 +65,19 @@ export type VeterinaryResearchStudyGroupBatchEntry = Readonly<{
|
|
|
119
65
|
export type VeterinaryResearchStudyGroupBatch = Readonly<{
|
|
120
66
|
data: readonly VeterinaryResearchStudyGroupBatchEntry[];
|
|
121
67
|
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Canonical URI Identifier.system linking one descriptive review-team Group
|
|
70
|
+
* to its study through an RFC 9562 UUIDv5 value.
|
|
71
|
+
*
|
|
72
|
+
* @see https://hl7.org/fhir/R5/datatypes.html#Identifier
|
|
73
|
+
* @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
|
|
74
|
+
*/
|
|
75
|
+
export declare const VeterinaryResearchStudyReviewTeamIdentifierSystem: "urn:ietf:rfc:3986";
|
|
76
|
+
/** UUIDv5 namespace governed by this SDK for ResearchStudy review-team identifiers. */
|
|
77
|
+
export declare const VeterinaryResearchStudyReviewTeamIdentifierNamespace: "6434afe2-d152-5ec2-a88b-ec2a96e0f010";
|
|
78
|
+
export type VeterinaryResearchStudyReviewTeamGroupSearch = Readonly<{
|
|
79
|
+
identifier: string;
|
|
80
|
+
}>;
|
|
122
81
|
export type VeterinaryResearchStudySmartAuthorization = Readonly<{
|
|
123
82
|
purpose: typeof HealthcareConsentPurposes.Research;
|
|
124
83
|
researchStudyReference: string;
|
|
@@ -135,7 +94,6 @@ export type VeterinaryResearchStudyActiveConsentInput = Readonly<{
|
|
|
135
94
|
export type VeterinaryResearchStudyActiveConsent = Readonly<{
|
|
136
95
|
resourceType: typeof ResourceTypesFhirR5.Consent;
|
|
137
96
|
id: string;
|
|
138
|
-
status: typeof ConsentStatuses.Active;
|
|
139
97
|
meta: Readonly<{
|
|
140
98
|
claims: Readonly<Record<string, string>>;
|
|
141
99
|
}>;
|
|
@@ -179,46 +137,34 @@ export declare function buildVeterinaryResearchStudySmartAuthorization(input: Re
|
|
|
179
137
|
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
180
138
|
*/
|
|
181
139
|
export declare function buildVeterinaryResearchStudyActiveConsentBatch(inputs: readonly VeterinaryResearchStudyActiveConsentInput[]): VeterinaryResearchStudyActiveConsentBatch;
|
|
182
|
-
/** Builds one POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
140
|
+
/** Builds one flat-claims POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
183
141
|
export declare function buildVeterinaryResearchStudyActiveConsentEntry(input: VeterinaryResearchStudyActiveConsentInput): VeterinaryResearchStudyActiveConsentEntry;
|
|
184
142
|
export declare function buildVeterinaryResearchStudyBatch(entries: readonly VeterinaryResearchStudyBatchEntry[]): VeterinaryResearchStudyBatch;
|
|
185
143
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* search claim is added to Group.
|
|
144
|
+
* Builds the flat-claims Group POST used to create one enumerated ResearchStudy
|
|
145
|
+
* review team. Every member is added later as its own PractitionerRole claim
|
|
146
|
+
* PATCH. Group remains a description only and cannot carry access policy.
|
|
190
147
|
*
|
|
191
|
-
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
192
148
|
* @see https://hl7.org/fhir/R5/group.html
|
|
149
|
+
* @see https://hl7.org/fhir/R5/group-search.html
|
|
193
150
|
*/
|
|
194
|
-
export declare function
|
|
195
|
-
reference: `Group/${string}`;
|
|
196
|
-
type: 'Group';
|
|
197
|
-
}>;
|
|
198
|
-
/**
|
|
199
|
-
* Builds the native ResearchStudy PATCH that links a descriptive Group through
|
|
200
|
-
* `ResearchStudy.recruitment.eligibility`. The existing ResearchStudy batch
|
|
201
|
-
* builder wraps this entry; Group and ResearchStudy remain separate resource
|
|
202
|
-
* endpoints and no mixed-resource transport envelope is invented.
|
|
203
|
-
*
|
|
204
|
-
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
205
|
-
* @see https://hl7.org/fhir/R5/researchstudy-search.html
|
|
206
|
-
*/
|
|
207
|
-
export declare function buildVeterinaryResearchStudyEligibilityGroupPatchEntry(input: Readonly<{
|
|
151
|
+
export declare function buildVeterinaryResearchStudyGroupCreateEntry(input: Readonly<{
|
|
208
152
|
researchStudyId: string;
|
|
209
|
-
|
|
210
|
-
}>):
|
|
153
|
+
group: GroupR5ResearchTeam;
|
|
154
|
+
}>): VeterinaryResearchStudyGroupBatchEntry;
|
|
155
|
+
/** Builds the governed business Identifier; it describes correlation, never authority. */
|
|
156
|
+
export declare function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId: string): Readonly<{
|
|
157
|
+
system: typeof VeterinaryResearchStudyReviewTeamIdentifierSystem;
|
|
158
|
+
value: string;
|
|
159
|
+
}>;
|
|
211
160
|
/**
|
|
212
|
-
* Builds the Group
|
|
213
|
-
*
|
|
214
|
-
*
|
|
161
|
+
* Builds the standard FHIR `Group.identifier` token search used to recover the
|
|
162
|
+
* Group after refresh. Re-derivation avoids storing a second linkage and never
|
|
163
|
+
* constitutes access authority.
|
|
215
164
|
*
|
|
216
|
-
* @see https://hl7.org/fhir/R5/group.html
|
|
217
165
|
* @see https://hl7.org/fhir/R5/group-search.html
|
|
218
166
|
*/
|
|
219
|
-
export declare function
|
|
220
|
-
group: GroupR5ResearchTeam;
|
|
221
|
-
}>): VeterinaryResearchStudyGroupBatchEntry;
|
|
167
|
+
export declare function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId: string): VeterinaryResearchStudyReviewTeamGroupSearch;
|
|
222
168
|
/**
|
|
223
169
|
* Builds one independently correlated PATCH per PractitionerRole member. A
|
|
224
170
|
* PATCH changes descriptive membership only; DCR, Consent and SMART remain
|
|
@@ -235,8 +181,9 @@ export declare function buildVeterinaryResearchStudyCreateEntry(input: Readonly<
|
|
|
235
181
|
}>): VeterinaryResearchStudyBatchEntry;
|
|
236
182
|
/**
|
|
237
183
|
* Builds one independent `_batch` PATCH entry per R5 associatedParty. Keeping
|
|
238
|
-
* each
|
|
239
|
-
* from being correlated with the wrong PractitionerRole.
|
|
184
|
+
* each party's claim map in its own entry prevents roles, periods and
|
|
185
|
+
* classifiers from being correlated with the wrong PractitionerRole. Native
|
|
186
|
+
* `associatedParty` JSON is not sent to persistence. These entries update
|
|
240
187
|
* study membership only; they do not grant CRUDS access.
|
|
241
188
|
*/
|
|
242
189
|
export declare function buildVeterinaryResearchStudyPartyPatchEntries(input: Readonly<{
|
|
@@ -245,17 +192,15 @@ export declare function buildVeterinaryResearchStudyPartyPatchEntries(input: Rea
|
|
|
245
192
|
}>): readonly VeterinaryResearchStudyBatchEntry[];
|
|
246
193
|
/**
|
|
247
194
|
* Builds the auditable R5 invitation sent to one ResearchStudy associated
|
|
248
|
-
* party. The Communication
|
|
249
|
-
*
|
|
195
|
+
* party. The claims-only Communication carries an `application/vnd.api+json`
|
|
196
|
+
* primary document with exactly one claims-only `Consent.status = draft`.
|
|
250
197
|
* Draft is descriptive only and can never satisfy authorization. The invitee
|
|
251
198
|
* must finish DCR and a controller must later persist a separate active
|
|
252
199
|
* Consent before the study-scoped SMART request can succeed.
|
|
253
200
|
*
|
|
254
|
-
*
|
|
255
|
-
* `
|
|
256
|
-
*
|
|
257
|
-
* search; their FHIR representation uses the matching REST interaction codes,
|
|
258
|
-
* where search is `search-type`. Delete is deliberately absent.
|
|
201
|
+
* The flat Consent claims retain the exact study boundary and affected
|
|
202
|
+
* `ResearchSubject` resource family. Delete is deliberately absent. Native
|
|
203
|
+
* FHIR projection belongs only at an explicit import/export boundary.
|
|
259
204
|
*
|
|
260
205
|
* @see https://hl7.org/fhir/R5/communication.html
|
|
261
206
|
* @see https://hl7.org/fhir/R5/consent.html
|
|
@@ -284,6 +229,5 @@ export declare function buildVeterinaryResearchStudyPartyInvitations(input: Read
|
|
|
284
229
|
consentId: string;
|
|
285
230
|
}>[];
|
|
286
231
|
}>): readonly VeterinaryResearchStudyPartyInvitation[];
|
|
287
|
-
/** Decodes the
|
|
288
|
-
export declare function
|
|
289
|
-
export {};
|
|
232
|
+
/** Decodes the attached JSON:API Consent document after validating its claims-only boundary. */
|
|
233
|
+
export declare function decodeVeterinaryResearchStudyInvitationDocument(communication: VeterinaryResearchStudyPartyInvitation): VeterinaryResearchStudyInvitationDocument;
|
package/dist/research-study.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { normalizeResearchStudyAssociatedParties,
|
|
1
|
+
import { normalizeResearchStudyAssociatedParties, normalizeResearchStudyR5FlatClaimsResource, projectResearchStudyAssociatedPartyFlatClaimResources, projectResearchStudyR5FlatClaimsResource, } from 'vet-data-utils-ts/research-study';
|
|
2
2
|
import { VeterinaryCommunicationPresetFilters, VeterinaryResearchCommunicationTopics, } from 'vet-data-utils-ts/communication';
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeGroupR5FlatClaimsResource, normalizeGroupR5ResearchTeam, projectGroupPractitionerRoleMemberFlatClaimResources, projectGroupR5FlatClaimsResource, } from 'vet-data-utils-ts/group';
|
|
4
|
+
import { HttpMediaTypes } from 'gdc-common-utils-ts/constants/http';
|
|
4
5
|
import { HealthcareConsentPurposes } from 'gdc-common-utils-ts/constants/healthcare';
|
|
5
6
|
import { ResourceTypesFhirR5 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
|
|
6
7
|
import { InteroperableContext } from 'gdc-common-utils-ts/constants/lifecycle';
|
|
7
8
|
import { ClaimConsent, ConsentDecisions, ConsentStatuses, } from 'gdc-common-utils-ts/models/consent-rule';
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { CommunicationClaim } from 'gdc-common-utils-ts/models/interoperable-claims/communication-claims';
|
|
10
|
+
import { sha1 } from '@noble/hashes/legacy.js';
|
|
10
11
|
export const VeterinaryResearchStudyPermissionActions = Object.freeze([
|
|
11
12
|
'create',
|
|
12
13
|
'read',
|
|
@@ -22,6 +23,16 @@ export const VeterinaryResearchStudyInvitationClaimNames = Object.freeze({
|
|
|
22
23
|
Action: ClaimConsent.action,
|
|
23
24
|
ResourceType: ClaimConsent.resourceType,
|
|
24
25
|
});
|
|
26
|
+
/**
|
|
27
|
+
* Canonical URI Identifier.system linking one descriptive review-team Group
|
|
28
|
+
* to its study through an RFC 9562 UUIDv5 value.
|
|
29
|
+
*
|
|
30
|
+
* @see https://hl7.org/fhir/R5/datatypes.html#Identifier
|
|
31
|
+
* @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
|
|
32
|
+
*/
|
|
33
|
+
export const VeterinaryResearchStudyReviewTeamIdentifierSystem = 'urn:ietf:rfc:3986';
|
|
34
|
+
/** UUIDv5 namespace governed by this SDK for ResearchStudy review-team identifiers. */
|
|
35
|
+
export const VeterinaryResearchStudyReviewTeamIdentifierNamespace = '6434afe2-d152-5ec2-a88b-ec2a96e0f010';
|
|
25
36
|
/** Builds the exact GW study-pinned ResearchSubject `crus` capability. */
|
|
26
37
|
export function buildVeterinaryResearchStudyAccessScope(researchStudyId) {
|
|
27
38
|
return `organization/ResearchSubject.crus?study=ResearchStudy/${boundedId(researchStudyId)}`;
|
|
@@ -69,7 +80,7 @@ export function buildVeterinaryResearchStudyActiveConsentBatch(inputs) {
|
|
|
69
80
|
const data = inputs.map(buildVeterinaryResearchStudyActiveConsentEntry);
|
|
70
81
|
return deepFreeze({ data });
|
|
71
82
|
}
|
|
72
|
-
/** Builds one POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
83
|
+
/** Builds one flat-claims POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
73
84
|
export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
|
|
74
85
|
if (input.status !== undefined && input.status !== ConsentStatuses.Active) {
|
|
75
86
|
throw new TypeError('veterinary_research_study_active_consent_status_invalid');
|
|
@@ -88,7 +99,6 @@ export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
|
|
|
88
99
|
resource: {
|
|
89
100
|
resourceType: ResourceTypesFhirR5.Consent,
|
|
90
101
|
id: consentId,
|
|
91
|
-
status: ConsentStatuses.Active,
|
|
92
102
|
meta: {
|
|
93
103
|
claims: {
|
|
94
104
|
'@context': InteroperableContext.FhirApi,
|
|
@@ -109,60 +119,45 @@ export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
|
|
|
109
119
|
export function buildVeterinaryResearchStudyBatch(entries) {
|
|
110
120
|
if (!Array.isArray(entries) || entries.length === 0)
|
|
111
121
|
throw new TypeError('veterinary_research_study_batch_empty');
|
|
112
|
-
return deepFreeze({ data: entries.map(
|
|
122
|
+
return deepFreeze({ data: entries.map(normalizeResearchStudyBatchEntry) });
|
|
113
123
|
}
|
|
114
124
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* search claim is added to Group.
|
|
125
|
+
* Builds the flat-claims Group POST used to create one enumerated ResearchStudy
|
|
126
|
+
* review team. Every member is added later as its own PractitionerRole claim
|
|
127
|
+
* PATCH. Group remains a description only and cannot carry access policy.
|
|
119
128
|
*
|
|
120
|
-
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
121
129
|
* @see https://hl7.org/fhir/R5/group.html
|
|
130
|
+
* @see https://hl7.org/fhir/R5/group-search.html
|
|
122
131
|
*/
|
|
123
|
-
export function
|
|
124
|
-
|
|
132
|
+
export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
|
|
133
|
+
const group = normalizedResearchTeamGroup(input.group);
|
|
134
|
+
if (group.member.length > 0)
|
|
135
|
+
throw new TypeError('group_members_require_patch_entries');
|
|
136
|
+
const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(input.researchStudyId);
|
|
137
|
+
return deepFreeze({
|
|
138
|
+
type: 'Group-v5.0.0',
|
|
139
|
+
resource: projectGroupR5FlatClaimsResource({ ...group, identifier: [identifier] }),
|
|
140
|
+
request: { method: 'POST', url: 'Group' },
|
|
141
|
+
});
|
|
125
142
|
}
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
* builder wraps this entry; Group and ResearchStudy remain separate resource
|
|
130
|
-
* endpoints and no mixed-resource transport envelope is invented.
|
|
131
|
-
*
|
|
132
|
-
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
133
|
-
* @see https://hl7.org/fhir/R5/researchstudy-search.html
|
|
134
|
-
*/
|
|
135
|
-
export function buildVeterinaryResearchStudyEligibilityGroupPatchEntry(input) {
|
|
136
|
-
if (containsAuthorizationProperty(input)) {
|
|
137
|
-
throw new TypeError('veterinary_research_study_group_authorization_forbidden');
|
|
138
|
-
}
|
|
139
|
-
const researchStudyId = boundedId(input.researchStudyId);
|
|
140
|
-
const eligibility = buildVeterinaryResearchStudyEligibilityGroupReference(input.groupId);
|
|
143
|
+
/** Builds the governed business Identifier; it describes correlation, never authority. */
|
|
144
|
+
export function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId) {
|
|
145
|
+
const studyUuid = boundedStudyUuid(researchStudyId);
|
|
141
146
|
return deepFreeze({
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
resourceType: 'ResearchStudy',
|
|
145
|
-
id: researchStudyId,
|
|
146
|
-
recruitment: { eligibility },
|
|
147
|
-
}),
|
|
148
|
-
request: { method: 'PATCH', url: `ResearchStudy/${researchStudyId}` },
|
|
147
|
+
system: VeterinaryResearchStudyReviewTeamIdentifierSystem,
|
|
148
|
+
value: `urn:uuid:${uuidV5(studyUuid, VeterinaryResearchStudyReviewTeamIdentifierNamespace)}`,
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
|
-
* Builds the Group
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
* Builds the standard FHIR `Group.identifier` token search used to recover the
|
|
153
|
+
* Group after refresh. Re-derivation avoids storing a second linkage and never
|
|
154
|
+
* constitutes access authority.
|
|
155
155
|
*
|
|
156
|
-
* @see https://hl7.org/fhir/R5/group.html
|
|
157
156
|
* @see https://hl7.org/fhir/R5/group-search.html
|
|
158
157
|
*/
|
|
159
|
-
export function
|
|
160
|
-
const
|
|
161
|
-
return deepFreeze({
|
|
162
|
-
type: 'Group-v5.0.0',
|
|
163
|
-
resource: withProjectedGroupClaims(group),
|
|
164
|
-
request: { method: 'POST', url: 'Group' },
|
|
165
|
-
});
|
|
158
|
+
export function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId) {
|
|
159
|
+
const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId);
|
|
160
|
+
return deepFreeze({ identifier: `${identifier.system}|${identifier.value}` });
|
|
166
161
|
}
|
|
167
162
|
/**
|
|
168
163
|
* Builds one independently correlated PATCH per PractitionerRole member. A
|
|
@@ -171,16 +166,10 @@ export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
|
|
|
171
166
|
*/
|
|
172
167
|
export function buildVeterinaryResearchStudyGroupMemberPatchEntries(input) {
|
|
173
168
|
const id = boundedGroupId(input.groupId);
|
|
174
|
-
const
|
|
175
|
-
return deepFreeze(
|
|
169
|
+
const resources = projectGroupPractitionerRoleMemberFlatClaimResources({ groupId: id, members: input.members });
|
|
170
|
+
return deepFreeze(resources.map(resource => ({
|
|
176
171
|
type: 'Group-v5.0.0',
|
|
177
|
-
resource
|
|
178
|
-
resourceType: 'Group',
|
|
179
|
-
id,
|
|
180
|
-
type: 'practitioner',
|
|
181
|
-
membership: 'enumerated',
|
|
182
|
-
member: [member],
|
|
183
|
-
})),
|
|
172
|
+
resource,
|
|
184
173
|
request: { method: 'PATCH', url: `Group/${id}` },
|
|
185
174
|
})));
|
|
186
175
|
}
|
|
@@ -195,42 +184,40 @@ export function buildVeterinaryResearchStudyCreateEntry(input) {
|
|
|
195
184
|
const study = normalizedStudy(input.study);
|
|
196
185
|
return deepFreeze({
|
|
197
186
|
type: 'ResearchStudy-v5.0.0',
|
|
198
|
-
resource:
|
|
187
|
+
resource: projectResearchStudyR5FlatClaimsResource(study),
|
|
199
188
|
request: { method: 'POST', url: 'ResearchStudy' },
|
|
200
189
|
});
|
|
201
190
|
}
|
|
202
191
|
/**
|
|
203
192
|
* Builds one independent `_batch` PATCH entry per R5 associatedParty. Keeping
|
|
204
|
-
* each
|
|
205
|
-
* from being correlated with the wrong PractitionerRole.
|
|
193
|
+
* each party's claim map in its own entry prevents roles, periods and
|
|
194
|
+
* classifiers from being correlated with the wrong PractitionerRole. Native
|
|
195
|
+
* `associatedParty` JSON is not sent to persistence. These entries update
|
|
206
196
|
* study membership only; they do not grant CRUDS access.
|
|
207
197
|
*/
|
|
208
198
|
export function buildVeterinaryResearchStudyPartyPatchEntries(input) {
|
|
209
199
|
const id = boundedId(input.researchStudyId);
|
|
210
|
-
const
|
|
211
|
-
|
|
200
|
+
const resources = projectResearchStudyAssociatedPartyFlatClaimResources({
|
|
201
|
+
researchStudyId: id,
|
|
202
|
+
associatedParties: input.associatedParties,
|
|
203
|
+
});
|
|
204
|
+
return deepFreeze(resources.map(resource => ({
|
|
212
205
|
type: 'ResearchStudy-v5.0.0',
|
|
213
|
-
resource
|
|
214
|
-
resourceType: 'ResearchStudy',
|
|
215
|
-
id,
|
|
216
|
-
associatedParty: [party],
|
|
217
|
-
}),
|
|
206
|
+
resource,
|
|
218
207
|
request: { method: 'PATCH', url: `ResearchStudy/${id}` },
|
|
219
208
|
})));
|
|
220
209
|
}
|
|
221
210
|
/**
|
|
222
211
|
* Builds the auditable R5 invitation sent to one ResearchStudy associated
|
|
223
|
-
* party. The Communication
|
|
224
|
-
*
|
|
212
|
+
* party. The claims-only Communication carries an `application/vnd.api+json`
|
|
213
|
+
* primary document with exactly one claims-only `Consent.status = draft`.
|
|
225
214
|
* Draft is descriptive only and can never satisfy authorization. The invitee
|
|
226
215
|
* must finish DCR and a controller must later persist a separate active
|
|
227
216
|
* Consent before the study-scoped SMART request can succeed.
|
|
228
217
|
*
|
|
229
|
-
*
|
|
230
|
-
* `
|
|
231
|
-
*
|
|
232
|
-
* search; their FHIR representation uses the matching REST interaction codes,
|
|
233
|
-
* where search is `search-type`. Delete is deliberately absent.
|
|
218
|
+
* The flat Consent claims retain the exact study boundary and affected
|
|
219
|
+
* `ResearchSubject` resource family. Delete is deliberately absent. Native
|
|
220
|
+
* FHIR projection belongs only at an explicit import/export boundary.
|
|
234
221
|
*
|
|
235
222
|
* @see https://hl7.org/fhir/R5/communication.html
|
|
236
223
|
* @see https://hl7.org/fhir/R5/consent.html
|
|
@@ -256,66 +243,42 @@ export function buildVeterinaryResearchStudyPartyInvitation(input) {
|
|
|
256
243
|
if (!topic)
|
|
257
244
|
throw new TypeError('veterinary_research_study_invitation_topic_unavailable');
|
|
258
245
|
const consent = {
|
|
259
|
-
|
|
246
|
+
type: 'Consent-v5.0.0',
|
|
260
247
|
id: consentId,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
code: action === 'search' ? 'search-type' : action,
|
|
272
|
-
}],
|
|
273
|
-
})),
|
|
274
|
-
purpose: [{ system: topic.system, code: topic.code }],
|
|
275
|
-
resourceType: [{ system: FHIR_RESOURCE_TYPES_SYSTEM, code: 'ResearchSubject' }],
|
|
276
|
-
data: [{ meaning: 'related', reference: { reference: researchStudyReference, type: 'ResearchStudy' } }],
|
|
277
|
-
}],
|
|
278
|
-
meta: {
|
|
279
|
-
claims: {
|
|
280
|
-
[VeterinaryResearchStudyInvitationClaimNames.Context]: 'org.hl7.fhir.api',
|
|
281
|
-
[VeterinaryResearchStudyInvitationClaimNames.Status]: 'draft',
|
|
282
|
-
[VeterinaryResearchStudyInvitationClaimNames.Decision]: 'permit',
|
|
283
|
-
[VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
|
|
284
|
-
[VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
|
|
285
|
-
[VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
|
|
286
|
-
[VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
|
|
287
|
-
},
|
|
248
|
+
attributes: {
|
|
249
|
+
[VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
|
|
250
|
+
[VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
|
|
251
|
+
[VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
|
|
252
|
+
[VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
|
|
253
|
+
[ClaimConsent.grantee]: partyReference,
|
|
254
|
+
[VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
|
|
255
|
+
[VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
|
|
256
|
+
[VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
|
|
257
|
+
[ClaimConsent.sourceReference]: researchStudyReference,
|
|
288
258
|
},
|
|
289
259
|
};
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
type: 'batch',
|
|
293
|
-
entry: [{
|
|
294
|
-
fullUrl: `urn:uuid:${consentId}`,
|
|
295
|
-
resource: consent,
|
|
296
|
-
request: { method: 'POST', url: 'Consent' },
|
|
297
|
-
}],
|
|
260
|
+
const document = {
|
|
261
|
+
data: [consent],
|
|
298
262
|
};
|
|
299
263
|
const category = VeterinaryCommunicationPresetFilters.ResearchAgreements.categories[0];
|
|
300
|
-
const [categorySystem, categoryCode] = splitCodingToken(category);
|
|
301
264
|
const sent = optionalInstant(input.sentAt);
|
|
302
265
|
return deepFreeze({
|
|
303
266
|
resourceType: 'Communication',
|
|
304
267
|
id: communicationId,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
268
|
+
meta: { claims: {
|
|
269
|
+
'@context': InteroperableContext.FhirApi,
|
|
270
|
+
[CommunicationClaim.Identifier]: communicationId,
|
|
271
|
+
[CommunicationClaim.Status]: 'completed',
|
|
272
|
+
[CommunicationClaim.Category]: category,
|
|
273
|
+
[CommunicationClaim.Topic]: topic.value,
|
|
274
|
+
[CommunicationClaim.ContentReference]: researchStudyReference,
|
|
275
|
+
[CommunicationClaim.Recipient]: partyReference,
|
|
276
|
+
[CommunicationClaim.Sender]: controllerReference,
|
|
277
|
+
...(sent ? { [CommunicationClaim.Sent]: sent } : {}),
|
|
278
|
+
[CommunicationClaim.ContentAttachmentType]: HttpMediaTypes.JsonApi,
|
|
279
|
+
[CommunicationClaim.ContentAttachmentTitle]: 'research-study-draft-consent.json',
|
|
280
|
+
[CommunicationClaim.ContentAttachmentData]: encodeJsonBase64(document),
|
|
281
|
+
} },
|
|
319
282
|
});
|
|
320
283
|
}
|
|
321
284
|
/**
|
|
@@ -334,41 +297,50 @@ export function buildVeterinaryResearchStudyPartyInvitations(input) {
|
|
|
334
297
|
...invitation,
|
|
335
298
|
})));
|
|
336
299
|
}
|
|
337
|
-
/** Decodes the
|
|
338
|
-
export function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|| attachment?.contentType !== 'application/fhir+json'
|
|
342
|
-
|| !attachment.data) {
|
|
343
|
-
throw new TypeError('veterinary_research_study_invitation_bundle_invalid');
|
|
300
|
+
/** Decodes the attached JSON:API Consent document after validating its claims-only boundary. */
|
|
301
|
+
export function decodeVeterinaryResearchStudyInvitationDocument(communication) {
|
|
302
|
+
if (!isClaimsOnlyResource(communication, 'Communication')) {
|
|
303
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
344
304
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
305
|
+
const claims = communication.meta.claims;
|
|
306
|
+
const encoded = claims[CommunicationClaim.ContentAttachmentData];
|
|
307
|
+
if (claims[CommunicationClaim.ContentAttachmentType] !== HttpMediaTypes.JsonApi || !encoded) {
|
|
308
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
309
|
+
}
|
|
310
|
+
let decoded;
|
|
311
|
+
try {
|
|
312
|
+
decoded = JSON.parse(decodeJsonBase64(encoded));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
316
|
+
}
|
|
317
|
+
const entry = decoded?.data?.[0];
|
|
318
|
+
if (decoded?.data?.length !== 1
|
|
319
|
+
|| entry?.type !== 'Consent-v5.0.0'
|
|
320
|
+
|| Object.keys(entry).some(key => !['type', 'id', 'attributes'].includes(key))
|
|
321
|
+
|| !boundedJsonApiId(entry.id)
|
|
322
|
+
|| !isFlatStringAttributes(entry.attributes)
|
|
323
|
+
|| !isValidInvitationDraftConsentAttributes(entry.attributes, claims)) {
|
|
324
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
352
325
|
}
|
|
353
326
|
return deepFreeze(decoded);
|
|
354
327
|
}
|
|
355
328
|
function normalizedStudy(input) {
|
|
356
329
|
if (!input || input.resourceType !== 'ResearchStudy')
|
|
357
330
|
throw new TypeError('veterinary_research_study_invalid');
|
|
331
|
+
if (input.associatedParty !== undefined) {
|
|
332
|
+
throw new TypeError('veterinary_research_study_parties_require_patch_entries');
|
|
333
|
+
}
|
|
358
334
|
const source = clonePlain(input);
|
|
359
335
|
const id = boundedId(input.id);
|
|
360
336
|
const status = String(input.status || '');
|
|
361
337
|
if (!['draft', 'active', 'retired', 'unknown'].includes(status)) {
|
|
362
338
|
throw new TypeError('veterinary_research_study_status_invalid');
|
|
363
339
|
}
|
|
364
|
-
const associatedParty = input.associatedParty === undefined
|
|
365
|
-
? undefined
|
|
366
|
-
: normalizeResearchStudyAssociatedParties(input.associatedParty);
|
|
367
340
|
return deepFreeze({
|
|
368
341
|
...source,
|
|
369
342
|
id,
|
|
370
343
|
status: status,
|
|
371
|
-
...(associatedParty ? { associatedParty } : {}),
|
|
372
344
|
});
|
|
373
345
|
}
|
|
374
346
|
function normalizedResearchTeamGroup(input) {
|
|
@@ -380,28 +352,24 @@ function normalizeResearchStudyGroupEntry(input) {
|
|
|
380
352
|
if (!input || input.type !== 'Group-v5.0.0') {
|
|
381
353
|
throw new TypeError('veterinary_research_study_group_entry_invalid');
|
|
382
354
|
}
|
|
383
|
-
|
|
355
|
+
let resource;
|
|
356
|
+
try {
|
|
357
|
+
resource = normalizeGroupR5FlatClaimsResource(input.resource);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
throw new TypeError('veterinary_research_study_group_entry_invalid');
|
|
361
|
+
}
|
|
384
362
|
const method = input.request?.method;
|
|
385
|
-
const expectedUrl = method === 'POST' ? 'Group' : method === 'PATCH' ? `Group/${
|
|
363
|
+
const expectedUrl = method === 'POST' ? 'Group' : method === 'PATCH' ? `Group/${resource.id}` : '';
|
|
386
364
|
if (!expectedUrl || input.request.url !== expectedUrl) {
|
|
387
365
|
throw new TypeError('veterinary_research_study_group_request_invalid');
|
|
388
366
|
}
|
|
389
367
|
return deepFreeze({
|
|
390
368
|
type: 'Group-v5.0.0',
|
|
391
|
-
resource
|
|
369
|
+
resource,
|
|
392
370
|
request: { method, url: expectedUrl },
|
|
393
371
|
});
|
|
394
372
|
}
|
|
395
|
-
function withProjectedGroupClaims(group) {
|
|
396
|
-
const claims = projectGroupR5SearchClaims(group);
|
|
397
|
-
return deepFreeze({
|
|
398
|
-
...group,
|
|
399
|
-
meta: {
|
|
400
|
-
...(typeof group.meta === 'object' && group.meta !== null ? group.meta : {}),
|
|
401
|
-
claims,
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
373
|
function clonePlain(value) {
|
|
406
374
|
if (Array.isArray(value))
|
|
407
375
|
return value.map(clonePlain);
|
|
@@ -410,33 +378,66 @@ function clonePlain(value) {
|
|
|
410
378
|
}
|
|
411
379
|
return value;
|
|
412
380
|
}
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
381
|
+
function normalizeResearchStudyBatchEntry(input) {
|
|
382
|
+
if (!input || input.type !== 'ResearchStudy-v5.0.0') {
|
|
383
|
+
throw new TypeError('veterinary_research_study_entry_invalid');
|
|
384
|
+
}
|
|
385
|
+
let resource;
|
|
386
|
+
try {
|
|
387
|
+
resource = normalizeResearchStudyR5FlatClaimsResource(input.resource);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
throw new TypeError('veterinary_research_study_entry_invalid');
|
|
391
|
+
}
|
|
392
|
+
const method = input.request?.method;
|
|
393
|
+
const expectedUrl = method === 'POST'
|
|
394
|
+
? 'ResearchStudy'
|
|
395
|
+
: method === 'PATCH'
|
|
396
|
+
? `ResearchStudy/${resource.id}`
|
|
397
|
+
: '';
|
|
398
|
+
if (!expectedUrl || input.request.url !== expectedUrl) {
|
|
399
|
+
throw new TypeError('veterinary_research_study_request_invalid');
|
|
400
|
+
}
|
|
416
401
|
return deepFreeze({
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
claims: { ...standardClaims, ...partyClaims },
|
|
421
|
-
},
|
|
402
|
+
type: 'ResearchStudy-v5.0.0',
|
|
403
|
+
resource,
|
|
404
|
+
request: { method, url: expectedUrl },
|
|
422
405
|
});
|
|
423
406
|
}
|
|
424
|
-
function aggregatePartyClaims(parties) {
|
|
425
|
-
const output = {};
|
|
426
|
-
for (const projection of projectResearchStudyAssociatedPartyClaims(parties)) {
|
|
427
|
-
for (const [name, values] of Object.entries(projection.claims)) {
|
|
428
|
-
if (values.length > 0)
|
|
429
|
-
(output[name] || (output[name] = [])).push(...values);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return Object.fromEntries(Object.entries(output).map(([name, values]) => [name, Object.freeze([...values])]));
|
|
433
|
-
}
|
|
434
407
|
function boundedId(value) {
|
|
435
408
|
const id = String(value || '').trim();
|
|
436
409
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
437
410
|
throw new TypeError('veterinary_research_study_id_invalid');
|
|
438
411
|
return id;
|
|
439
412
|
}
|
|
413
|
+
function boundedStudyUuid(value) {
|
|
414
|
+
const uuid = String(value || '').trim().toLowerCase();
|
|
415
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(uuid)) {
|
|
416
|
+
throw new TypeError('veterinary_research_study_review_team_study_id_invalid');
|
|
417
|
+
}
|
|
418
|
+
return uuid;
|
|
419
|
+
}
|
|
420
|
+
function uuidV5(name, namespace) {
|
|
421
|
+
const namespaceHexBytes = namespace.replace(/-/g, '').match(/.{2}/g) || [];
|
|
422
|
+
const namespaceBytes = Uint8Array.from(namespaceHexBytes.map(byte => Number.parseInt(byte, 16)));
|
|
423
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
424
|
+
const input = new Uint8Array(namespaceBytes.length + nameBytes.length);
|
|
425
|
+
input.set(namespaceBytes);
|
|
426
|
+
input.set(nameBytes, namespaceBytes.length);
|
|
427
|
+
const digest = sha1(input).slice(0, 16);
|
|
428
|
+
digest[6] = (digest[6] & 0x0f) | 0x50;
|
|
429
|
+
digest[8] = (digest[8] & 0x3f) | 0x80;
|
|
430
|
+
const hex = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
431
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
432
|
+
}
|
|
433
|
+
function boundedJsonApiId(value) {
|
|
434
|
+
try {
|
|
435
|
+
return boundedId(value);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
440
441
|
function boundedGroupId(value) {
|
|
441
442
|
const id = String(value || '').trim();
|
|
442
443
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
@@ -470,23 +471,6 @@ function optionalInstant(value) {
|
|
|
470
471
|
throw new TypeError('veterinary_research_study_invitation_sent_invalid');
|
|
471
472
|
return instant;
|
|
472
473
|
}
|
|
473
|
-
function containsAuthorizationProperty(value, seen = new WeakSet()) {
|
|
474
|
-
if (!value || typeof value !== 'object')
|
|
475
|
-
return false;
|
|
476
|
-
if (seen.has(value))
|
|
477
|
-
return false;
|
|
478
|
-
seen.add(value);
|
|
479
|
-
if (Array.isArray(value))
|
|
480
|
-
return value.some(item => containsAuthorizationProperty(item, seen));
|
|
481
|
-
return Object.entries(value).some(([name, nested]) => ['authorization', 'permission', 'scope'].includes(name) || containsAuthorizationProperty(nested, seen));
|
|
482
|
-
}
|
|
483
|
-
function splitCodingToken(value) {
|
|
484
|
-
const separator = value.lastIndexOf('|');
|
|
485
|
-
if (separator < 1 || separator === value.length - 1) {
|
|
486
|
-
throw new TypeError('veterinary_research_study_invitation_category_invalid');
|
|
487
|
-
}
|
|
488
|
-
return [value.slice(0, separator), value.slice(separator + 1)];
|
|
489
|
-
}
|
|
490
474
|
function encodeJsonBase64(value) {
|
|
491
475
|
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
492
476
|
let binary = '';
|
|
@@ -507,3 +491,36 @@ function deepFreeze(value, seen = new WeakSet()) {
|
|
|
507
491
|
deepFreeze(nested, seen);
|
|
508
492
|
return Object.freeze(value);
|
|
509
493
|
}
|
|
494
|
+
function isRecord(value) {
|
|
495
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
496
|
+
}
|
|
497
|
+
function isClaimsOnlyResource(value, resourceType) {
|
|
498
|
+
if (!isRecord(value) || value.resourceType !== resourceType)
|
|
499
|
+
return false;
|
|
500
|
+
if (Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
|
|
501
|
+
return false;
|
|
502
|
+
if (typeof value.id !== 'string' || !isRecord(value.meta) || !isRecord(value.meta.claims))
|
|
503
|
+
return false;
|
|
504
|
+
return Object.values(value.meta.claims).every(claim => typeof claim === 'string' && claim.trim().length > 0);
|
|
505
|
+
}
|
|
506
|
+
function isFlatStringAttributes(value) {
|
|
507
|
+
return isRecord(value)
|
|
508
|
+
&& Object.keys(value).length > 0
|
|
509
|
+
&& Object.values(value).every(attribute => typeof attribute === 'string' && attribute.trim().length > 0);
|
|
510
|
+
}
|
|
511
|
+
function isValidInvitationDraftConsentAttributes(attributes, communicationClaims) {
|
|
512
|
+
const expected = {
|
|
513
|
+
[VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
|
|
514
|
+
[VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
|
|
515
|
+
[VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
|
|
516
|
+
[VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: communicationClaims[CommunicationClaim.Recipient],
|
|
517
|
+
[ClaimConsent.grantee]: communicationClaims[CommunicationClaim.Recipient],
|
|
518
|
+
[VeterinaryResearchStudyInvitationClaimNames.Purpose]: communicationClaims[CommunicationClaim.Topic],
|
|
519
|
+
[VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
|
|
520
|
+
[VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
|
|
521
|
+
[ClaimConsent.sourceReference]: communicationClaims[CommunicationClaim.ContentReference],
|
|
522
|
+
};
|
|
523
|
+
const expectedEntries = Object.entries(expected);
|
|
524
|
+
return Object.keys(attributes).length === expectedEntries.length
|
|
525
|
+
&& expectedEntries.every(([name, value]) => Boolean(value) && attributes[name] === value);
|
|
526
|
+
}
|
|
@@ -4,8 +4,9 @@ This SDK separates study membership, an invitation, and authorization. They
|
|
|
4
4
|
are three different facts:
|
|
5
5
|
|
|
6
6
|
1. `ResearchStudy.associatedParty` records who is expected to collaborate.
|
|
7
|
-
2. One
|
|
8
|
-
|
|
7
|
+
2. One claims-only `Communication` invites that exact `PractitionerRole` and
|
|
8
|
+
carries a JSON:API primary document with one claims-only
|
|
9
|
+
`Consent.status = draft`.
|
|
9
10
|
3. Only after the professional completes DCR and the controller persists a
|
|
10
11
|
later `Consent.status = active` may the professional request the
|
|
11
12
|
study-scoped SMART authorization.
|
|
@@ -17,8 +18,6 @@ related to the exact ResearchStudy. Whole-twin delete is absent.
|
|
|
17
18
|
```ts
|
|
18
19
|
import {
|
|
19
20
|
buildVeterinaryResearchStudyActiveConsentBatch,
|
|
20
|
-
buildVeterinaryResearchStudyBatch,
|
|
21
|
-
buildVeterinaryResearchStudyEligibilityGroupPatchEntry,
|
|
22
21
|
buildVeterinaryResearchStudyGroupBatch,
|
|
23
22
|
buildVeterinaryResearchStudyGroupCreateEntry,
|
|
24
23
|
buildVeterinaryResearchStudyGroupMemberPatchEntries,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
|
|
29
28
|
const researchGroupId = 'review-team'
|
|
30
29
|
const groupCreate = buildVeterinaryResearchStudyGroupCreateEntry({
|
|
30
|
+
researchStudyId: study.id,
|
|
31
31
|
group: {
|
|
32
32
|
resourceType: 'Group',
|
|
33
33
|
id: researchGroupId,
|
|
@@ -42,13 +42,6 @@ const memberPatches = buildVeterinaryResearchStudyGroupMemberPatchEntries({
|
|
|
42
42
|
})
|
|
43
43
|
const groupBatch = buildVeterinaryResearchStudyGroupBatch([groupCreate, ...memberPatches])
|
|
44
44
|
// The protected controller BFF submits `groupBatch`; membership grants no access.
|
|
45
|
-
const eligibilityPatch = buildVeterinaryResearchStudyEligibilityGroupPatchEntry({
|
|
46
|
-
researchStudyId: study.id,
|
|
47
|
-
groupId: researchGroupId,
|
|
48
|
-
})
|
|
49
|
-
const studyPatchBatch = buildVeterinaryResearchStudyBatch([eligibilityPatch])
|
|
50
|
-
// Submit `studyPatchBatch` through the protected ResearchStudy BFF after the
|
|
51
|
-
// Group exists. The two native resource endpoints remain separate.
|
|
52
45
|
|
|
53
46
|
const invitations = buildVeterinaryResearchStudyPartyInvitations({
|
|
54
47
|
researchStudyId: study.id,
|
|
@@ -89,14 +82,28 @@ const authorization = buildVeterinaryResearchStudySmartAuthorization({
|
|
|
89
82
|
await professionalRuntime.requestSmartToken(authorization)
|
|
90
83
|
```
|
|
91
84
|
|
|
92
|
-
The controller may
|
|
85
|
+
The controller may independently create a descriptive review team with
|
|
93
86
|
`buildVeterinaryResearchStudyGroupCreateEntry()` and append each registered
|
|
94
87
|
`PractitionerRole` with
|
|
95
|
-
`buildVeterinaryResearchStudyGroupMemberPatchEntries()`.
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
ResearchStudy
|
|
99
|
-
|
|
88
|
+
`buildVeterinaryResearchStudyGroupMemberPatchEntries()`. Individual
|
|
89
|
+
professionals are represented in the study by their separate
|
|
90
|
+
`ResearchStudy.associatedParty` PATCH entries. The professional Group is not
|
|
91
|
+
`ResearchStudy.recruitment.eligibility`: that FHIR element describes the
|
|
92
|
+
research-subject cohort or its eligibility criteria. Group membership is not
|
|
93
|
+
evidence of DCR or Consent and never grants the SMART authorization above.
|
|
94
|
+
|
|
95
|
+
GW persistence entries for ResearchStudy, professional Group and active
|
|
96
|
+
Consent contain only `resourceType`, `id` and `resource.meta.claims`. Their
|
|
97
|
+
FHIR-like field names are flat claims. Native nested FHIR JSON is reserved for
|
|
98
|
+
an explicit export/projection boundary, not these GW builders.
|
|
99
|
+
|
|
100
|
+
The draft Consent inside the Communication attachment is an explicit JSON:API
|
|
101
|
+
1.x `application/vnd.api+json` proposal for recipient review. Its `data[]`
|
|
102
|
+
resource object contains only `type`, `id` and `attributes`; those attributes
|
|
103
|
+
are the flat Consent claims. It has no internal `resource`, `request`, FHIR
|
|
104
|
+
Bundle or Bundle entry. The outer Communication remains claims-only. The draft
|
|
105
|
+
never authorizes access; only the later flat-claims active Consent POST is
|
|
106
|
+
evaluated by GW.
|
|
100
107
|
|
|
101
108
|
These builders follow the native [FHIR R5 Group resource](https://hl7.org/fhir/R5/group.html)
|
|
102
109
|
and its exact [resource-specific search catalogue](https://hl7.org/fhir/R5/group-search.html).
|
|
@@ -107,17 +114,26 @@ the authorization subject `ResearchStudy/<id>` from the exact unescaped scope.
|
|
|
107
114
|
Percent-encoding the `/` in that scope would no longer match the active
|
|
108
115
|
Consent action and is rejected.
|
|
109
116
|
|
|
110
|
-
The
|
|
111
|
-
`Communication.
|
|
112
|
-
`
|
|
117
|
+
The JSON:API primary document is base64 encoded in canonical
|
|
118
|
+
`Communication.content-attachment-data`, paired with
|
|
119
|
+
`Communication.content-attachment-type = application/vnd.api+json`. The
|
|
120
|
+
Consent claims use:
|
|
113
121
|
|
|
114
|
-
- `Consent.grantee` and `Consent.
|
|
122
|
+
- `Consent.grantee` and `Consent.actor-identifier` for the invited
|
|
115
123
|
`PractitionerRole`;
|
|
116
|
-
-
|
|
117
|
-
- `Consent.
|
|
118
|
-
- `Consent.
|
|
119
|
-
- `Consent.
|
|
120
|
-
-
|
|
124
|
+
- outer `Communication.sender` for the controller organization;
|
|
125
|
+
- `Consent.purpose` with HL7 v3 ActReason `HRESCH`;
|
|
126
|
+
- `Consent.resource-type` for `ResearchSubject`;
|
|
127
|
+
- `Consent.source-reference` for the exact `ResearchStudy/{id}`;
|
|
128
|
+
- `Consent.action` for `create`, `read`, `update`, and `search`.
|
|
129
|
+
|
|
130
|
+
The Group POST adds a governed business `Group.identifier` derived from the
|
|
131
|
+
ResearchStudy UUID. Use
|
|
132
|
+
`buildVeterinaryResearchStudyReviewTeamGroupSearch(study.id)` to recover the
|
|
133
|
+
same descriptive review-team Group after a refresh. The helper deterministically
|
|
134
|
+
re-derives a UUIDv5 using the SDK-exported, governed namespace and emits
|
|
135
|
+
`urn:ietf:rfc:3986|urn:uuid:<uuidv5>`. This one-way correlation is a standard
|
|
136
|
+
Group identifier token, not eligibility, Consent, permission or SMART authority.
|
|
121
137
|
|
|
122
138
|
FHIR R5 references:
|
|
123
139
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-sdk-core-ts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15",
|
|
4
4
|
"description": "Browser-safe VetChain core contracts and governed animal species identifiers",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -64,6 +64,6 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@noble/hashes": "^2.2.0",
|
|
66
66
|
"gdc-common-utils-ts": "2.9.4",
|
|
67
|
-
"vet-data-utils-ts": "0.5.
|
|
67
|
+
"vet-data-utils-ts": "0.5.2"
|
|
68
68
|
}
|
|
69
69
|
}
|