vet-data-utils-ts 0.5.25 → 0.5.27

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,14 @@ 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
+
122
130
  ResearchStudy professional teams use native FHIR R5 Group resources from
123
131
  `vet-data-utils-ts/group`: required type and membership codes, the exact twelve
124
132
  resource-specific SearchParameters, canonical flat claim projection and
@@ -215,6 +223,17 @@ authorization are separate checks performed by their owning adapters.
215
223
  `projectHealthDcatResourceToJsonLd()` is the explicit standards serialization
216
224
  boundary used by host/dataspace adapters.
217
225
 
226
+ Research publication is study-scoped. A derived cohort Dataset links to its
227
+ generating `ResearchStudy` through PROV-O and a separate `CatalogRecord`
228
+ controls whether external catalogue searches may return it. Archiving that
229
+ record withdraws discovery without deleting the study, eligibility/team
230
+ Groups, ResearchSubjects or Dataset, and without changing `dct:accessRights`.
231
+ No individual ResearchSubject identifier is projected into HealthDCAT.
232
+ The Dataset `dct:provenance` points to the separate FHIR `Provenance` record;
233
+ it does not replace the `prov:wasGeneratedBy` study relationship.
234
+ `Distribution` describes the HDAB-controlled representation; `DataService`
235
+ describes the host API serving Dataset metadata.
236
+
218
237
  `vet-data-utils-ts/organization-application` is the product-neutral application
219
238
  value used by UHC UNID, VetChain and SOSChain adapters. The host supplies the
220
239
  allowed sector list and translated labels. The value contains one official
