vet-sdk-core-ts 0.4.13 → 0.4.14
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 +22 -22
- package/dist/research-study.d.ts +28 -102
- package/dist/research-study.js +123 -180
- package/docs/101-RESEARCH-STUDY-INVITATIONS.md +31 -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,25 @@ 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
|
+
claims-only draft Consent. `Consent.source-reference` pins the ResearchStudy
|
|
41
|
+
and `Consent.resource-type` pins `ResearchSubject`. The draft requests only
|
|
42
|
+
create, read, update and search; it contains no delete permission and never
|
|
43
|
+
authorizes SMART access. Native FHIR Communication, Consent or Bundle objects
|
|
44
|
+
are accepted only by explicit import/projection/export adapters.
|
|
43
45
|
|
|
44
46
|
`buildVeterinaryResearchStudyGroupCreateEntry` and
|
|
45
|
-
`buildVeterinaryResearchStudyGroupMemberPatchEntries` build
|
|
47
|
+
`buildVeterinaryResearchStudyGroupMemberPatchEntries` build flat-claim Group
|
|
46
48
|
POST/PATCH entries for an enumerated professional review team. Every member is
|
|
47
49
|
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
|
-
a mixed-resource gateway batch. Group itself contains no reverse study link
|
|
55
|
-
and grants no authorization, permission or SMART scope.
|
|
50
|
+
derived from `vet-data-utils-ts/group`, and each member has its own PATCH entry.
|
|
51
|
+
The Group stays independent: individual professionals are related to the study
|
|
52
|
+
through separate `ResearchStudy.associatedParty` PATCH entries. It must not use
|
|
53
|
+
`ResearchStudy.recruitment.eligibility`, which represents research-subject
|
|
54
|
+
cohorts or eligibility criteria. Group grants no authorization, permission or
|
|
55
|
+
SMART scope; DCR and active Consent remain separate.
|
|
56
56
|
|
|
57
57
|
The complete controller-to-professional contract and a browser-safe snippet
|
|
58
58
|
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,46 +14,16 @@ 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
|
resourceType: 'Consent';
|
|
30
19
|
id: string;
|
|
31
|
-
status: 'draft';
|
|
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
20
|
meta: Readonly<{
|
|
49
21
|
claims: Readonly<Record<string, string>>;
|
|
50
22
|
}>;
|
|
51
23
|
}>;
|
|
52
|
-
export type
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
entry: readonly Readonly<{
|
|
56
|
-
fullUrl: string;
|
|
24
|
+
export type VeterinaryResearchStudyInvitationDocument = Readonly<{
|
|
25
|
+
data: readonly Readonly<{
|
|
26
|
+
type: 'Consent-v5.0.0';
|
|
57
27
|
resource: VeterinaryResearchStudyDraftConsent;
|
|
58
28
|
request: Readonly<{
|
|
59
29
|
method: 'POST';
|
|
@@ -64,22 +34,12 @@ export type VeterinaryResearchStudyInvitationBundle = Readonly<{
|
|
|
64
34
|
export type VeterinaryResearchStudyPartyInvitation = Readonly<{
|
|
65
35
|
resourceType: 'Communication';
|
|
66
36
|
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
|
-
}>[];
|
|
37
|
+
meta: Readonly<{
|
|
38
|
+
claims: Readonly<Record<string, string>>;
|
|
39
|
+
}>;
|
|
81
40
|
}>;
|
|
82
|
-
|
|
41
|
+
/** Native-shaped builder input used only to derive the persisted flat claims. */
|
|
42
|
+
export type VeterinaryResearchStudySourceResource = Readonly<{
|
|
83
43
|
resourceType: 'ResearchStudy';
|
|
84
44
|
id: string;
|
|
85
45
|
status?: 'draft' | 'active' | 'retired' | 'unknown';
|
|
@@ -87,9 +47,10 @@ export type VeterinaryResearchStudyResource = Readonly<{
|
|
|
87
47
|
meta?: Readonly<Record<string, unknown>>;
|
|
88
48
|
[key: string]: unknown;
|
|
89
49
|
}>;
|
|
90
|
-
export type VeterinaryResearchStudyCreateResource =
|
|
91
|
-
status: NonNullable<
|
|
50
|
+
export type VeterinaryResearchStudyCreateResource = VeterinaryResearchStudySourceResource & Readonly<{
|
|
51
|
+
status: NonNullable<VeterinaryResearchStudySourceResource['status']>;
|
|
92
52
|
}>;
|
|
53
|
+
export type VeterinaryResearchStudyResource = ResearchStudyR5FlatClaimsResource;
|
|
93
54
|
export type VeterinaryResearchStudyBatchEntry = Readonly<{
|
|
94
55
|
type: 'ResearchStudy-v5.0.0';
|
|
95
56
|
resource: VeterinaryResearchStudyResource;
|
|
@@ -101,13 +62,7 @@ export type VeterinaryResearchStudyBatchEntry = Readonly<{
|
|
|
101
62
|
export type VeterinaryResearchStudyBatch = Readonly<{
|
|
102
63
|
data: readonly VeterinaryResearchStudyBatchEntry[];
|
|
103
64
|
}>;
|
|
104
|
-
export type VeterinaryResearchStudyGroupResource =
|
|
105
|
-
id: string;
|
|
106
|
-
meta: Readonly<{
|
|
107
|
-
claims: Readonly<Record<string, readonly string[]>>;
|
|
108
|
-
[key: string]: unknown;
|
|
109
|
-
}>;
|
|
110
|
-
}>;
|
|
65
|
+
export type VeterinaryResearchStudyGroupResource = GroupR5FlatClaimsResource;
|
|
111
66
|
export type VeterinaryResearchStudyGroupBatchEntry = Readonly<{
|
|
112
67
|
type: 'Group-v5.0.0';
|
|
113
68
|
resource: VeterinaryResearchStudyGroupResource;
|
|
@@ -135,7 +90,6 @@ export type VeterinaryResearchStudyActiveConsentInput = Readonly<{
|
|
|
135
90
|
export type VeterinaryResearchStudyActiveConsent = Readonly<{
|
|
136
91
|
resourceType: typeof ResourceTypesFhirR5.Consent;
|
|
137
92
|
id: string;
|
|
138
|
-
status: typeof ConsentStatuses.Active;
|
|
139
93
|
meta: Readonly<{
|
|
140
94
|
claims: Readonly<Record<string, string>>;
|
|
141
95
|
}>;
|
|
@@ -179,39 +133,13 @@ export declare function buildVeterinaryResearchStudySmartAuthorization(input: Re
|
|
|
179
133
|
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
180
134
|
*/
|
|
181
135
|
export declare function buildVeterinaryResearchStudyActiveConsentBatch(inputs: readonly VeterinaryResearchStudyActiveConsentInput[]): VeterinaryResearchStudyActiveConsentBatch;
|
|
182
|
-
/** Builds one POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
136
|
+
/** Builds one flat-claims POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
183
137
|
export declare function buildVeterinaryResearchStudyActiveConsentEntry(input: VeterinaryResearchStudyActiveConsentInput): VeterinaryResearchStudyActiveConsentEntry;
|
|
184
138
|
export declare function buildVeterinaryResearchStudyBatch(entries: readonly VeterinaryResearchStudyBatchEntry[]): VeterinaryResearchStudyBatch;
|
|
185
139
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* search claim is added to Group.
|
|
190
|
-
*
|
|
191
|
-
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
192
|
-
* @see https://hl7.org/fhir/R5/group.html
|
|
193
|
-
*/
|
|
194
|
-
export declare function buildVeterinaryResearchStudyEligibilityGroupReference(groupId: string): Readonly<{
|
|
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<{
|
|
208
|
-
researchStudyId: string;
|
|
209
|
-
groupId: string;
|
|
210
|
-
}>): VeterinaryResearchStudyBatchEntry;
|
|
211
|
-
/**
|
|
212
|
-
* Builds the Group POST used to create one enumerated ResearchStudy review
|
|
213
|
-
* team. Every member is a native PractitionerRole reference. Group remains a
|
|
214
|
-
* description only and the generated resource cannot carry access policy.
|
|
140
|
+
* Builds the flat-claims Group POST used to create one enumerated ResearchStudy
|
|
141
|
+
* review team. Every member is added later as its own PractitionerRole claim
|
|
142
|
+
* PATCH. Group remains a description only and cannot carry access policy.
|
|
215
143
|
*
|
|
216
144
|
* @see https://hl7.org/fhir/R5/group.html
|
|
217
145
|
* @see https://hl7.org/fhir/R5/group-search.html
|
|
@@ -235,8 +163,9 @@ export declare function buildVeterinaryResearchStudyCreateEntry(input: Readonly<
|
|
|
235
163
|
}>): VeterinaryResearchStudyBatchEntry;
|
|
236
164
|
/**
|
|
237
165
|
* Builds one independent `_batch` PATCH entry per R5 associatedParty. Keeping
|
|
238
|
-
* each
|
|
239
|
-
* from being correlated with the wrong PractitionerRole.
|
|
166
|
+
* each party's claim map in its own entry prevents roles, periods and
|
|
167
|
+
* classifiers from being correlated with the wrong PractitionerRole. Native
|
|
168
|
+
* `associatedParty` JSON is not sent to persistence. These entries update
|
|
240
169
|
* study membership only; they do not grant CRUDS access.
|
|
241
170
|
*/
|
|
242
171
|
export declare function buildVeterinaryResearchStudyPartyPatchEntries(input: Readonly<{
|
|
@@ -245,17 +174,15 @@ export declare function buildVeterinaryResearchStudyPartyPatchEntries(input: Rea
|
|
|
245
174
|
}>): readonly VeterinaryResearchStudyBatchEntry[];
|
|
246
175
|
/**
|
|
247
176
|
* Builds the auditable R5 invitation sent to one ResearchStudy associated
|
|
248
|
-
* party. The Communication
|
|
249
|
-
*
|
|
177
|
+
* party. The claims-only Communication carries an `application/vnd.api+json`
|
|
178
|
+
* primary document with exactly one claims-only `Consent.status = draft`.
|
|
250
179
|
* Draft is descriptive only and can never satisfy authorization. The invitee
|
|
251
180
|
* must finish DCR and a controller must later persist a separate active
|
|
252
181
|
* Consent before the study-scoped SMART request can succeed.
|
|
253
182
|
*
|
|
254
|
-
*
|
|
255
|
-
* `
|
|
256
|
-
*
|
|
257
|
-
* search; their FHIR representation uses the matching REST interaction codes,
|
|
258
|
-
* where search is `search-type`. Delete is deliberately absent.
|
|
183
|
+
* The flat Consent claims retain the exact study boundary and affected
|
|
184
|
+
* `ResearchSubject` resource family. Delete is deliberately absent. Native
|
|
185
|
+
* FHIR projection belongs only at an explicit import/export boundary.
|
|
259
186
|
*
|
|
260
187
|
* @see https://hl7.org/fhir/R5/communication.html
|
|
261
188
|
* @see https://hl7.org/fhir/R5/consent.html
|
|
@@ -284,6 +211,5 @@ export declare function buildVeterinaryResearchStudyPartyInvitations(input: Read
|
|
|
284
211
|
consentId: string;
|
|
285
212
|
}>[];
|
|
286
213
|
}>): readonly VeterinaryResearchStudyPartyInvitation[];
|
|
287
|
-
/** Decodes the
|
|
288
|
-
export declare function
|
|
289
|
-
export {};
|
|
214
|
+
/** Decodes the attached JSON:API Consent document after validating its claims-only boundary. */
|
|
215
|
+
export declare function decodeVeterinaryResearchStudyInvitationDocument(communication: VeterinaryResearchStudyPartyInvitation): VeterinaryResearchStudyInvitationDocument;
|
package/dist/research-study.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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
|
-
const FHIR_RESOURCE_TYPES_SYSTEM = 'http://hl7.org/fhir/fhir-types';
|
|
9
|
+
import { CommunicationClaim } from 'gdc-common-utils-ts/models/interoperable-claims/communication-claims';
|
|
10
10
|
export const VeterinaryResearchStudyPermissionActions = Object.freeze([
|
|
11
11
|
'create',
|
|
12
12
|
'read',
|
|
@@ -69,7 +69,7 @@ export function buildVeterinaryResearchStudyActiveConsentBatch(inputs) {
|
|
|
69
69
|
const data = inputs.map(buildVeterinaryResearchStudyActiveConsentEntry);
|
|
70
70
|
return deepFreeze({ data });
|
|
71
71
|
}
|
|
72
|
-
/** Builds one POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
72
|
+
/** Builds one flat-claims POST Consent entry accepted by the VET ResearchStudy manager. */
|
|
73
73
|
export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
|
|
74
74
|
if (input.status !== undefined && input.status !== ConsentStatuses.Active) {
|
|
75
75
|
throw new TypeError('veterinary_research_study_active_consent_status_invalid');
|
|
@@ -88,7 +88,6 @@ export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
|
|
|
88
88
|
resource: {
|
|
89
89
|
resourceType: ResourceTypesFhirR5.Consent,
|
|
90
90
|
id: consentId,
|
|
91
|
-
status: ConsentStatuses.Active,
|
|
92
91
|
meta: {
|
|
93
92
|
claims: {
|
|
94
93
|
'@context': InteroperableContext.FhirApi,
|
|
@@ -109,58 +108,23 @@ export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
|
|
|
109
108
|
export function buildVeterinaryResearchStudyBatch(entries) {
|
|
110
109
|
if (!Array.isArray(entries) || entries.length === 0)
|
|
111
110
|
throw new TypeError('veterinary_research_study_batch_empty');
|
|
112
|
-
return deepFreeze({ data: entries.map(
|
|
111
|
+
return deepFreeze({ data: entries.map(normalizeResearchStudyBatchEntry) });
|
|
113
112
|
}
|
|
114
113
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* search claim is added to Group.
|
|
119
|
-
*
|
|
120
|
-
* @see https://hl7.org/fhir/R5/researchstudy.html
|
|
121
|
-
* @see https://hl7.org/fhir/R5/group.html
|
|
122
|
-
*/
|
|
123
|
-
export function buildVeterinaryResearchStudyEligibilityGroupReference(groupId) {
|
|
124
|
-
return deepFreeze({ reference: `Group/${boundedGroupId(groupId)}`, type: 'Group' });
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Builds the native ResearchStudy PATCH that links a descriptive Group through
|
|
128
|
-
* `ResearchStudy.recruitment.eligibility`. The existing ResearchStudy batch
|
|
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);
|
|
141
|
-
return deepFreeze({
|
|
142
|
-
type: 'ResearchStudy-v5.0.0',
|
|
143
|
-
resource: withProjectedClaims({
|
|
144
|
-
resourceType: 'ResearchStudy',
|
|
145
|
-
id: researchStudyId,
|
|
146
|
-
recruitment: { eligibility },
|
|
147
|
-
}),
|
|
148
|
-
request: { method: 'PATCH', url: `ResearchStudy/${researchStudyId}` },
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Builds the Group POST used to create one enumerated ResearchStudy review
|
|
153
|
-
* team. Every member is a native PractitionerRole reference. Group remains a
|
|
154
|
-
* description only and the generated resource cannot carry access policy.
|
|
114
|
+
* Builds the flat-claims Group POST used to create one enumerated ResearchStudy
|
|
115
|
+
* review team. Every member is added later as its own PractitionerRole claim
|
|
116
|
+
* PATCH. Group remains a description only and cannot carry access policy.
|
|
155
117
|
*
|
|
156
118
|
* @see https://hl7.org/fhir/R5/group.html
|
|
157
119
|
* @see https://hl7.org/fhir/R5/group-search.html
|
|
158
120
|
*/
|
|
159
121
|
export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
|
|
160
122
|
const group = normalizedResearchTeamGroup(input.group);
|
|
123
|
+
if (group.member.length > 0)
|
|
124
|
+
throw new TypeError('group_members_require_patch_entries');
|
|
161
125
|
return deepFreeze({
|
|
162
126
|
type: 'Group-v5.0.0',
|
|
163
|
-
resource:
|
|
127
|
+
resource: projectGroupR5FlatClaimsResource(group),
|
|
164
128
|
request: { method: 'POST', url: 'Group' },
|
|
165
129
|
});
|
|
166
130
|
}
|
|
@@ -171,16 +135,10 @@ export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
|
|
|
171
135
|
*/
|
|
172
136
|
export function buildVeterinaryResearchStudyGroupMemberPatchEntries(input) {
|
|
173
137
|
const id = boundedGroupId(input.groupId);
|
|
174
|
-
const
|
|
175
|
-
return deepFreeze(
|
|
138
|
+
const resources = projectGroupPractitionerRoleMemberFlatClaimResources({ groupId: id, members: input.members });
|
|
139
|
+
return deepFreeze(resources.map(resource => ({
|
|
176
140
|
type: 'Group-v5.0.0',
|
|
177
|
-
resource
|
|
178
|
-
resourceType: 'Group',
|
|
179
|
-
id,
|
|
180
|
-
type: 'practitioner',
|
|
181
|
-
membership: 'enumerated',
|
|
182
|
-
member: [member],
|
|
183
|
-
})),
|
|
141
|
+
resource,
|
|
184
142
|
request: { method: 'PATCH', url: `Group/${id}` },
|
|
185
143
|
})));
|
|
186
144
|
}
|
|
@@ -195,42 +153,40 @@ export function buildVeterinaryResearchStudyCreateEntry(input) {
|
|
|
195
153
|
const study = normalizedStudy(input.study);
|
|
196
154
|
return deepFreeze({
|
|
197
155
|
type: 'ResearchStudy-v5.0.0',
|
|
198
|
-
resource:
|
|
156
|
+
resource: projectResearchStudyR5FlatClaimsResource(study),
|
|
199
157
|
request: { method: 'POST', url: 'ResearchStudy' },
|
|
200
158
|
});
|
|
201
159
|
}
|
|
202
160
|
/**
|
|
203
161
|
* Builds one independent `_batch` PATCH entry per R5 associatedParty. Keeping
|
|
204
|
-
* each
|
|
205
|
-
* from being correlated with the wrong PractitionerRole.
|
|
162
|
+
* each party's claim map in its own entry prevents roles, periods and
|
|
163
|
+
* classifiers from being correlated with the wrong PractitionerRole. Native
|
|
164
|
+
* `associatedParty` JSON is not sent to persistence. These entries update
|
|
206
165
|
* study membership only; they do not grant CRUDS access.
|
|
207
166
|
*/
|
|
208
167
|
export function buildVeterinaryResearchStudyPartyPatchEntries(input) {
|
|
209
168
|
const id = boundedId(input.researchStudyId);
|
|
210
|
-
const
|
|
211
|
-
|
|
169
|
+
const resources = projectResearchStudyAssociatedPartyFlatClaimResources({
|
|
170
|
+
researchStudyId: id,
|
|
171
|
+
associatedParties: input.associatedParties,
|
|
172
|
+
});
|
|
173
|
+
return deepFreeze(resources.map(resource => ({
|
|
212
174
|
type: 'ResearchStudy-v5.0.0',
|
|
213
|
-
resource
|
|
214
|
-
resourceType: 'ResearchStudy',
|
|
215
|
-
id,
|
|
216
|
-
associatedParty: [party],
|
|
217
|
-
}),
|
|
175
|
+
resource,
|
|
218
176
|
request: { method: 'PATCH', url: `ResearchStudy/${id}` },
|
|
219
177
|
})));
|
|
220
178
|
}
|
|
221
179
|
/**
|
|
222
180
|
* Builds the auditable R5 invitation sent to one ResearchStudy associated
|
|
223
|
-
* party. The Communication
|
|
224
|
-
*
|
|
181
|
+
* party. The claims-only Communication carries an `application/vnd.api+json`
|
|
182
|
+
* primary document with exactly one claims-only `Consent.status = draft`.
|
|
225
183
|
* Draft is descriptive only and can never satisfy authorization. The invitee
|
|
226
184
|
* must finish DCR and a controller must later persist a separate active
|
|
227
185
|
* Consent before the study-scoped SMART request can succeed.
|
|
228
186
|
*
|
|
229
|
-
*
|
|
230
|
-
* `
|
|
231
|
-
*
|
|
232
|
-
* search; their FHIR representation uses the matching REST interaction codes,
|
|
233
|
-
* where search is `search-type`. Delete is deliberately absent.
|
|
187
|
+
* The flat Consent claims retain the exact study boundary and affected
|
|
188
|
+
* `ResearchSubject` resource family. Delete is deliberately absent. Native
|
|
189
|
+
* FHIR projection belongs only at an explicit import/export boundary.
|
|
234
190
|
*
|
|
235
191
|
* @see https://hl7.org/fhir/R5/communication.html
|
|
236
192
|
* @see https://hl7.org/fhir/R5/consent.html
|
|
@@ -258,64 +214,46 @@ export function buildVeterinaryResearchStudyPartyInvitation(input) {
|
|
|
258
214
|
const consent = {
|
|
259
215
|
resourceType: 'Consent',
|
|
260
216
|
id: consentId,
|
|
261
|
-
status: 'draft',
|
|
262
|
-
grantor: [{ reference: controllerReference, type: 'Organization' }],
|
|
263
|
-
controller: [{ reference: controllerReference, type: 'Organization' }],
|
|
264
|
-
grantee: [{ reference: partyReference, type: 'PractitionerRole' }],
|
|
265
|
-
decision: 'permit',
|
|
266
|
-
provision: [{
|
|
267
|
-
actor: [{ reference: { reference: partyReference, type: 'PractitionerRole' } }],
|
|
268
|
-
action: VeterinaryResearchStudyPermissionActions.map(action => ({
|
|
269
|
-
coding: [{
|
|
270
|
-
system: FHIR_RESTFUL_INTERACTION_SYSTEM,
|
|
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
217
|
meta: {
|
|
279
218
|
claims: {
|
|
280
|
-
[VeterinaryResearchStudyInvitationClaimNames.Context]:
|
|
281
|
-
[VeterinaryResearchStudyInvitationClaimNames.Status]:
|
|
282
|
-
[VeterinaryResearchStudyInvitationClaimNames.Decision]:
|
|
219
|
+
[VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
|
|
220
|
+
[VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
|
|
221
|
+
[VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
|
|
283
222
|
[VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
|
|
223
|
+
[ClaimConsent.grantee]: partyReference,
|
|
284
224
|
[VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
|
|
285
225
|
[VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
|
|
286
226
|
[VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
|
|
227
|
+
[ClaimConsent.sourceReference]: researchStudyReference,
|
|
287
228
|
},
|
|
288
229
|
},
|
|
289
230
|
};
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
entry: [{
|
|
294
|
-
fullUrl: `urn:uuid:${consentId}`,
|
|
231
|
+
const document = {
|
|
232
|
+
data: [{
|
|
233
|
+
type: 'Consent-v5.0.0',
|
|
295
234
|
resource: consent,
|
|
296
235
|
request: { method: 'POST', url: 'Consent' },
|
|
297
236
|
}],
|
|
298
237
|
};
|
|
299
238
|
const category = VeterinaryCommunicationPresetFilters.ResearchAgreements.categories[0];
|
|
300
|
-
const [categorySystem, categoryCode] = splitCodingToken(category);
|
|
301
239
|
const sent = optionalInstant(input.sentAt);
|
|
302
240
|
return deepFreeze({
|
|
303
241
|
resourceType: 'Communication',
|
|
304
242
|
id: communicationId,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
243
|
+
meta: { claims: {
|
|
244
|
+
'@context': InteroperableContext.FhirApi,
|
|
245
|
+
[CommunicationClaim.Identifier]: communicationId,
|
|
246
|
+
[CommunicationClaim.Status]: 'completed',
|
|
247
|
+
[CommunicationClaim.Category]: category,
|
|
248
|
+
[CommunicationClaim.Topic]: topic.value,
|
|
249
|
+
[CommunicationClaim.ContentReference]: researchStudyReference,
|
|
250
|
+
[CommunicationClaim.Recipient]: partyReference,
|
|
251
|
+
[CommunicationClaim.Sender]: controllerReference,
|
|
252
|
+
...(sent ? { [CommunicationClaim.Sent]: sent } : {}),
|
|
253
|
+
[CommunicationClaim.ContentAttachmentType]: HttpMediaTypes.JsonApi,
|
|
254
|
+
[CommunicationClaim.ContentAttachmentTitle]: 'research-study-draft-consent.json',
|
|
255
|
+
[CommunicationClaim.ContentAttachmentData]: encodeJsonBase64(document),
|
|
256
|
+
} },
|
|
319
257
|
});
|
|
320
258
|
}
|
|
321
259
|
/**
|
|
@@ -334,41 +272,50 @@ export function buildVeterinaryResearchStudyPartyInvitations(input) {
|
|
|
334
272
|
...invitation,
|
|
335
273
|
})));
|
|
336
274
|
}
|
|
337
|
-
/** Decodes the
|
|
338
|
-
export function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
275
|
+
/** Decodes the attached JSON:API Consent document after validating its claims-only boundary. */
|
|
276
|
+
export function decodeVeterinaryResearchStudyInvitationDocument(communication) {
|
|
277
|
+
if (!isClaimsOnlyResource(communication, 'Communication')) {
|
|
278
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
279
|
+
}
|
|
280
|
+
const claims = communication.meta.claims;
|
|
281
|
+
const encoded = claims[CommunicationClaim.ContentAttachmentData];
|
|
282
|
+
if (claims[CommunicationClaim.ContentAttachmentType] !== HttpMediaTypes.JsonApi || !encoded) {
|
|
283
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
284
|
+
}
|
|
285
|
+
let decoded;
|
|
286
|
+
try {
|
|
287
|
+
decoded = JSON.parse(decodeJsonBase64(encoded));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
344
291
|
}
|
|
345
|
-
const
|
|
346
|
-
if (decoded?.
|
|
347
|
-
||
|
|
348
|
-
||
|
|
349
|
-
||
|
|
350
|
-
||
|
|
351
|
-
|
|
292
|
+
const entry = decoded?.data?.[0];
|
|
293
|
+
if (decoded?.data?.length !== 1
|
|
294
|
+
|| entry?.type !== 'Consent-v5.0.0'
|
|
295
|
+
|| entry.request?.method !== 'POST'
|
|
296
|
+
|| entry.request.url !== 'Consent'
|
|
297
|
+
|| !isClaimsOnlyResource(entry.resource, 'Consent')
|
|
298
|
+
|| entry.resource.meta.claims[ClaimConsent.status] !== ConsentStatuses.Draft) {
|
|
299
|
+
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
352
300
|
}
|
|
353
301
|
return deepFreeze(decoded);
|
|
354
302
|
}
|
|
355
303
|
function normalizedStudy(input) {
|
|
356
304
|
if (!input || input.resourceType !== 'ResearchStudy')
|
|
357
305
|
throw new TypeError('veterinary_research_study_invalid');
|
|
306
|
+
if (input.associatedParty !== undefined) {
|
|
307
|
+
throw new TypeError('veterinary_research_study_parties_require_patch_entries');
|
|
308
|
+
}
|
|
358
309
|
const source = clonePlain(input);
|
|
359
310
|
const id = boundedId(input.id);
|
|
360
311
|
const status = String(input.status || '');
|
|
361
312
|
if (!['draft', 'active', 'retired', 'unknown'].includes(status)) {
|
|
362
313
|
throw new TypeError('veterinary_research_study_status_invalid');
|
|
363
314
|
}
|
|
364
|
-
const associatedParty = input.associatedParty === undefined
|
|
365
|
-
? undefined
|
|
366
|
-
: normalizeResearchStudyAssociatedParties(input.associatedParty);
|
|
367
315
|
return deepFreeze({
|
|
368
316
|
...source,
|
|
369
317
|
id,
|
|
370
318
|
status: status,
|
|
371
|
-
...(associatedParty ? { associatedParty } : {}),
|
|
372
319
|
});
|
|
373
320
|
}
|
|
374
321
|
function normalizedResearchTeamGroup(input) {
|
|
@@ -380,28 +327,24 @@ function normalizeResearchStudyGroupEntry(input) {
|
|
|
380
327
|
if (!input || input.type !== 'Group-v5.0.0') {
|
|
381
328
|
throw new TypeError('veterinary_research_study_group_entry_invalid');
|
|
382
329
|
}
|
|
383
|
-
|
|
330
|
+
let resource;
|
|
331
|
+
try {
|
|
332
|
+
resource = normalizeGroupR5FlatClaimsResource(input.resource);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
throw new TypeError('veterinary_research_study_group_entry_invalid');
|
|
336
|
+
}
|
|
384
337
|
const method = input.request?.method;
|
|
385
|
-
const expectedUrl = method === 'POST' ? 'Group' : method === 'PATCH' ? `Group/${
|
|
338
|
+
const expectedUrl = method === 'POST' ? 'Group' : method === 'PATCH' ? `Group/${resource.id}` : '';
|
|
386
339
|
if (!expectedUrl || input.request.url !== expectedUrl) {
|
|
387
340
|
throw new TypeError('veterinary_research_study_group_request_invalid');
|
|
388
341
|
}
|
|
389
342
|
return deepFreeze({
|
|
390
343
|
type: 'Group-v5.0.0',
|
|
391
|
-
resource
|
|
344
|
+
resource,
|
|
392
345
|
request: { method, url: expectedUrl },
|
|
393
346
|
});
|
|
394
347
|
}
|
|
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
348
|
function clonePlain(value) {
|
|
406
349
|
if (Array.isArray(value))
|
|
407
350
|
return value.map(clonePlain);
|
|
@@ -410,27 +353,32 @@ function clonePlain(value) {
|
|
|
410
353
|
}
|
|
411
354
|
return value;
|
|
412
355
|
}
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
356
|
+
function normalizeResearchStudyBatchEntry(input) {
|
|
357
|
+
if (!input || input.type !== 'ResearchStudy-v5.0.0') {
|
|
358
|
+
throw new TypeError('veterinary_research_study_entry_invalid');
|
|
359
|
+
}
|
|
360
|
+
let resource;
|
|
361
|
+
try {
|
|
362
|
+
resource = normalizeResearchStudyR5FlatClaimsResource(input.resource);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
throw new TypeError('veterinary_research_study_entry_invalid');
|
|
366
|
+
}
|
|
367
|
+
const method = input.request?.method;
|
|
368
|
+
const expectedUrl = method === 'POST'
|
|
369
|
+
? 'ResearchStudy'
|
|
370
|
+
: method === 'PATCH'
|
|
371
|
+
? `ResearchStudy/${resource.id}`
|
|
372
|
+
: '';
|
|
373
|
+
if (!expectedUrl || input.request.url !== expectedUrl) {
|
|
374
|
+
throw new TypeError('veterinary_research_study_request_invalid');
|
|
375
|
+
}
|
|
416
376
|
return deepFreeze({
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
claims: { ...standardClaims, ...partyClaims },
|
|
421
|
-
},
|
|
377
|
+
type: 'ResearchStudy-v5.0.0',
|
|
378
|
+
resource,
|
|
379
|
+
request: { method, url: expectedUrl },
|
|
422
380
|
});
|
|
423
381
|
}
|
|
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
382
|
function boundedId(value) {
|
|
435
383
|
const id = String(value || '').trim();
|
|
436
384
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
@@ -470,23 +418,6 @@ function optionalInstant(value) {
|
|
|
470
418
|
throw new TypeError('veterinary_research_study_invitation_sent_invalid');
|
|
471
419
|
return instant;
|
|
472
420
|
}
|
|
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
421
|
function encodeJsonBase64(value) {
|
|
491
422
|
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
492
423
|
let binary = '';
|
|
@@ -507,3 +438,15 @@ function deepFreeze(value, seen = new WeakSet()) {
|
|
|
507
438
|
deepFreeze(nested, seen);
|
|
508
439
|
return Object.freeze(value);
|
|
509
440
|
}
|
|
441
|
+
function isRecord(value) {
|
|
442
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
443
|
+
}
|
|
444
|
+
function isClaimsOnlyResource(value, resourceType) {
|
|
445
|
+
if (!isRecord(value) || value.resourceType !== resourceType)
|
|
446
|
+
return false;
|
|
447
|
+
if (Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
|
|
448
|
+
return false;
|
|
449
|
+
if (typeof value.id !== 'string' || !isRecord(value.meta) || !isRecord(value.meta.claims))
|
|
450
|
+
return false;
|
|
451
|
+
return Object.values(value.meta.claims).every(claim => typeof claim === 'string' && claim.trim().length > 0);
|
|
452
|
+
}
|
|
@@ -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,
|
|
@@ -42,13 +41,6 @@ const memberPatches = buildVeterinaryResearchStudyGroupMemberPatchEntries({
|
|
|
42
41
|
})
|
|
43
42
|
const groupBatch = buildVeterinaryResearchStudyGroupBatch([groupCreate, ...memberPatches])
|
|
44
43
|
// 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
44
|
|
|
53
45
|
const invitations = buildVeterinaryResearchStudyPartyInvitations({
|
|
54
46
|
researchStudyId: study.id,
|
|
@@ -89,14 +81,26 @@ const authorization = buildVeterinaryResearchStudySmartAuthorization({
|
|
|
89
81
|
await professionalRuntime.requestSmartToken(authorization)
|
|
90
82
|
```
|
|
91
83
|
|
|
92
|
-
The controller may
|
|
84
|
+
The controller may independently create a descriptive review team with
|
|
93
85
|
`buildVeterinaryResearchStudyGroupCreateEntry()` and append each registered
|
|
94
86
|
`PractitionerRole` with
|
|
95
|
-
`buildVeterinaryResearchStudyGroupMemberPatchEntries()`.
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
ResearchStudy
|
|
99
|
-
|
|
87
|
+
`buildVeterinaryResearchStudyGroupMemberPatchEntries()`. Individual
|
|
88
|
+
professionals are represented in the study by their separate
|
|
89
|
+
`ResearchStudy.associatedParty` PATCH entries. The professional Group is not
|
|
90
|
+
`ResearchStudy.recruitment.eligibility`: that FHIR element describes the
|
|
91
|
+
research-subject cohort or its eligibility criteria. Group membership is not
|
|
92
|
+
evidence of DCR or Consent and never grants the SMART authorization above.
|
|
93
|
+
|
|
94
|
+
GW persistence entries for ResearchStudy, professional Group and active
|
|
95
|
+
Consent contain only `resourceType`, `id` and `resource.meta.claims`. Their
|
|
96
|
+
FHIR-like field names are flat claims. Native nested FHIR JSON is reserved for
|
|
97
|
+
an explicit export/projection boundary, not these GW builders.
|
|
98
|
+
|
|
99
|
+
The draft Consent inside the Communication attachment is an explicit
|
|
100
|
+
`application/vnd.api+json` proposal for recipient review. Both the outer
|
|
101
|
+
Communication and the attached Consent contain only `resourceType`, `id` and
|
|
102
|
+
`resource.meta.claims`. It never authorizes access; only the later flat-claims
|
|
103
|
+
active Consent POST is evaluated by GW.
|
|
100
104
|
|
|
101
105
|
These builders follow the native [FHIR R5 Group resource](https://hl7.org/fhir/R5/group.html)
|
|
102
106
|
and its exact [resource-specific search catalogue](https://hl7.org/fhir/R5/group-search.html).
|
|
@@ -107,17 +111,18 @@ the authorization subject `ResearchStudy/<id>` from the exact unescaped scope.
|
|
|
107
111
|
Percent-encoding the `/` in that scope would no longer match the active
|
|
108
112
|
Consent action and is rejected.
|
|
109
113
|
|
|
110
|
-
The
|
|
111
|
-
`Communication.
|
|
112
|
-
`
|
|
114
|
+
The JSON:API primary document is base64 encoded in canonical
|
|
115
|
+
`Communication.content-attachment-data`, paired with
|
|
116
|
+
`Communication.content-attachment-type = application/vnd.api+json`. The
|
|
117
|
+
Consent claims use:
|
|
113
118
|
|
|
114
|
-
- `Consent.grantee` and `Consent.
|
|
119
|
+
- `Consent.grantee` and `Consent.actor-identifier` for the invited
|
|
115
120
|
`PractitionerRole`;
|
|
116
|
-
-
|
|
117
|
-
- `Consent.
|
|
118
|
-
- `Consent.
|
|
119
|
-
- `Consent.
|
|
120
|
-
-
|
|
121
|
+
- outer `Communication.sender` for the controller organization;
|
|
122
|
+
- `Consent.purpose` with HL7 v3 ActReason `HRESCH`;
|
|
123
|
+
- `Consent.resource-type` for `ResearchSubject`;
|
|
124
|
+
- `Consent.source-reference` for the exact `ResearchStudy/{id}`;
|
|
125
|
+
- `Consent.action` for `create`, `read`, `update`, and `search`.
|
|
121
126
|
|
|
122
127
|
FHIR R5 references:
|
|
123
128
|
|
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.14",
|
|
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
|
}
|