vet-sdk-core-ts 0.4.14 → 0.4.16
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 +15 -1
- package/dist/research-study.d.ts +52 -12
- package/dist/research-study.js +146 -23
- package/docs/101-RESEARCH-STUDY-INVITATIONS.md +34 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,14 @@ owns gateway request construction. GW VET remains the policy authority.
|
|
|
8
8
|
|
|
9
9
|
## Research studies
|
|
10
10
|
|
|
11
|
+
`buildVeterinaryResearchStudyCreateWorkflowIds` accepts a client-generated
|
|
12
|
+
operation UUID and the exact `PractitionerRole/...` references. It reuses the
|
|
13
|
+
operation UUID as `researchStudyId` and deterministically derives distinct
|
|
14
|
+
UUIDv5 values for the Group and every Communication/draft Consent pair. A
|
|
15
|
+
retry therefore addresses the same resources and a partially completed saga
|
|
16
|
+
can resume safely. Stable identifiers do not make separate service calls
|
|
17
|
+
atomic and confer no membership, Consent or SMART authority.
|
|
18
|
+
|
|
11
19
|
`buildVeterinaryResearchStudyCreateEntry` accepts ResearchStudy business data
|
|
12
20
|
and projects it into the canonical JSON:API resource object: `resourceType`,
|
|
13
21
|
`id` and flat `resource.meta.claims`. It does not send native `status`, `title`,
|
|
@@ -37,7 +45,9 @@ cannot be broadened to delete.
|
|
|
37
45
|
`buildVeterinaryResearchStudyPartyInvitations` creates exactly one claims-only
|
|
38
46
|
`Communication` per associated `PractitionerRole`. Its canonical attachment
|
|
39
47
|
claims carry an `application/vnd.api+json` primary document containing one
|
|
40
|
-
|
|
48
|
+
JSON:API resource object with `type`, `id` and flat Consent claims in
|
|
49
|
+
`attributes`; `resource`, `request` and FHIR Bundle shapes are rejected.
|
|
50
|
+
`Consent.source-reference` pins the ResearchStudy
|
|
41
51
|
and `Consent.resource-type` pins `ResearchSubject`. The draft requests only
|
|
42
52
|
create, read, update and search; it contains no delete permission and never
|
|
43
53
|
authorizes SMART access. Native FHIR Communication, Consent or Bundle objects
|
|
@@ -53,6 +63,10 @@ through separate `ResearchStudy.associatedParty` PATCH entries. It must not use
|
|
|
53
63
|
`ResearchStudy.recruitment.eligibility`, which represents research-subject
|
|
54
64
|
cohorts or eligibility criteria. Group grants no authorization, permission or
|
|
55
65
|
SMART scope; DCR and active Consent remain separate.
|
|
66
|
+
The Group create builder requires the ResearchStudy UUID and derives a
|
|
67
|
+
governed `Group.identifier`. Its exported search helper re-derives
|
|
68
|
+
the same deterministic UUIDv5 review-team identifier after refresh without
|
|
69
|
+
using eligibility or granting authority.
|
|
56
70
|
|
|
57
71
|
The complete controller-to-professional contract and a browser-safe snippet
|
|
58
72
|
are in
|
package/dist/research-study.d.ts
CHANGED
|
@@ -15,21 +15,12 @@ export declare const VeterinaryResearchStudyInvitationClaimNames: Readonly<{
|
|
|
15
15
|
readonly ResourceType: ClaimConsent.resourceType;
|
|
16
16
|
}>;
|
|
17
17
|
export type VeterinaryResearchStudyDraftConsent = Readonly<{
|
|
18
|
-
|
|
18
|
+
type: 'Consent-v5.0.0';
|
|
19
19
|
id: string;
|
|
20
|
-
|
|
21
|
-
claims: Readonly<Record<string, string>>;
|
|
22
|
-
}>;
|
|
20
|
+
attributes: Readonly<Record<string, string>>;
|
|
23
21
|
}>;
|
|
24
22
|
export type VeterinaryResearchStudyInvitationDocument = Readonly<{
|
|
25
|
-
data: readonly
|
|
26
|
-
type: 'Consent-v5.0.0';
|
|
27
|
-
resource: VeterinaryResearchStudyDraftConsent;
|
|
28
|
-
request: Readonly<{
|
|
29
|
-
method: 'POST';
|
|
30
|
-
url: 'Consent';
|
|
31
|
-
}>;
|
|
32
|
-
}>[];
|
|
23
|
+
data: readonly VeterinaryResearchStudyDraftConsent[];
|
|
33
24
|
}>;
|
|
34
25
|
export type VeterinaryResearchStudyPartyInvitation = Readonly<{
|
|
35
26
|
resourceType: 'Communication';
|
|
@@ -74,11 +65,46 @@ export type VeterinaryResearchStudyGroupBatchEntry = Readonly<{
|
|
|
74
65
|
export type VeterinaryResearchStudyGroupBatch = Readonly<{
|
|
75
66
|
data: readonly VeterinaryResearchStudyGroupBatchEntry[];
|
|
76
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
|
+
}>;
|
|
81
|
+
export type VeterinaryResearchStudyCreateWorkflowIds = Readonly<{
|
|
82
|
+
researchStudyId: string;
|
|
83
|
+
groupId: string;
|
|
84
|
+
invitations: readonly Readonly<{
|
|
85
|
+
practitionerRoleReference: string;
|
|
86
|
+
communicationId: string;
|
|
87
|
+
consentId: string;
|
|
88
|
+
}>[];
|
|
89
|
+
}>;
|
|
77
90
|
export type VeterinaryResearchStudySmartAuthorization = Readonly<{
|
|
78
91
|
purpose: typeof HealthcareConsentPurposes.Research;
|
|
79
92
|
researchStudyReference: string;
|
|
80
93
|
scopes: readonly [string];
|
|
81
94
|
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Derives every identifier needed to retry or resume one ResearchStudy create
|
|
97
|
+
* workflow. The client-generated operation UUID is the ResearchStudy id and
|
|
98
|
+
* UUIDv5 names produce a stable Group, Communication and draft Consent id.
|
|
99
|
+
* Reusing these ids supports idempotent saga steps; it does not make separate
|
|
100
|
+
* persistence calls atomic and does not convey Consent or SMART authority.
|
|
101
|
+
*
|
|
102
|
+
* @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildVeterinaryResearchStudyCreateWorkflowIds(input: Readonly<{
|
|
105
|
+
operationId: string;
|
|
106
|
+
practitionerRoleReferences: readonly string[];
|
|
107
|
+
}>): VeterinaryResearchStudyCreateWorkflowIds;
|
|
82
108
|
export type VeterinaryResearchStudyActiveConsentInput = Readonly<{
|
|
83
109
|
consentId: string;
|
|
84
110
|
researchStudyId: string;
|
|
@@ -145,8 +171,22 @@ export declare function buildVeterinaryResearchStudyBatch(entries: readonly Vete
|
|
|
145
171
|
* @see https://hl7.org/fhir/R5/group-search.html
|
|
146
172
|
*/
|
|
147
173
|
export declare function buildVeterinaryResearchStudyGroupCreateEntry(input: Readonly<{
|
|
174
|
+
researchStudyId: string;
|
|
148
175
|
group: GroupR5ResearchTeam;
|
|
149
176
|
}>): VeterinaryResearchStudyGroupBatchEntry;
|
|
177
|
+
/** Builds the governed business Identifier; it describes correlation, never authority. */
|
|
178
|
+
export declare function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId: string): Readonly<{
|
|
179
|
+
system: typeof VeterinaryResearchStudyReviewTeamIdentifierSystem;
|
|
180
|
+
value: string;
|
|
181
|
+
}>;
|
|
182
|
+
/**
|
|
183
|
+
* Builds the standard FHIR `Group.identifier` token search used to recover the
|
|
184
|
+
* Group after refresh. Re-derivation avoids storing a second linkage and never
|
|
185
|
+
* constitutes access authority.
|
|
186
|
+
*
|
|
187
|
+
* @see https://hl7.org/fhir/R5/group-search.html
|
|
188
|
+
*/
|
|
189
|
+
export declare function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId: string): VeterinaryResearchStudyReviewTeamGroupSearch;
|
|
150
190
|
/**
|
|
151
191
|
* Builds one independently correlated PATCH per PractitionerRole member. A
|
|
152
192
|
* PATCH changes descriptive membership only; DCR, Consent and SMART remain
|
package/dist/research-study.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ResourceTypesFhirR5 } from 'gdc-common-utils-ts/constants/fhir-resource
|
|
|
7
7
|
import { InteroperableContext } from 'gdc-common-utils-ts/constants/lifecycle';
|
|
8
8
|
import { ClaimConsent, ConsentDecisions, ConsentStatuses, } from 'gdc-common-utils-ts/models/consent-rule';
|
|
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,57 @@ 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';
|
|
36
|
+
/**
|
|
37
|
+
* Derives every identifier needed to retry or resume one ResearchStudy create
|
|
38
|
+
* workflow. The client-generated operation UUID is the ResearchStudy id and
|
|
39
|
+
* UUIDv5 names produce a stable Group, Communication and draft Consent id.
|
|
40
|
+
* Reusing these ids supports idempotent saga steps; it does not make separate
|
|
41
|
+
* persistence calls atomic and does not convey Consent or SMART authority.
|
|
42
|
+
*
|
|
43
|
+
* @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
|
|
44
|
+
*/
|
|
45
|
+
export function buildVeterinaryResearchStudyCreateWorkflowIds(input) {
|
|
46
|
+
let operationId;
|
|
47
|
+
try {
|
|
48
|
+
operationId = normalizedUuid(input?.operationId);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new TypeError('veterinary_research_study_create_operation_id_invalid');
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(input?.practitionerRoleReferences)) {
|
|
54
|
+
throw new TypeError('veterinary_research_study_create_practitioner_role_reference_invalid');
|
|
55
|
+
}
|
|
56
|
+
const references = input.practitionerRoleReferences.map(reference => {
|
|
57
|
+
try {
|
|
58
|
+
return boundedReference(reference, 'PractitionerRole');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new TypeError('veterinary_research_study_create_practitioner_role_reference_invalid');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (new Set(references).size !== references.length) {
|
|
65
|
+
throw new TypeError('veterinary_research_study_create_practitioner_role_duplicate');
|
|
66
|
+
}
|
|
67
|
+
return deepFreeze({
|
|
68
|
+
researchStudyId: operationId,
|
|
69
|
+
groupId: uuidV5('Group', operationId),
|
|
70
|
+
invitations: references.map(practitionerRoleReference => ({
|
|
71
|
+
practitionerRoleReference,
|
|
72
|
+
communicationId: uuidV5(`Communication|${practitionerRoleReference}`, operationId),
|
|
73
|
+
consentId: uuidV5(`Consent|${practitionerRoleReference}`, operationId),
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
25
77
|
/** Builds the exact GW study-pinned ResearchSubject `crus` capability. */
|
|
26
78
|
export function buildVeterinaryResearchStudyAccessScope(researchStudyId) {
|
|
27
79
|
return `organization/ResearchSubject.crus?study=ResearchStudy/${boundedId(researchStudyId)}`;
|
|
@@ -122,12 +174,32 @@ export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
|
|
|
122
174
|
const group = normalizedResearchTeamGroup(input.group);
|
|
123
175
|
if (group.member.length > 0)
|
|
124
176
|
throw new TypeError('group_members_require_patch_entries');
|
|
177
|
+
const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(input.researchStudyId);
|
|
125
178
|
return deepFreeze({
|
|
126
179
|
type: 'Group-v5.0.0',
|
|
127
|
-
resource: projectGroupR5FlatClaimsResource(group),
|
|
180
|
+
resource: projectGroupR5FlatClaimsResource({ ...group, identifier: [identifier] }),
|
|
128
181
|
request: { method: 'POST', url: 'Group' },
|
|
129
182
|
});
|
|
130
183
|
}
|
|
184
|
+
/** Builds the governed business Identifier; it describes correlation, never authority. */
|
|
185
|
+
export function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId) {
|
|
186
|
+
const studyUuid = boundedStudyUuid(researchStudyId);
|
|
187
|
+
return deepFreeze({
|
|
188
|
+
system: VeterinaryResearchStudyReviewTeamIdentifierSystem,
|
|
189
|
+
value: `urn:uuid:${uuidV5(studyUuid, VeterinaryResearchStudyReviewTeamIdentifierNamespace)}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Builds the standard FHIR `Group.identifier` token search used to recover the
|
|
194
|
+
* Group after refresh. Re-derivation avoids storing a second linkage and never
|
|
195
|
+
* constitutes access authority.
|
|
196
|
+
*
|
|
197
|
+
* @see https://hl7.org/fhir/R5/group-search.html
|
|
198
|
+
*/
|
|
199
|
+
export function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId) {
|
|
200
|
+
const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId);
|
|
201
|
+
return deepFreeze({ identifier: `${identifier.system}|${identifier.value}` });
|
|
202
|
+
}
|
|
131
203
|
/**
|
|
132
204
|
* Builds one independently correlated PATCH per PractitionerRole member. A
|
|
133
205
|
* PATCH changes descriptive membership only; DCR, Consent and SMART remain
|
|
@@ -212,28 +284,22 @@ export function buildVeterinaryResearchStudyPartyInvitation(input) {
|
|
|
212
284
|
if (!topic)
|
|
213
285
|
throw new TypeError('veterinary_research_study_invitation_topic_unavailable');
|
|
214
286
|
const consent = {
|
|
215
|
-
|
|
287
|
+
type: 'Consent-v5.0.0',
|
|
216
288
|
id: consentId,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
[ClaimConsent.sourceReference]: researchStudyReference,
|
|
228
|
-
},
|
|
289
|
+
attributes: {
|
|
290
|
+
[VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
|
|
291
|
+
[VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
|
|
292
|
+
[VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
|
|
293
|
+
[VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
|
|
294
|
+
[ClaimConsent.grantee]: partyReference,
|
|
295
|
+
[VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
|
|
296
|
+
[VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
|
|
297
|
+
[VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
|
|
298
|
+
[ClaimConsent.sourceReference]: researchStudyReference,
|
|
229
299
|
},
|
|
230
300
|
};
|
|
231
301
|
const document = {
|
|
232
|
-
data: [
|
|
233
|
-
type: 'Consent-v5.0.0',
|
|
234
|
-
resource: consent,
|
|
235
|
-
request: { method: 'POST', url: 'Consent' },
|
|
236
|
-
}],
|
|
302
|
+
data: [consent],
|
|
237
303
|
};
|
|
238
304
|
const category = VeterinaryCommunicationPresetFilters.ResearchAgreements.categories[0];
|
|
239
305
|
const sent = optionalInstant(input.sentAt);
|
|
@@ -292,10 +358,10 @@ export function decodeVeterinaryResearchStudyInvitationDocument(communication) {
|
|
|
292
358
|
const entry = decoded?.data?.[0];
|
|
293
359
|
if (decoded?.data?.length !== 1
|
|
294
360
|
|| entry?.type !== 'Consent-v5.0.0'
|
|
295
|
-
|| entry.
|
|
296
|
-
|| entry.
|
|
297
|
-
|| !
|
|
298
|
-
|| entry.
|
|
361
|
+
|| Object.keys(entry).some(key => !['type', 'id', 'attributes'].includes(key))
|
|
362
|
+
|| !boundedJsonApiId(entry.id)
|
|
363
|
+
|| !isFlatStringAttributes(entry.attributes)
|
|
364
|
+
|| !isValidInvitationDraftConsentAttributes(entry.attributes, claims)) {
|
|
299
365
|
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
300
366
|
}
|
|
301
367
|
return deepFreeze(decoded);
|
|
@@ -385,6 +451,42 @@ function boundedId(value) {
|
|
|
385
451
|
throw new TypeError('veterinary_research_study_id_invalid');
|
|
386
452
|
return id;
|
|
387
453
|
}
|
|
454
|
+
function boundedStudyUuid(value) {
|
|
455
|
+
try {
|
|
456
|
+
return normalizedUuid(value);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
throw new TypeError('veterinary_research_study_review_team_study_id_invalid');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function normalizedUuid(value) {
|
|
463
|
+
const uuid = String(value || '').trim().toLowerCase();
|
|
464
|
+
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)) {
|
|
465
|
+
throw new TypeError('uuid_invalid');
|
|
466
|
+
}
|
|
467
|
+
return uuid;
|
|
468
|
+
}
|
|
469
|
+
function uuidV5(name, namespace) {
|
|
470
|
+
const namespaceHexBytes = namespace.replace(/-/g, '').match(/.{2}/g) || [];
|
|
471
|
+
const namespaceBytes = Uint8Array.from(namespaceHexBytes.map(byte => Number.parseInt(byte, 16)));
|
|
472
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
473
|
+
const input = new Uint8Array(namespaceBytes.length + nameBytes.length);
|
|
474
|
+
input.set(namespaceBytes);
|
|
475
|
+
input.set(nameBytes, namespaceBytes.length);
|
|
476
|
+
const digest = sha1(input).slice(0, 16);
|
|
477
|
+
digest[6] = (digest[6] & 0x0f) | 0x50;
|
|
478
|
+
digest[8] = (digest[8] & 0x3f) | 0x80;
|
|
479
|
+
const hex = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
480
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
481
|
+
}
|
|
482
|
+
function boundedJsonApiId(value) {
|
|
483
|
+
try {
|
|
484
|
+
return boundedId(value);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
388
490
|
function boundedGroupId(value) {
|
|
389
491
|
const id = String(value || '').trim();
|
|
390
492
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
@@ -450,3 +552,24 @@ function isClaimsOnlyResource(value, resourceType) {
|
|
|
450
552
|
return false;
|
|
451
553
|
return Object.values(value.meta.claims).every(claim => typeof claim === 'string' && claim.trim().length > 0);
|
|
452
554
|
}
|
|
555
|
+
function isFlatStringAttributes(value) {
|
|
556
|
+
return isRecord(value)
|
|
557
|
+
&& Object.keys(value).length > 0
|
|
558
|
+
&& Object.values(value).every(attribute => typeof attribute === 'string' && attribute.trim().length > 0);
|
|
559
|
+
}
|
|
560
|
+
function isValidInvitationDraftConsentAttributes(attributes, communicationClaims) {
|
|
561
|
+
const expected = {
|
|
562
|
+
[VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
|
|
563
|
+
[VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
|
|
564
|
+
[VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
|
|
565
|
+
[VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: communicationClaims[CommunicationClaim.Recipient],
|
|
566
|
+
[ClaimConsent.grantee]: communicationClaims[CommunicationClaim.Recipient],
|
|
567
|
+
[VeterinaryResearchStudyInvitationClaimNames.Purpose]: communicationClaims[CommunicationClaim.Topic],
|
|
568
|
+
[VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
|
|
569
|
+
[VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
|
|
570
|
+
[ClaimConsent.sourceReference]: communicationClaims[CommunicationClaim.ContentReference],
|
|
571
|
+
};
|
|
572
|
+
const expectedEntries = Object.entries(expected);
|
|
573
|
+
return Object.keys(attributes).length === expectedEntries.length
|
|
574
|
+
&& expectedEntries.every(([name, value]) => Boolean(value) && attributes[name] === value);
|
|
575
|
+
}
|
|
@@ -18,6 +18,7 @@ related to the exact ResearchStudy. Whole-twin delete is absent.
|
|
|
18
18
|
```ts
|
|
19
19
|
import {
|
|
20
20
|
buildVeterinaryResearchStudyActiveConsentBatch,
|
|
21
|
+
buildVeterinaryResearchStudyCreateWorkflowIds,
|
|
21
22
|
buildVeterinaryResearchStudyGroupBatch,
|
|
22
23
|
buildVeterinaryResearchStudyGroupCreateEntry,
|
|
23
24
|
buildVeterinaryResearchStudyGroupMemberPatchEntries,
|
|
@@ -25,8 +26,13 @@ import {
|
|
|
25
26
|
buildVeterinaryResearchStudySmartAuthorization,
|
|
26
27
|
} from 'vet-sdk-core-ts/research-study'
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const workflowIds = buildVeterinaryResearchStudyCreateWorkflowIds({
|
|
30
|
+
operationId: crypto.randomUUID(),
|
|
31
|
+
practitionerRoleReferences: associatedParties.map(({ party }) => party.reference),
|
|
32
|
+
})
|
|
33
|
+
const researchGroupId = workflowIds.groupId
|
|
29
34
|
const groupCreate = buildVeterinaryResearchStudyGroupCreateEntry({
|
|
35
|
+
researchStudyId: workflowIds.researchStudyId,
|
|
30
36
|
group: {
|
|
31
37
|
resourceType: 'Group',
|
|
32
38
|
id: researchGroupId,
|
|
@@ -43,13 +49,13 @@ const groupBatch = buildVeterinaryResearchStudyGroupBatch([groupCreate, ...membe
|
|
|
43
49
|
// The protected controller BFF submits `groupBatch`; membership grants no access.
|
|
44
50
|
|
|
45
51
|
const invitations = buildVeterinaryResearchStudyPartyInvitations({
|
|
46
|
-
researchStudyId:
|
|
52
|
+
researchStudyId: workflowIds.researchStudyId,
|
|
47
53
|
controllerReference: organizationReference,
|
|
48
54
|
sentAt: new Date().toISOString(),
|
|
49
55
|
invitations: associatedParties.map((associatedParty, index) => ({
|
|
50
56
|
associatedParty,
|
|
51
|
-
communicationId:
|
|
52
|
-
consentId:
|
|
57
|
+
communicationId: workflowIds.invitations[index].communicationId,
|
|
58
|
+
consentId: workflowIds.invitations[index].consentId,
|
|
53
59
|
})),
|
|
54
60
|
})
|
|
55
61
|
|
|
@@ -63,7 +69,7 @@ for (const communication of invitations) {
|
|
|
63
69
|
// is separate from the draft inside Communication and is the actual grant.
|
|
64
70
|
const activeConsentBatch = buildVeterinaryResearchStudyActiveConsentBatch([{
|
|
65
71
|
consentId: activeConsentId,
|
|
66
|
-
researchStudyId:
|
|
72
|
+
researchStudyId: workflowIds.researchStudyId,
|
|
67
73
|
practitionerRoleId: protectedProfessional.practitionerRoleId,
|
|
68
74
|
actorRole: protectedProfessional.iscoRole,
|
|
69
75
|
date: new Date().toISOString().slice(0, 10),
|
|
@@ -74,13 +80,20 @@ const activeConsentBatch = buildVeterinaryResearchStudyActiveConsentBatch([{
|
|
|
74
80
|
// This succeeds only after protected state proves both prerequisites. Browser
|
|
75
81
|
// input must not be trusted as that proof.
|
|
76
82
|
const authorization = buildVeterinaryResearchStudySmartAuthorization({
|
|
77
|
-
researchStudyId:
|
|
83
|
+
researchStudyId: workflowIds.researchStudyId,
|
|
78
84
|
dcrCompleted: protectedProfessional.dcrCompleted,
|
|
79
85
|
consentStatus: protectedStudyConsent.status,
|
|
80
86
|
})
|
|
81
87
|
await professionalRuntime.requestSmartToken(authorization)
|
|
82
88
|
```
|
|
83
89
|
|
|
90
|
+
Persist the client-generated `operationId` with the UI operation state. A
|
|
91
|
+
retry passes the same UUID and the same set of exact PractitionerRole
|
|
92
|
+
references, producing the same ResearchStudy, Group, Communication and draft
|
|
93
|
+
Consent identifiers. This enables an idempotent, resumable saga after a
|
|
94
|
+
partial failure. It does not create a transaction across services, roll back
|
|
95
|
+
completed steps, or authorize any professional.
|
|
96
|
+
|
|
84
97
|
The controller may independently create a descriptive review team with
|
|
85
98
|
`buildVeterinaryResearchStudyGroupCreateEntry()` and append each registered
|
|
86
99
|
`PractitionerRole` with
|
|
@@ -96,11 +109,13 @@ Consent contain only `resourceType`, `id` and `resource.meta.claims`. Their
|
|
|
96
109
|
FHIR-like field names are flat claims. Native nested FHIR JSON is reserved for
|
|
97
110
|
an explicit export/projection boundary, not these GW builders.
|
|
98
111
|
|
|
99
|
-
The draft Consent inside the Communication attachment is an explicit
|
|
100
|
-
`application/vnd.api+json` proposal for recipient review.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
The draft Consent inside the Communication attachment is an explicit JSON:API
|
|
113
|
+
1.x `application/vnd.api+json` proposal for recipient review. Its `data[]`
|
|
114
|
+
resource object contains only `type`, `id` and `attributes`; those attributes
|
|
115
|
+
are the flat Consent claims. It has no internal `resource`, `request`, FHIR
|
|
116
|
+
Bundle or Bundle entry. The outer Communication remains claims-only. The draft
|
|
117
|
+
never authorizes access; only the later flat-claims active Consent POST is
|
|
118
|
+
evaluated by GW.
|
|
104
119
|
|
|
105
120
|
These builders follow the native [FHIR R5 Group resource](https://hl7.org/fhir/R5/group.html)
|
|
106
121
|
and its exact [resource-specific search catalogue](https://hl7.org/fhir/R5/group-search.html).
|
|
@@ -124,6 +139,14 @@ Consent claims use:
|
|
|
124
139
|
- `Consent.source-reference` for the exact `ResearchStudy/{id}`;
|
|
125
140
|
- `Consent.action` for `create`, `read`, `update`, and `search`.
|
|
126
141
|
|
|
142
|
+
The Group POST adds a governed business `Group.identifier` derived from the
|
|
143
|
+
ResearchStudy UUID. Use
|
|
144
|
+
`buildVeterinaryResearchStudyReviewTeamGroupSearch(workflowIds.researchStudyId)` to recover the
|
|
145
|
+
same descriptive review-team Group after a refresh. The helper deterministically
|
|
146
|
+
re-derives a UUIDv5 using the SDK-exported, governed namespace and emits
|
|
147
|
+
`urn:ietf:rfc:3986|urn:uuid:<uuidv5>`. This one-way correlation is a standard
|
|
148
|
+
Group identifier token, not eligibility, Consent, permission or SMART authority.
|
|
149
|
+
|
|
127
150
|
FHIR R5 references:
|
|
128
151
|
|
|
129
152
|
- [ResearchStudy](https://hl7.org/fhir/R5/researchstudy.html)
|
package/package.json
CHANGED