vet-sdk-core-ts 0.4.14 → 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 +7 -1
- package/dist/research-study.d.ts +30 -12
- package/dist/research-study.js +97 -23
- package/docs/101-RESEARCH-STUDY-INVITATIONS.md +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ cannot be broadened to delete.
|
|
|
37
37
|
`buildVeterinaryResearchStudyPartyInvitations` creates exactly one claims-only
|
|
38
38
|
`Communication` per associated `PractitionerRole`. Its canonical attachment
|
|
39
39
|
claims carry an `application/vnd.api+json` primary document containing one
|
|
40
|
-
|
|
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
|
|
41
43
|
and `Consent.resource-type` pins `ResearchSubject`. The draft requests only
|
|
42
44
|
create, read, update and search; it contains no delete permission and never
|
|
43
45
|
authorizes SMART access. Native FHIR Communication, Consent or Bundle objects
|
|
@@ -53,6 +55,10 @@ through separate `ResearchStudy.associatedParty` PATCH entries. It must not use
|
|
|
53
55
|
`ResearchStudy.recruitment.eligibility`, which represents research-subject
|
|
54
56
|
cohorts or eligibility criteria. Group grants no authorization, permission or
|
|
55
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
|
@@ -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,6 +65,19 @@ 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
|
+
}>;
|
|
77
81
|
export type VeterinaryResearchStudySmartAuthorization = Readonly<{
|
|
78
82
|
purpose: typeof HealthcareConsentPurposes.Research;
|
|
79
83
|
researchStudyReference: string;
|
|
@@ -145,8 +149,22 @@ export declare function buildVeterinaryResearchStudyBatch(entries: readonly Vete
|
|
|
145
149
|
* @see https://hl7.org/fhir/R5/group-search.html
|
|
146
150
|
*/
|
|
147
151
|
export declare function buildVeterinaryResearchStudyGroupCreateEntry(input: Readonly<{
|
|
152
|
+
researchStudyId: string;
|
|
148
153
|
group: GroupR5ResearchTeam;
|
|
149
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
|
+
}>;
|
|
160
|
+
/**
|
|
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.
|
|
164
|
+
*
|
|
165
|
+
* @see https://hl7.org/fhir/R5/group-search.html
|
|
166
|
+
*/
|
|
167
|
+
export declare function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId: string): VeterinaryResearchStudyReviewTeamGroupSearch;
|
|
150
168
|
/**
|
|
151
169
|
* Builds one independently correlated PATCH per PractitionerRole member. A
|
|
152
170
|
* 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,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)}`;
|
|
@@ -122,12 +133,32 @@ export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
|
|
|
122
133
|
const group = normalizedResearchTeamGroup(input.group);
|
|
123
134
|
if (group.member.length > 0)
|
|
124
135
|
throw new TypeError('group_members_require_patch_entries');
|
|
136
|
+
const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(input.researchStudyId);
|
|
125
137
|
return deepFreeze({
|
|
126
138
|
type: 'Group-v5.0.0',
|
|
127
|
-
resource: projectGroupR5FlatClaimsResource(group),
|
|
139
|
+
resource: projectGroupR5FlatClaimsResource({ ...group, identifier: [identifier] }),
|
|
128
140
|
request: { method: 'POST', url: 'Group' },
|
|
129
141
|
});
|
|
130
142
|
}
|
|
143
|
+
/** Builds the governed business Identifier; it describes correlation, never authority. */
|
|
144
|
+
export function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId) {
|
|
145
|
+
const studyUuid = boundedStudyUuid(researchStudyId);
|
|
146
|
+
return deepFreeze({
|
|
147
|
+
system: VeterinaryResearchStudyReviewTeamIdentifierSystem,
|
|
148
|
+
value: `urn:uuid:${uuidV5(studyUuid, VeterinaryResearchStudyReviewTeamIdentifierNamespace)}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
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
|
+
*
|
|
156
|
+
* @see https://hl7.org/fhir/R5/group-search.html
|
|
157
|
+
*/
|
|
158
|
+
export function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId) {
|
|
159
|
+
const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId);
|
|
160
|
+
return deepFreeze({ identifier: `${identifier.system}|${identifier.value}` });
|
|
161
|
+
}
|
|
131
162
|
/**
|
|
132
163
|
* Builds one independently correlated PATCH per PractitionerRole member. A
|
|
133
164
|
* PATCH changes descriptive membership only; DCR, Consent and SMART remain
|
|
@@ -212,28 +243,22 @@ export function buildVeterinaryResearchStudyPartyInvitation(input) {
|
|
|
212
243
|
if (!topic)
|
|
213
244
|
throw new TypeError('veterinary_research_study_invitation_topic_unavailable');
|
|
214
245
|
const consent = {
|
|
215
|
-
|
|
246
|
+
type: 'Consent-v5.0.0',
|
|
216
247
|
id: consentId,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
[ClaimConsent.sourceReference]: researchStudyReference,
|
|
228
|
-
},
|
|
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,
|
|
229
258
|
},
|
|
230
259
|
};
|
|
231
260
|
const document = {
|
|
232
|
-
data: [
|
|
233
|
-
type: 'Consent-v5.0.0',
|
|
234
|
-
resource: consent,
|
|
235
|
-
request: { method: 'POST', url: 'Consent' },
|
|
236
|
-
}],
|
|
261
|
+
data: [consent],
|
|
237
262
|
};
|
|
238
263
|
const category = VeterinaryCommunicationPresetFilters.ResearchAgreements.categories[0];
|
|
239
264
|
const sent = optionalInstant(input.sentAt);
|
|
@@ -292,10 +317,10 @@ export function decodeVeterinaryResearchStudyInvitationDocument(communication) {
|
|
|
292
317
|
const entry = decoded?.data?.[0];
|
|
293
318
|
if (decoded?.data?.length !== 1
|
|
294
319
|
|| entry?.type !== 'Consent-v5.0.0'
|
|
295
|
-
|| entry.
|
|
296
|
-
|| entry.
|
|
297
|
-
|| !
|
|
298
|
-
|| entry.
|
|
320
|
+
|| Object.keys(entry).some(key => !['type', 'id', 'attributes'].includes(key))
|
|
321
|
+
|| !boundedJsonApiId(entry.id)
|
|
322
|
+
|| !isFlatStringAttributes(entry.attributes)
|
|
323
|
+
|| !isValidInvitationDraftConsentAttributes(entry.attributes, claims)) {
|
|
299
324
|
throw new TypeError('veterinary_research_study_invitation_document_invalid');
|
|
300
325
|
}
|
|
301
326
|
return deepFreeze(decoded);
|
|
@@ -385,6 +410,34 @@ function boundedId(value) {
|
|
|
385
410
|
throw new TypeError('veterinary_research_study_id_invalid');
|
|
386
411
|
return id;
|
|
387
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
|
+
}
|
|
388
441
|
function boundedGroupId(value) {
|
|
389
442
|
const id = String(value || '').trim();
|
|
390
443
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
@@ -450,3 +503,24 @@ function isClaimsOnlyResource(value, resourceType) {
|
|
|
450
503
|
return false;
|
|
451
504
|
return Object.values(value.meta.claims).every(claim => typeof claim === 'string' && claim.trim().length > 0);
|
|
452
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
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
const researchGroupId = 'review-team'
|
|
29
29
|
const groupCreate = buildVeterinaryResearchStudyGroupCreateEntry({
|
|
30
|
+
researchStudyId: study.id,
|
|
30
31
|
group: {
|
|
31
32
|
resourceType: 'Group',
|
|
32
33
|
id: researchGroupId,
|
|
@@ -96,11 +97,13 @@ Consent contain only `resourceType`, `id` and `resource.meta.claims`. Their
|
|
|
96
97
|
FHIR-like field names are flat claims. Native nested FHIR JSON is reserved for
|
|
97
98
|
an explicit export/projection boundary, not these GW builders.
|
|
98
99
|
|
|
99
|
-
The draft Consent inside the Communication attachment is an explicit
|
|
100
|
-
`application/vnd.api+json` proposal for recipient review.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
104
107
|
|
|
105
108
|
These builders follow the native [FHIR R5 Group resource](https://hl7.org/fhir/R5/group.html)
|
|
106
109
|
and its exact [resource-specific search catalogue](https://hl7.org/fhir/R5/group-search.html).
|
|
@@ -124,6 +127,14 @@ Consent claims use:
|
|
|
124
127
|
- `Consent.source-reference` for the exact `ResearchStudy/{id}`;
|
|
125
128
|
- `Consent.action` for `create`, `read`, `update`, and `search`.
|
|
126
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.
|
|
137
|
+
|
|
127
138
|
FHIR R5 references:
|
|
128
139
|
|
|
129
140
|
- [ResearchStudy](https://hl7.org/fhir/R5/researchstudy.html)
|
package/package.json
CHANGED