vet-data-utils-ts 0.5.26 → 0.5.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,19 @@ cannot be attributed to the wrong party. A party classifier describes an
119
119
  organization category; it never represents Consent or CRUDS authorization. See
120
120
  [`docs/research-study-r5-contract.md`](docs/research-study-r5-contract.md).
121
121
 
122
+ Explicit study provenance uses a separate claims-first FHIR R5 `Provenance`
123
+ resource from `vet-data-utils-ts/provenance`. It exposes all thirteen
124
+ resource-specific standard SearchParameters and preserves agent correlation as
125
+ `Provenance.agent-<participation-type>-who`, where `<participation-type>` is an
126
+ exact code from the FHIR ParticipationRoleType ValueSet and the value is the
127
+ agent's FHIR Reference or URI. The standard `Provenance.agent` and
128
+ `Provenance.agent-type` claims are indexed alongside that correlation claim.
129
+ For a professional, use
130
+ `buildPractitionerRoleReferenceFromAssignmentIdentifier()` with the real
131
+ Occupation/PractitionerRole assignment UUID returned by GW. The role-bearing
132
+ employee URN and role-license hash remain operational authorization identities;
133
+ they are not substituted for the clinical `PractitionerRole` reference.
134
+
122
135
  ResearchStudy professional teams use native FHIR R5 Group resources from
123
136
  `vet-data-utils-ts/group`: required type and membership codes, the exact twelve
124
137
  resource-specific SearchParameters, canonical flat claim projection and
@@ -221,6 +234,8 @@ controls whether external catalogue searches may return it. Archiving that
221
234
  record withdraws discovery without deleting the study, eligibility/team
222
235
  Groups, ResearchSubjects or Dataset, and without changing `dct:accessRights`.
223
236
  No individual ResearchSubject identifier is projected into HealthDCAT.
237
+ The Dataset `dct:provenance` points to the separate FHIR `Provenance` record;
238
+ it does not replace the `prov:wasGeneratedBy` study relationship.
224
239
  `Distribution` describes the HDAB-controlled representation; `DataService`
225
240
  describes the host API serving Dataset metadata.
226
241
 