@@ -19,6 +19,7 @@ export declare enum ClaimsHealthDcatDataset {
19
19
  distribution = "dcat.distribution",
20
20
  source = "dct.source",
21
21
  provenance = "dct.provenance",
22
+ wasGeneratedBy = "prov.wasGeneratedBy",
22
23
  personalData = "dpv.hasPersonalData",
23
24
  healthCategory = "healthdcatap.healthCategory",
24
25
  healthDataAccessBody = "healthdcatap.hdab",
@@ -38,13 +39,47 @@ export declare enum ClaimsHealthDcatCatalog {
38
39
  dataset = "dcat.dataset",
39
40
  userSelected = "Catalog.user-selected"
40
41
  }
42
+ /** DCAT Catalogue Record editorial claims; status is distinct from data access rights. */
43
+ export declare enum ClaimsHealthDcatCatalogRecord {
44
+ identifier = "dct.identifier",
45
+ primaryTopic = "foaf.primaryTopic",
46
+ status = "adms.status",
47
+ modified = "dct.modified",
48
+ conformsTo = "dct.conformsTo",
49
+ userSelected = "CatalogRecord.user-selected"
50
+ }
51
+ /** DCAT Distribution claims used for downloadable or controlled-access representations. */
52
+ export declare enum ClaimsHealthDcatDistribution {
53
+ identifier = "dct.identifier",
54
+ title = "dct.title",
55
+ accessUrl = "dcat.accessURL",
56
+ format = "dct.format",
57
+ mediaType = "dcat.mediaType",
58
+ accessRights = "dct.accessRights",
59
+ policy = "odrl.hasPolicy",
60
+ userSelected = "Distribution.user-selected"
61
+ }
62
+ /** DCAT DataService claims used for host APIs serving one or more Datasets. */
63
+ export declare enum ClaimsHealthDcatDataService {
64
+ identifier = "dct.identifier",
65
+ title = "dct.title",
66
+ publisher = "dct.publisher",
67
+ endpointUrl = "dcat.endpointURL",
68
+ servesDataset = "dcat.servesDataset",
69
+ accessRights = "dct.accessRights",
70
+ userSelected = "DataService.user-selected"
71
+ }
72
+ export type HealthDcatCatalogRecordStatus = 'draft' | 'published' | 'archived';
41
73
  /** Complete allowlist for product-persisted HealthDCAT flat claims. */
42
74
  export declare const HealthDcatFlatClaimCatalog: Readonly<{
43
75
  readonly Dataset: readonly ClaimsHealthDcatDataset[];
44
76
  readonly Catalog: readonly ClaimsHealthDcatCatalog[];
77
+ readonly CatalogRecord: readonly ClaimsHealthDcatCatalogRecord[];
78
+ readonly Distribution: readonly ClaimsHealthDcatDistribution[];
79
+ readonly DataService: readonly ClaimsHealthDcatDataService[];
45
80
  }>;
46
81
  export type HealthDcatResource = Readonly<{
47
- resourceType: 'Dataset' | 'Catalog';
82
+ resourceType: 'Dataset' | 'Catalog' | 'CatalogRecord' | 'Distribution' | 'DataService';
48
83
  id: string;
49
84
  meta: Readonly<{
50
85
  claims: Readonly<Record<string, ClaimValue>>;
@@ -87,6 +122,49 @@ export declare function buildHealthDcatCatalog(input: ControllerAuthority & Read
87
122
  datasetIdentifiers: readonly string[];
88
123
  applicableLegislation: readonly string[];
89
124
  }>): HealthDcatResource;
125
+ /** Builds a governed Dataset Distribution without exposing the source records. */
126
+ export declare function buildHealthDcatDistribution(input: ControllerAuthority & Readonly<{
127
+ id: string;
128
+ title: string;
129
+ accessUrl: string;
130
+ format: string;
131
+ mediaType?: string;
132
+ accessRights: HealthDcatAccessRights;
133
+ healthDataAccessBodyIdentifier: string;
134
+ policyIdentifier?: string;
135
+ }>): HealthDcatResource;
136
+ /** Builds metadata for a host API that serves published Dataset descriptions. */
137
+ export declare function buildHealthDcatDataService(input: ControllerAuthority & Readonly<{
138
+ id: string;
139
+ title: string;
140
+ endpointUrl: string;
141
+ servesDatasetIdentifiers: readonly string[];
142
+ accessRights: HealthDcatAccessRights;
143
+ }>): HealthDcatResource;
144
+ /**
145
+ * Links one cohort Dataset to the ResearchStudy that generated it and creates
146
+ * the separate editorial CatalogRecord controlling external discoverability.
147
+ * ResearchSubject identifiers are deliberately absent from this projection.
148
+ */
149
+ export declare function buildResearchStudyDatasetPublication(input: ControllerAuthority & Readonly<{
150
+ dataset: unknown;
151
+ researchStudyIdentifier: string;
152
+ provenanceReference: string;
153
+ catalogRecordIdentifier: string;
154
+ status: HealthDcatCatalogRecordStatus;
155
+ modifiedAt: string;
156
+ }>): Readonly<{
157
+ dataset: HealthDcatResource;
158
+ record: HealthDcatResource;
159
+ }>;
160
+ /** Controller-only editorial transition; archiving removes external discovery only. */
161
+ export declare function updateHealthDcatCatalogRecordStatus(input: ControllerAuthority & Readonly<{
162
+ record: unknown;
163
+ status: HealthDcatCatalogRecordStatus;
164
+ modifiedAt: string;
165
+ }>): HealthDcatResource;
166
+ /** True only for records eligible for an external marketplace search result. */
167
+ export declare function isHealthDcatCatalogRecordExternallyDiscoverable(record: unknown): boolean;
90
168
  /** Creates an auditable tenant authorization for host-provided catalogue federation. */
91
169
  export declare function buildHealthDcatFederationRequest(input: ControllerAuthority & Readonly<{
92
170
  catalogIdentifier: string;
@@ -21,6 +21,7 @@ export var ClaimsHealthDcatDataset;
21
21
  ClaimsHealthDcatDataset["distribution"] = "dcat.distribution";
22
22
  ClaimsHealthDcatDataset["source"] = "dct.source";
23
23
  ClaimsHealthDcatDataset["provenance"] = "dct.provenance";
24
+ ClaimsHealthDcatDataset["wasGeneratedBy"] = "prov.wasGeneratedBy";
24
25
  ClaimsHealthDcatDataset["personalData"] = "dpv.hasPersonalData";
25
26
  ClaimsHealthDcatDataset["healthCategory"] = "healthdcatap.healthCategory";
26
27
  ClaimsHealthDcatDataset["healthDataAccessBody"] = "healthdcatap.hdab";
@@ -41,17 +42,53 @@ export var ClaimsHealthDcatCatalog;
41
42
  ClaimsHealthDcatCatalog["dataset"] = "dcat.dataset";
42
43
  ClaimsHealthDcatCatalog["userSelected"] = "Catalog.user-selected";
43
44
  })(ClaimsHealthDcatCatalog || (ClaimsHealthDcatCatalog = {}));
45
+ /** DCAT Catalogue Record editorial claims; status is distinct from data access rights. */
46
+ export var ClaimsHealthDcatCatalogRecord;
47
+ (function (ClaimsHealthDcatCatalogRecord) {
48
+ ClaimsHealthDcatCatalogRecord["identifier"] = "dct.identifier";
49
+ ClaimsHealthDcatCatalogRecord["primaryTopic"] = "foaf.primaryTopic";
50
+ ClaimsHealthDcatCatalogRecord["status"] = "adms.status";
51
+ ClaimsHealthDcatCatalogRecord["modified"] = "dct.modified";
52
+ ClaimsHealthDcatCatalogRecord["conformsTo"] = "dct.conformsTo";
53
+ ClaimsHealthDcatCatalogRecord["userSelected"] = "CatalogRecord.user-selected";
54
+ })(ClaimsHealthDcatCatalogRecord || (ClaimsHealthDcatCatalogRecord = {}));
55
+ /** DCAT Distribution claims used for downloadable or controlled-access representations. */
56
+ export var ClaimsHealthDcatDistribution;
57
+ (function (ClaimsHealthDcatDistribution) {
58
+ ClaimsHealthDcatDistribution["identifier"] = "dct.identifier";
59
+ ClaimsHealthDcatDistribution["title"] = "dct.title";
60
+ ClaimsHealthDcatDistribution["accessUrl"] = "dcat.accessURL";
61
+ ClaimsHealthDcatDistribution["format"] = "dct.format";
62
+ ClaimsHealthDcatDistribution["mediaType"] = "dcat.mediaType";
63
+ ClaimsHealthDcatDistribution["accessRights"] = "dct.accessRights";
64
+ ClaimsHealthDcatDistribution["policy"] = "odrl.hasPolicy";
65
+ ClaimsHealthDcatDistribution["userSelected"] = "Distribution.user-selected";
66
+ })(ClaimsHealthDcatDistribution || (ClaimsHealthDcatDistribution = {}));
67
+ /** DCAT DataService claims used for host APIs serving one or more Datasets. */
68
+ export var ClaimsHealthDcatDataService;
69
+ (function (ClaimsHealthDcatDataService) {
70
+ ClaimsHealthDcatDataService["identifier"] = "dct.identifier";
71
+ ClaimsHealthDcatDataService["title"] = "dct.title";
72
+ ClaimsHealthDcatDataService["publisher"] = "dct.publisher";
73
+ ClaimsHealthDcatDataService["endpointUrl"] = "dcat.endpointURL";
74
+ ClaimsHealthDcatDataService["servesDataset"] = "dcat.servesDataset";
75
+ ClaimsHealthDcatDataService["accessRights"] = "dct.accessRights";
76
+ ClaimsHealthDcatDataService["userSelected"] = "DataService.user-selected";
77
+ })(ClaimsHealthDcatDataService || (ClaimsHealthDcatDataService = {}));
44
78
  /** Complete allowlist for product-persisted HealthDCAT flat claims. */
45
79
  export const HealthDcatFlatClaimCatalog = Object.freeze({
46
80
  Dataset: Object.freeze(Object.values(ClaimsHealthDcatDataset)),
47
81
  Catalog: Object.freeze(Object.values(ClaimsHealthDcatCatalog)),
82
+ CatalogRecord: Object.freeze(Object.values(ClaimsHealthDcatCatalogRecord)),
83
+ Distribution: Object.freeze(Object.values(ClaimsHealthDcatDistribution)),
84
+ DataService: Object.freeze(Object.values(ClaimsHealthDcatDataService)),
48
85
  });
49
86
  /** Validates an indexed HealthDCAT resource and rejects nested or unknown claims. */
50
87
  export function normalizeHealthDcatResource(candidate) {
51
88
  if (!candidate || typeof candidate !== 'object')
52
89
  throw new TypeError('health_dcat_resource_invalid');
53
90
  const value = candidate;
54
- if (value.resourceType !== 'Dataset' && value.resourceType !== 'Catalog')
91
+ if (!['Dataset', 'Catalog', 'CatalogRecord', 'Distribution', 'DataService'].includes(String(value.resourceType)))
55
92
  throw new TypeError('health_dcat_resource_invalid');
56
93
  if (typeof value.id !== 'string' || !value.id.trim())
57
94
  throw new TypeError('health_dcat_resource_invalid');
@@ -60,7 +97,8 @@ export function normalizeHealthDcatResource(candidate) {
60
97
  const claims = value.meta.claims;
61
98
  if (!claims || typeof claims !== 'object' || Array.isArray(claims))
62
99
  throw new TypeError('health_dcat_resource_invalid');
63
- const allowed = HealthDcatFlatClaimCatalog[value.resourceType];
100
+ const resourceType = value.resourceType;
101
+ const allowed = HealthDcatFlatClaimCatalog[resourceType];
64
102
  const normalized = {};
65
103
  for (const [claim, raw] of Object.entries(claims)) {
66
104
  if (!allowed.includes(claim))
@@ -73,7 +111,7 @@ export function normalizeHealthDcatResource(candidate) {
73
111
  throw new TypeError(`health_dcat_claim_value_invalid:${claim}`);
74
112
  }
75
113
  return Object.freeze({
76
- resourceType: value.resourceType,
114
+ resourceType,
77
115
  id: value.id,
78
116
  meta: Object.freeze({ claims: Object.freeze(normalized) }),
79
117
  });
@@ -90,6 +128,7 @@ const JsonLdClaimNames = Object.freeze({
90
128
  [ClaimsHealthDcatDataset.distribution]: 'dcat:distribution',
91
129
  [ClaimsHealthDcatDataset.source]: 'dct:source',
92
130
  [ClaimsHealthDcatDataset.provenance]: 'dct:provenance',
131
+ [ClaimsHealthDcatDataset.wasGeneratedBy]: 'prov:wasGeneratedBy',
93
132
  [ClaimsHealthDcatDataset.personalData]: 'dpv:hasPersonalData',
94
133
  [ClaimsHealthDcatDataset.healthCategory]: 'healthdcatap:healthCategory',
95
134
  [ClaimsHealthDcatDataset.healthDataAccessBody]: 'healthdcatap:hdab',
@@ -97,6 +136,16 @@ const JsonLdClaimNames = Object.freeze({
97
136
  [ClaimsHealthDcatDataset.variables]: 'healthdcatap:hasVariables',
98
137
  [ClaimsHealthDcatDataset.authorizationReference]: 'prov:qualifiedAttribution',
99
138
  [ClaimsHealthDcatCatalog.dataset]: 'dcat:dataset',
139
+ [ClaimsHealthDcatCatalogRecord.primaryTopic]: 'foaf:primaryTopic',
140
+ [ClaimsHealthDcatCatalogRecord.status]: 'adms:status',
141
+ [ClaimsHealthDcatCatalogRecord.modified]: 'dct:modified',
142
+ [ClaimsHealthDcatCatalogRecord.conformsTo]: 'dct:conformsTo',
143
+ [ClaimsHealthDcatDistribution.accessUrl]: 'dcat:accessURL',
144
+ [ClaimsHealthDcatDistribution.format]: 'dct:format',
145
+ [ClaimsHealthDcatDistribution.mediaType]: 'dcat:mediaType',
146
+ [ClaimsHealthDcatDistribution.policy]: 'odrl:hasPolicy',
147
+ [ClaimsHealthDcatDataService.endpointUrl]: 'dcat:endpointURL',
148
+ [ClaimsHealthDcatDataService.servesDataset]: 'dcat:servesDataset',
100
149
  });
101
150
  /** Explicitly projects internal flat claims to DCAT/HealthDCAT JSON-LD. */
102
151
  export function projectHealthDcatResourceToJsonLd(resource) {
@@ -110,9 +159,12 @@ export function projectHealthDcatResourceToJsonLd(resource) {
110
159
  healthdcatap: 'http://healthdataportal.eu/ns/health#',
111
160
  dpv: 'https://w3id.org/dpv#',
112
161
  prov: 'http://www.w3.org/ns/prov#',
162
+ foaf: 'http://xmlns.com/foaf/0.1/',
163
+ adms: 'http://www.w3.org/ns/adms#',
164
+ odrl: 'http://www.w3.org/ns/odrl/2/',
113
165
  }),
114
166
  '@id': normalized.id,
115
- '@type': normalized.resourceType === 'Catalog' ? 'dcat:Catalog' : 'dcat:Dataset',
167
+ '@type': `dcat:${normalized.resourceType}`,
116
168
  };
117
169
  for (const [claim, value] of Object.entries(normalized.meta.claims)) {
118
170
  const jsonLdName = JsonLdClaimNames[claim];
@@ -197,6 +249,118 @@ export function buildHealthDcatCatalog(input) {
197
249
  };
198
250
  return Object.freeze({ resourceType: 'Catalog', id: input.id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
199
251
  }
252
+ /** Builds a governed Dataset Distribution without exposing the source records. */
253
+ export function buildHealthDcatDistribution(input) {
254
+ assertController(input);
255
+ required(input.healthDataAccessBodyIdentifier, 'healthDataAccessBodyIdentifier');
256
+ const accessUrl = required(input.accessUrl, 'accessUrl');
257
+ if (!accessUrl.startsWith('https://'))
258
+ throw new TypeError('accessUrl must use HTTPS');
259
+ const claims = {
260
+ [ClaimsHealthDcatDistribution.identifier]: required(input.id, 'id'),
261
+ [ClaimsHealthDcatDistribution.title]: required(input.title, 'title'),
262
+ [ClaimsHealthDcatDistribution.accessUrl]: accessUrl,
263
+ [ClaimsHealthDcatDistribution.format]: required(input.format, 'format'),
264
+ [ClaimsHealthDcatDistribution.accessRights]: input.accessRights,
265
+ [ClaimsHealthDcatDistribution.userSelected]: true,
266
+ };
267
+ if (input.mediaType)
268
+ claims[ClaimsHealthDcatDistribution.mediaType] = required(input.mediaType, 'mediaType');
269
+ if (input.policyIdentifier)
270
+ claims[ClaimsHealthDcatDistribution.policy] = required(input.policyIdentifier, 'policyIdentifier');
271
+ return normalizeHealthDcatResource({ resourceType: 'Distribution', id: input.id, meta: { claims } });
272
+ }
273
+ /** Builds metadata for a host API that serves published Dataset descriptions. */
274
+ export function buildHealthDcatDataService(input) {
275
+ assertController(input);
276
+ const endpointUrl = required(input.endpointUrl, 'endpointUrl');
277
+ if (!endpointUrl.startsWith('https://'))
278
+ throw new TypeError('endpointUrl must use HTTPS');
279
+ if (!input.servesDatasetIdentifiers.length)
280
+ throw new TypeError('servesDatasetIdentifiers are required');
281
+ return normalizeHealthDcatResource({
282
+ resourceType: 'DataService', id: input.id, meta: { claims: {
283
+ [ClaimsHealthDcatDataService.identifier]: required(input.id, 'id'),
284
+ [ClaimsHealthDcatDataService.title]: required(input.title, 'title'),
285
+ [ClaimsHealthDcatDataService.publisher]: required(input.organizationIdentifier, 'organizationIdentifier'),
286
+ [ClaimsHealthDcatDataService.endpointUrl]: endpointUrl,
287
+ [ClaimsHealthDcatDataService.servesDataset]: Object.freeze([...input.servesDatasetIdentifiers]),
288
+ [ClaimsHealthDcatDataService.accessRights]: input.accessRights,
289
+ [ClaimsHealthDcatDataService.userSelected]: true,
290
+ } },
291
+ });
292
+ }
293
+ /**
294
+ * Links one cohort Dataset to the ResearchStudy that generated it and creates
295
+ * the separate editorial CatalogRecord controlling external discoverability.
296
+ * ResearchSubject identifiers are deliberately absent from this projection.
297
+ */
298
+ export function buildResearchStudyDatasetPublication(input) {
299
+ assertController(input);
300
+ const dataset = normalizeHealthDcatResource(input.dataset);
301
+ if (dataset.resourceType !== 'Dataset')
302
+ throw new TypeError('research_dataset_required');
303
+ const researchStudyIdentifier = required(input.researchStudyIdentifier, 'researchStudyIdentifier');
304
+ if (!researchStudyIdentifier.startsWith('ResearchStudy/'))
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');
309
+ const modifiedAt = new Date(input.modifiedAt);
310
+ if (!Number.isFinite(modifiedAt.getTime()) || modifiedAt.toISOString() !== input.modifiedAt)
311
+ throw new TypeError('modifiedAt must be an ISO instant');
312
+ const linkedDataset = normalizeHealthDcatResource({
313
+ ...dataset,
314
+ meta: { claims: {
315
+ ...dataset.meta.claims,
316
+ [ClaimsHealthDcatDataset.wasGeneratedBy]: researchStudyIdentifier,
317
+ [ClaimsHealthDcatDataset.provenance]: provenanceReference,
318
+ } },
319
+ });
320
+ const recordId = required(input.catalogRecordIdentifier, 'catalogRecordIdentifier');
321
+ const record = normalizeHealthDcatResource({
322
+ resourceType: 'CatalogRecord',
323
+ id: recordId,
324
+ meta: { claims: {
325
+ [ClaimsHealthDcatCatalogRecord.identifier]: recordId,
326
+ [ClaimsHealthDcatCatalogRecord.primaryTopic]: `Dataset/${dataset.id}`,
327
+ [ClaimsHealthDcatCatalogRecord.status]: input.status,
328
+ [ClaimsHealthDcatCatalogRecord.modified]: input.modifiedAt,
329
+ [ClaimsHealthDcatCatalogRecord.conformsTo]: 'HealthDCAT-AP Release 7',
330
+ [ClaimsHealthDcatCatalogRecord.userSelected]: true,
331
+ } },
332
+ });
333
+ return Object.freeze({ dataset: linkedDataset, record });
334
+ }
335
+ /** Controller-only editorial transition; archiving removes external discovery only. */
336
+ export function updateHealthDcatCatalogRecordStatus(input) {
337
+ assertController(input);
338
+ const record = normalizeHealthDcatResource(input.record);
339
+ if (record.resourceType !== 'CatalogRecord')
340
+ throw new TypeError('health_dcat_catalog_record_required');
341
+ const timestamp = new Date(input.modifiedAt);
342
+ if (!Number.isFinite(timestamp.getTime()) || timestamp.toISOString() !== input.modifiedAt)
343
+ throw new TypeError('modifiedAt must be an ISO instant');
344
+ return normalizeHealthDcatResource({
345
+ ...record,
346
+ meta: { claims: {
347
+ ...record.meta.claims,
348
+ [ClaimsHealthDcatCatalogRecord.status]: input.status,
349
+ [ClaimsHealthDcatCatalogRecord.modified]: input.modifiedAt,
350
+ } },
351
+ });
352
+ }
353
+ /** True only for records eligible for an external marketplace search result. */
354
+ export function isHealthDcatCatalogRecordExternallyDiscoverable(record) {
355
+ try {
356
+ const normalized = normalizeHealthDcatResource(record);
357
+ return normalized.resourceType === 'CatalogRecord'
358
+ && normalized.meta.claims[ClaimsHealthDcatCatalogRecord.status] === 'published';
359
+ }
360
+ catch {
361
+ return false;
362
+ }
363
+ }
200
364
  /** Creates an auditable tenant authorization for host-provided catalogue federation. */
201
365
  export function buildHealthDcatFederationRequest(input) {
202
366
  assertController(input);
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,41 @@
1
+ /** Browser-safe FHIR R5 Provenance flat-claim vocabulary and builders. */
2
+ export declare const FhirR5ParticipationRoleTypeValueSet: "http://hl7.org/fhir/ValueSet/participation-role-type";
3
+ export declare const FhirR5ProvenanceParticipantTypeSystem: "http://terminology.hl7.org/CodeSystem/provenance-participant-type";
4
+ export declare const FhirR5ExtraSecurityRoleTypeSystem: "http://terminology.hl7.org/CodeSystem/extra-security-role-type";
5
+ export declare const DicomControlledTerminologySystem: "http://dicom.nema.org/resources/ontology/DCM";
6
+ /** Exact FHIR R5 ParticipationRoleType 5.0.0 expansion used by Provenance.agent.type. */
7
+ 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"];
8
+ export type FhirR5ParticipationRoleTypeCode = (typeof FhirR5ParticipationRoleTypeCodes)[number];
9
+ export type ProvenanceSearchParameterType = 'date' | 'reference' | 'token';
10
+ export type ProvenanceR5SearchParameter = Readonly<{
11
+ code: string;
12
+ type: ProvenanceSearchParameterType;
13
+ expression: string;
14
+ }>;
15
+ /** Exact resource-specific FHIR R5 Provenance SearchParameters. */
16
+ export declare const ProvenanceR5SearchParameterCatalog: readonly ProvenanceR5SearchParameter[];
17
+ /** Standard search claims plus typed-agent correlation claims. */
18
+ export declare const ProvenanceFlatClaimCatalog: readonly string[];
19
+ export type ProvenanceR5FlatClaimsResource = Readonly<{
20
+ resourceType: 'Provenance';
21
+ id: string;
22
+ meta: Readonly<{
23
+ claims: Readonly<Record<string, readonly string[]>>;
24
+ }>;
25
+ }>;
26
+ /** Returns the correlated agent reference claim for one official participation type. */
27
+ export declare function provenanceAgentClaim(code: FhirR5ParticipationRoleTypeCode | string): string;
28
+ /** Returns the system-qualified FHIR token for one official participation type. */
29
+ export declare function participationRoleTypeToken(code: FhirR5ParticipationRoleTypeCode): string;
30
+ /** Validates a claims-only Provenance resource and rejects nested/native fields. */
31
+ export declare function normalizeProvenanceR5FlatClaimsResource(candidate: unknown): ProvenanceR5FlatClaimsResource;
32
+ /** Builds provenance for creation or update of a ResearchStudy with correlated typed agents. */
33
+ export declare function buildResearchStudyProvenanceResource(input: Readonly<{
34
+ id: string;
35
+ researchStudyReference: string;
36
+ recorded: string;
37
+ agents: readonly Readonly<{
38
+ type: FhirR5ParticipationRoleTypeCode;
39
+ who: string;
40
+ }>[];
41
+ }>): ProvenanceR5FlatClaimsResource;
@@ -0,0 +1,99 @@
1
+ /** Browser-safe FHIR R5 Provenance flat-claim vocabulary and builders. */
2
+ export const FhirR5ParticipationRoleTypeValueSet = 'http://hl7.org/fhir/ValueSet/participation-role-type';
3
+ export const FhirR5ProvenanceParticipantTypeSystem = 'http://terminology.hl7.org/CodeSystem/provenance-participant-type';
4
+ export const FhirR5ExtraSecurityRoleTypeSystem = 'http://terminology.hl7.org/CodeSystem/extra-security-role-type';
5
+ export const DicomControlledTerminologySystem = 'http://dicom.nema.org/resources/ontology/DCM';
6
+ /** Exact FHIR R5 ParticipationRoleType 5.0.0 expansion used by Provenance.agent.type. */
7
+ export const FhirR5ParticipationRoleTypeCodes = Object.freeze([
8
+ 'enterer', 'performer', 'author', 'verifier', 'legal', 'attester', 'informant',
9
+ 'custodian', 'assembler', 'composer', '110150', '110151', '110152', '110153',
10
+ '110154', '110155', 'authserver', 'datacollector', 'dataprocessor', 'datasubject',
11
+ 'humanuser',
12
+ ]);
13
+ /** Exact resource-specific FHIR R5 Provenance SearchParameters. */
14
+ export const ProvenanceR5SearchParameterCatalog = Object.freeze([
15
+ { code: 'activity', type: 'token', expression: 'Provenance.activity' },
16
+ { code: 'agent', type: 'reference', expression: 'Provenance.agent.who' },
17
+ { code: 'agent-role', type: 'token', expression: 'Provenance.agent.role' },
18
+ { code: 'agent-type', type: 'token', expression: 'Provenance.agent.type' },
19
+ { code: 'based-on', type: 'reference', expression: 'Provenance.basedOn' },
20
+ { code: 'encounter', type: 'reference', expression: 'Provenance.encounter' },
21
+ { code: 'entity', type: 'reference', expression: 'Provenance.entity.what' },
22
+ { code: 'location', type: 'reference', expression: 'Provenance.location' },
23
+ { code: 'patient', type: 'reference', expression: 'Provenance.patient' },
24
+ { code: 'recorded', type: 'date', expression: 'Provenance.recorded' },
25
+ { code: 'signature-type', type: 'token', expression: 'Provenance.signature.type' },
26
+ { code: 'target', type: 'reference', expression: 'Provenance.target' },
27
+ { code: 'when', type: 'date', expression: 'Provenance.occurred.ofType(dateTime)' },
28
+ ]);
29
+ const standardClaims = ProvenanceR5SearchParameterCatalog.map(parameter => `Provenance.${parameter.code}`);
30
+ const correlatedAgentClaims = FhirR5ParticipationRoleTypeCodes.map(code => `Provenance.agent-${code}-who`);
31
+ /** Standard search claims plus typed-agent correlation claims. */
32
+ export const ProvenanceFlatClaimCatalog = Object.freeze([...standardClaims, ...correlatedAgentClaims]);
33
+ /** Returns the correlated agent reference claim for one official participation type. */
34
+ export function provenanceAgentClaim(code) {
35
+ if (!FhirR5ParticipationRoleTypeCodes.includes(code))
36
+ throw new TypeError('participation_role_type_invalid');
37
+ return `Provenance.agent-${code}-who`;
38
+ }
39
+ /** Returns the system-qualified FHIR token for one official participation type. */
40
+ export function participationRoleTypeToken(code) {
41
+ const system = /^11015[0-5]$/.test(code)
42
+ ? DicomControlledTerminologySystem
43
+ : ['authserver', 'datacollector', 'dataprocessor', 'datasubject', 'humanuser'].includes(code)
44
+ ? FhirR5ExtraSecurityRoleTypeSystem
45
+ : FhirR5ProvenanceParticipantTypeSystem;
46
+ return `${system}|${code}`;
47
+ }
48
+ /** Validates a claims-only Provenance resource and rejects nested/native fields. */
49
+ export function normalizeProvenanceR5FlatClaimsResource(candidate) {
50
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
51
+ throw new TypeError('provenance_flat_resource_invalid');
52
+ const input = candidate;
53
+ if (input.resourceType !== 'Provenance' || typeof input.id !== 'string' || !input.id.trim())
54
+ throw new TypeError('provenance_flat_resource_invalid');
55
+ if (Object.keys(input).some(key => !['resourceType', 'id', 'meta'].includes(key)))
56
+ throw new TypeError('provenance_flat_resource_invalid');
57
+ const meta = input.meta;
58
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta) || Object.keys(meta).some(key => key !== 'claims'))
59
+ throw new TypeError('provenance_flat_resource_invalid');
60
+ const claims = meta.claims;
61
+ if (!claims || typeof claims !== 'object' || Array.isArray(claims))
62
+ throw new TypeError('provenance_flat_resource_invalid');
63
+ const normalized = {};
64
+ for (const [name, rawValue] of Object.entries(claims)) {
65
+ if (!ProvenanceFlatClaimCatalog.includes(name))
66
+ throw new TypeError('provenance_flat_claim_invalid');
67
+ const values = (Array.isArray(rawValue) ? rawValue : [rawValue]).map(value => String(value).trim()).filter(Boolean);
68
+ if (!values.length)
69
+ throw new TypeError('provenance_flat_claim_value_invalid');
70
+ normalized[name] = Object.freeze(values);
71
+ }
72
+ return Object.freeze({ resourceType: 'Provenance', id: input.id.trim(), meta: Object.freeze({ claims: Object.freeze(normalized) }) });
73
+ }
74
+ /** Builds provenance for creation or update of a ResearchStudy with correlated typed agents. */
75
+ export function buildResearchStudyProvenanceResource(input) {
76
+ if (!input.researchStudyReference.startsWith('ResearchStudy/'))
77
+ throw new TypeError('research_study_reference_invalid');
78
+ const recorded = new Date(input.recorded);
79
+ if (!Number.isFinite(recorded.getTime()) || recorded.toISOString() !== input.recorded)
80
+ throw new TypeError('recorded_instant_invalid');
81
+ if (!input.agents.length)
82
+ throw new TypeError('provenance_agent_required');
83
+ const claims = {
84
+ 'Provenance.target': [input.researchStudyReference],
85
+ 'Provenance.recorded': [input.recorded],
86
+ 'Provenance.agent': [],
87
+ 'Provenance.agent-type': [],
88
+ };
89
+ for (const agent of input.agents) {
90
+ const who = String(agent.who || '').trim();
91
+ if (!who)
92
+ throw new TypeError('provenance_agent_who_required');
93
+ claims['Provenance.agent'].push(who);
94
+ claims['Provenance.agent-type'].push(participationRoleTypeToken(agent.type));
95
+ const correlatedClaim = provenanceAgentClaim(agent.type);
96
+ (claims[correlatedClaim] ||= []).push(who);
97
+ }
98
+ return normalizeProvenanceR5FlatClaimsResource({ resourceType: 'Provenance', id: input.id, meta: { claims } });
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
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"