@@ -149,6 +149,7 @@ export declare function buildHealthDcatDataService(input: ControllerAuthority &
149
149
  export declare function buildResearchStudyDatasetPublication(input: ControllerAuthority & Readonly<{
150
150
  dataset: unknown;
151
151
  researchStudyIdentifier: string;
152
+ provenanceReference: string;
152
153
  catalogRecordIdentifier: string;
153
154
  status: HealthDcatCatalogRecordStatus;
154
155
  modifiedAt: string;
@@ -303,12 +303,19 @@ export function buildResearchStudyDatasetPublication(input) {
303
303
  const researchStudyIdentifier = required(input.researchStudyIdentifier, 'researchStudyIdentifier');
304
304
  if (!researchStudyIdentifier.startsWith('ResearchStudy/'))
305
305
  throw new TypeError('research_study_reference_invalid');
306
+ const provenanceReference = required(input.provenanceReference, 'provenanceReference');
307
+ if (!provenanceReference.startsWith('Provenance/'))
308
+ throw new TypeError('provenance_reference_invalid');
306
309
  const modifiedAt = new Date(input.modifiedAt);
307
310
  if (!Number.isFinite(modifiedAt.getTime()) || modifiedAt.toISOString() !== input.modifiedAt)
308
311
  throw new TypeError('modifiedAt must be an ISO instant');
309
312
  const linkedDataset = normalizeHealthDcatResource({
310
313
  ...dataset,
311
- meta: { claims: { ...dataset.meta.claims, [ClaimsHealthDcatDataset.wasGeneratedBy]: researchStudyIdentifier } },
314
+ meta: { claims: {
315
+ ...dataset.meta.claims,
316
+ [ClaimsHealthDcatDataset.wasGeneratedBy]: researchStudyIdentifier,
317
+ [ClaimsHealthDcatDataset.provenance]: provenanceReference,
318
+ } },
312
319
  });
313
320
  const recordId = required(input.catalogRecordIdentifier, 'catalogRecordIdentifier');
314
321
  const record = normalizeHealthDcatResource({
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from './iso-jurisdictions.js';
15
15
  export * from './organization-application.js';
16
16
  export * from './payment.js';
17
17
  export * from './place-service-directory.js';
18
+ export * from './provenance.js';
18
19
  export * from './research-study.js';
19
20
  export * from './sectors.js';
20
21
  export * from './shc.js';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from './iso-jurisdictions.js';
15
15
  export * from './organization-application.js';
16
16
  export * from './payment.js';
17
17
  export * from './place-service-directory.js';
18
+ export * from './provenance.js';
18
19
  export * from './research-study.js';
19
20
  export * from './sectors.js';
20
21
  export * from './shc.js';
@@ -0,0 +1,42 @@
1
+ export declare const FhirR5ParticipationRoleTypeValueSet: "http://hl7.org/fhir/ValueSet/participation-role-type";
2
+ export declare const FhirR5ProvenanceParticipantTypeSystem: "http://terminology.hl7.org/CodeSystem/provenance-participant-type";
3
+ export declare const FhirR5ExtraSecurityRoleTypeSystem: "http://terminology.hl7.org/CodeSystem/extra-security-role-type";
4
+ export declare const DicomControlledTerminologySystem: "http://dicom.nema.org/resources/ontology/DCM";
5
+ /** Exact FHIR R5 ParticipationRoleType 5.0.0 expansion used by Provenance.agent.type. */
6
+ export declare const FhirR5ParticipationRoleTypeCodes: readonly ["enterer", "performer", "author", "verifier", "legal", "attester", "informant", "custodian", "assembler", "composer", "110150", "110151", "110152", "110153", "110154", "110155", "authserver", "datacollector", "dataprocessor", "datasubject", "humanuser"];
7
+ export type FhirR5ParticipationRoleTypeCode = (typeof FhirR5ParticipationRoleTypeCodes)[number];
8
+ export type ProvenanceSearchParameterType = 'date' | 'reference' | 'token';
9
+ export type ProvenanceR5SearchParameter = Readonly<{
10
+ code: string;
11
+ type: ProvenanceSearchParameterType;
12
+ expression: string;
13
+ }>;
14
+ /** Exact resource-specific FHIR R5 Provenance SearchParameters. */
15
+ export declare const ProvenanceR5SearchParameterCatalog: readonly ProvenanceR5SearchParameter[];
16
+ /** Standard search claims plus typed-agent correlation claims. */
17
+ export declare const ProvenanceFlatClaimCatalog: readonly string[];
18
+ export type ProvenanceR5FlatClaimsResource = Readonly<{
19
+ resourceType: 'Provenance';
20
+ id: string;
21
+ meta: Readonly<{
22
+ claims: Readonly<Record<string, readonly string[]>>;
23
+ }>;
24
+ }>;
25
+ /** Builds the FHIR PractitionerRole reference from the server-issued assignment UUID. */
26
+ export declare function buildPractitionerRoleReferenceFromAssignmentIdentifier(assignmentIdentifier: string): string;
27
+ /** Returns the correlated agent reference claim for one official participation type. */
28
+ export declare function provenanceAgentClaim(code: FhirR5ParticipationRoleTypeCode | string): string;
29
+ /** Returns the system-qualified FHIR token for one official participation type. */
30
+ export declare function participationRoleTypeToken(code: FhirR5ParticipationRoleTypeCode): string;
31
+ /** Validates a claims-only Provenance resource and rejects nested/native fields. */
32
+ export declare function normalizeProvenanceR5FlatClaimsResource(candidate: unknown): ProvenanceR5FlatClaimsResource;
33
+ /** Builds provenance for creation or update of a ResearchStudy with correlated typed agents. */
34
+ export declare function buildResearchStudyProvenanceResource(input: Readonly<{
35
+ id: string;
36
+ researchStudyReference: string;
37
+ recorded: string;
38
+ agents: readonly Readonly<{
39
+ type: FhirR5ParticipationRoleTypeCode;
40
+ who: string;
41
+ }>[];
42
+ }>): ProvenanceR5FlatClaimsResource;
@@ -0,0 +1,108 @@
1
+ /** Browser-safe FHIR R5 Provenance flat-claim vocabulary and builders. */
2
+ import { normalizeUuid } from 'gdc-common-utils-ts/utils/normalize-uuid';
3
+ export const FhirR5ParticipationRoleTypeValueSet = 'http://hl7.org/fhir/ValueSet/participation-role-type';
4
+ export const FhirR5ProvenanceParticipantTypeSystem = 'http://terminology.hl7.org/CodeSystem/provenance-participant-type';
5
+ export const FhirR5ExtraSecurityRoleTypeSystem = 'http://terminology.hl7.org/CodeSystem/extra-security-role-type';
6
+ export const DicomControlledTerminologySystem = 'http://dicom.nema.org/resources/ontology/DCM';
7
+ /** Exact FHIR R5 ParticipationRoleType 5.0.0 expansion used by Provenance.agent.type. */
8
+ export const FhirR5ParticipationRoleTypeCodes = Object.freeze([
9
+ 'enterer', 'performer', 'author', 'verifier', 'legal', 'attester', 'informant',
10
+ 'custodian', 'assembler', 'composer', '110150', '110151', '110152', '110153',
11
+ '110154', '110155', 'authserver', 'datacollector', 'dataprocessor', 'datasubject',
12
+ 'humanuser',
13
+ ]);
14
+ /** Exact resource-specific FHIR R5 Provenance SearchParameters. */
15
+ export const ProvenanceR5SearchParameterCatalog = Object.freeze([
16
+ { code: 'activity', type: 'token', expression: 'Provenance.activity' },
17
+ { code: 'agent', type: 'reference', expression: 'Provenance.agent.who' },
18
+ { code: 'agent-role', type: 'token', expression: 'Provenance.agent.role' },
19
+ { code: 'agent-type', type: 'token', expression: 'Provenance.agent.type' },
20
+ { code: 'based-on', type: 'reference', expression: 'Provenance.basedOn' },
21
+ { code: 'encounter', type: 'reference', expression: 'Provenance.encounter' },
22
+ { code: 'entity', type: 'reference', expression: 'Provenance.entity.what' },
23
+ { code: 'location', type: 'reference', expression: 'Provenance.location' },
24
+ { code: 'patient', type: 'reference', expression: 'Provenance.patient' },
25
+ { code: 'recorded', type: 'date', expression: 'Provenance.recorded' },
26
+ { code: 'signature-type', type: 'token', expression: 'Provenance.signature.type' },
27
+ { code: 'target', type: 'reference', expression: 'Provenance.target' },
28
+ { code: 'when', type: 'date', expression: 'Provenance.occurred.ofType(dateTime)' },
29
+ ]);
30
+ const standardClaims = ProvenanceR5SearchParameterCatalog.map(parameter => `Provenance.${parameter.code}`);
31
+ const correlatedAgentClaims = FhirR5ParticipationRoleTypeCodes.map(code => `Provenance.agent-${code}-who`);
32
+ /** Standard search claims plus typed-agent correlation claims. */
33
+ export const ProvenanceFlatClaimCatalog = Object.freeze([...standardClaims, ...correlatedAgentClaims]);
34
+ /** Builds the FHIR PractitionerRole reference from the server-issued assignment UUID. */
35
+ export function buildPractitionerRoleReferenceFromAssignmentIdentifier(assignmentIdentifier) {
36
+ const compact = normalizeUuid(assignmentIdentifier);
37
+ if (!compact)
38
+ throw new TypeError('professional_assignment_identifier_invalid');
39
+ const uuid = `${compact.slice(0, 8)}-${compact.slice(8, 12)}-${compact.slice(12, 16)}-${compact.slice(16, 20)}-${compact.slice(20)}`;
40
+ return `PractitionerRole/${uuid}`;
41
+ }
42
+ /** Returns the correlated agent reference claim for one official participation type. */
43
+ export function provenanceAgentClaim(code) {
44
+ if (!FhirR5ParticipationRoleTypeCodes.includes(code))
45
+ throw new TypeError('participation_role_type_invalid');
46
+ return `Provenance.agent-${code}-who`;
47
+ }
48
+ /** Returns the system-qualified FHIR token for one official participation type. */
49
+ export function participationRoleTypeToken(code) {
50
+ const system = /^11015[0-5]$/.test(code)
51
+ ? DicomControlledTerminologySystem
52
+ : ['authserver', 'datacollector', 'dataprocessor', 'datasubject', 'humanuser'].includes(code)
53
+ ? FhirR5ExtraSecurityRoleTypeSystem
54
+ : FhirR5ProvenanceParticipantTypeSystem;
55
+ return `${system}|${code}`;
56
+ }
57
+ /** Validates a claims-only Provenance resource and rejects nested/native fields. */
58
+ export function normalizeProvenanceR5FlatClaimsResource(candidate) {
59
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
60
+ throw new TypeError('provenance_flat_resource_invalid');
61
+ const input = candidate;
62
+ if (input.resourceType !== 'Provenance' || typeof input.id !== 'string' || !input.id.trim())
63
+ throw new TypeError('provenance_flat_resource_invalid');
64
+ if (Object.keys(input).some(key => !['resourceType', 'id', 'meta'].includes(key)))
65
+ throw new TypeError('provenance_flat_resource_invalid');
66
+ const meta = input.meta;
67
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta) || Object.keys(meta).some(key => key !== 'claims'))
68
+ throw new TypeError('provenance_flat_resource_invalid');
69
+ const claims = meta.claims;
70
+ if (!claims || typeof claims !== 'object' || Array.isArray(claims))
71
+ throw new TypeError('provenance_flat_resource_invalid');
72
+ const normalized = {};
73
+ for (const [name, rawValue] of Object.entries(claims)) {
74
+ if (!ProvenanceFlatClaimCatalog.includes(name))
75
+ throw new TypeError('provenance_flat_claim_invalid');
76
+ const values = (Array.isArray(rawValue) ? rawValue : [rawValue]).map(value => String(value).trim()).filter(Boolean);
77
+ if (!values.length)
78
+ throw new TypeError('provenance_flat_claim_value_invalid');
79
+ normalized[name] = Object.freeze(values);
80
+ }
81
+ return Object.freeze({ resourceType: 'Provenance', id: input.id.trim(), meta: Object.freeze({ claims: Object.freeze(normalized) }) });
82
+ }
83
+ /** Builds provenance for creation or update of a ResearchStudy with correlated typed agents. */
84
+ export function buildResearchStudyProvenanceResource(input) {
85
+ if (!input.researchStudyReference.startsWith('ResearchStudy/'))
86
+ throw new TypeError('research_study_reference_invalid');
87
+ const recorded = new Date(input.recorded);
88
+ if (!Number.isFinite(recorded.getTime()) || recorded.toISOString() !== input.recorded)
89
+ throw new TypeError('recorded_instant_invalid');
90
+ if (!input.agents.length)
91
+ throw new TypeError('provenance_agent_required');
92
+ const claims = {
93
+ 'Provenance.target': [input.researchStudyReference],
94
+ 'Provenance.recorded': [input.recorded],
95
+ 'Provenance.agent': [],
96
+ 'Provenance.agent-type': [],
97
+ };
98
+ for (const agent of input.agents) {
99
+ const who = String(agent.who || '').trim();
100
+ if (!who)
101
+ throw new TypeError('provenance_agent_who_required');
102
+ claims['Provenance.agent'].push(who);
103
+ claims['Provenance.agent-type'].push(participationRoleTypeToken(agent.type));
104
+ const correlatedClaim = provenanceAgentClaim(agent.type);
105
+ (claims[correlatedClaim] ||= []).push(who);
106
+ }
107
+ return normalizeProvenanceR5FlatClaimsResource({ resourceType: 'Provenance', id: input.id, meta: { claims } });
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.5.26",
3
+ "version": "0.5.28",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -72,6 +72,10 @@
72
72
  "types": "./dist/payment.d.ts",
73
73
  "default": "./dist/payment.js"
74
74
  },
75
+ "./provenance": {
76
+ "types": "./dist/provenance.d.ts",
77
+ "default": "./dist/provenance.js"
78
+ },
75
79
  "./place-service-directory": {
76
80
  "types": "./dist/place-service-directory.d.ts",
77
81
  "default": "./dist/place-service-directory.js"