vet-data-utils-ts 0.5.24 → 0.5.26
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 +12 -0
- package/dist/health-dcat.d.ts +87 -1
- package/dist/health-dcat.js +237 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,6 +211,18 @@ Federation 1.0 trust anchors and OpenID4VP 1.0 presentation definitions;
|
|
|
211
211
|
catalogue exchange remains Dataspace Protocol 2025-1. X.509/TLS chain
|
|
212
212
|
validation, federation trust resolution, VP verification and catalogue
|
|
213
213
|
authorization are separate checks performed by their owning adapters.
|
|
214
|
+
`normalizeHealthDcatResource()` rejects unknown or nested index claims, while
|
|
215
|
+
`projectHealthDcatResourceToJsonLd()` is the explicit standards serialization
|
|
216
|
+
boundary used by host/dataspace adapters.
|
|
217
|
+
|
|
218
|
+
Research publication is study-scoped. A derived cohort Dataset links to its
|
|
219
|
+
generating `ResearchStudy` through PROV-O and a separate `CatalogRecord`
|
|
220
|
+
controls whether external catalogue searches may return it. Archiving that
|
|
221
|
+
record withdraws discovery without deleting the study, eligibility/team
|
|
222
|
+
Groups, ResearchSubjects or Dataset, and without changing `dct:accessRights`.
|
|
223
|
+
No individual ResearchSubject identifier is projected into HealthDCAT.
|
|
224
|
+
`Distribution` describes the HDAB-controlled representation; `DataService`
|
|
225
|
+
describes the host API serving Dataset metadata.
|
|
214
226
|
|
|
215
227
|
`vet-data-utils-ts/organization-application` is the product-neutral application
|
|
216
228
|
value used by UHC UNID, VetChain and SOSChain adapters. The host supplies the
|
package/dist/health-dcat.d.ts
CHANGED
|
@@ -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,56 @@ 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';
|
|
73
|
+
/** Complete allowlist for product-persisted HealthDCAT flat claims. */
|
|
74
|
+
export declare const HealthDcatFlatClaimCatalog: Readonly<{
|
|
75
|
+
readonly Dataset: readonly ClaimsHealthDcatDataset[];
|
|
76
|
+
readonly Catalog: readonly ClaimsHealthDcatCatalog[];
|
|
77
|
+
readonly CatalogRecord: readonly ClaimsHealthDcatCatalogRecord[];
|
|
78
|
+
readonly Distribution: readonly ClaimsHealthDcatDistribution[];
|
|
79
|
+
readonly DataService: readonly ClaimsHealthDcatDataService[];
|
|
80
|
+
}>;
|
|
41
81
|
export type HealthDcatResource = Readonly<{
|
|
42
|
-
resourceType: 'Dataset' | 'Catalog';
|
|
82
|
+
resourceType: 'Dataset' | 'Catalog' | 'CatalogRecord' | 'Distribution' | 'DataService';
|
|
43
83
|
id: string;
|
|
44
84
|
meta: Readonly<{
|
|
45
85
|
claims: Readonly<Record<string, ClaimValue>>;
|
|
46
86
|
}>;
|
|
47
87
|
}>;
|
|
88
|
+
/** Validates an indexed HealthDCAT resource and rejects nested or unknown claims. */
|
|
89
|
+
export declare function normalizeHealthDcatResource(candidate: unknown): HealthDcatResource;
|
|
90
|
+
/** Explicitly projects internal flat claims to DCAT/HealthDCAT JSON-LD. */
|
|
91
|
+
export declare function projectHealthDcatResourceToJsonLd(resource: unknown): Readonly<Record<string, unknown>>;
|
|
48
92
|
type ControllerAuthority = Readonly<{
|
|
49
93
|
organizationIdentifier: string;
|
|
50
94
|
controllerIdentifier: string;
|
|
@@ -78,6 +122,48 @@ export declare function buildHealthDcatCatalog(input: ControllerAuthority & Read
|
|
|
78
122
|
datasetIdentifiers: readonly string[];
|
|
79
123
|
applicableLegislation: readonly string[];
|
|
80
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
|
+
catalogRecordIdentifier: string;
|
|
153
|
+
status: HealthDcatCatalogRecordStatus;
|
|
154
|
+
modifiedAt: string;
|
|
155
|
+
}>): Readonly<{
|
|
156
|
+
dataset: HealthDcatResource;
|
|
157
|
+
record: HealthDcatResource;
|
|
158
|
+
}>;
|
|
159
|
+
/** Controller-only editorial transition; archiving removes external discovery only. */
|
|
160
|
+
export declare function updateHealthDcatCatalogRecordStatus(input: ControllerAuthority & Readonly<{
|
|
161
|
+
record: unknown;
|
|
162
|
+
status: HealthDcatCatalogRecordStatus;
|
|
163
|
+
modifiedAt: string;
|
|
164
|
+
}>): HealthDcatResource;
|
|
165
|
+
/** True only for records eligible for an external marketplace search result. */
|
|
166
|
+
export declare function isHealthDcatCatalogRecordExternallyDiscoverable(record: unknown): boolean;
|
|
81
167
|
/** Creates an auditable tenant authorization for host-provided catalogue federation. */
|
|
82
168
|
export declare function buildHealthDcatFederationRequest(input: ControllerAuthority & Readonly<{
|
|
83
169
|
catalogIdentifier: string;
|
package/dist/health-dcat.js
CHANGED
|
@@ -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,6 +42,137 @@ 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 = {}));
|
|
78
|
+
/** Complete allowlist for product-persisted HealthDCAT flat claims. */
|
|
79
|
+
export const HealthDcatFlatClaimCatalog = Object.freeze({
|
|
80
|
+
Dataset: Object.freeze(Object.values(ClaimsHealthDcatDataset)),
|
|
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)),
|
|
85
|
+
});
|
|
86
|
+
/** Validates an indexed HealthDCAT resource and rejects nested or unknown claims. */
|
|
87
|
+
export function normalizeHealthDcatResource(candidate) {
|
|
88
|
+
if (!candidate || typeof candidate !== 'object')
|
|
89
|
+
throw new TypeError('health_dcat_resource_invalid');
|
|
90
|
+
const value = candidate;
|
|
91
|
+
if (!['Dataset', 'Catalog', 'CatalogRecord', 'Distribution', 'DataService'].includes(String(value.resourceType)))
|
|
92
|
+
throw new TypeError('health_dcat_resource_invalid');
|
|
93
|
+
if (typeof value.id !== 'string' || !value.id.trim())
|
|
94
|
+
throw new TypeError('health_dcat_resource_invalid');
|
|
95
|
+
if (!value.meta || typeof value.meta !== 'object' || Array.isArray(value.meta))
|
|
96
|
+
throw new TypeError('health_dcat_resource_invalid');
|
|
97
|
+
const claims = value.meta.claims;
|
|
98
|
+
if (!claims || typeof claims !== 'object' || Array.isArray(claims))
|
|
99
|
+
throw new TypeError('health_dcat_resource_invalid');
|
|
100
|
+
const resourceType = value.resourceType;
|
|
101
|
+
const allowed = HealthDcatFlatClaimCatalog[resourceType];
|
|
102
|
+
const normalized = {};
|
|
103
|
+
for (const [claim, raw] of Object.entries(claims)) {
|
|
104
|
+
if (!allowed.includes(claim))
|
|
105
|
+
throw new TypeError(`health_dcat_claim_not_governed:${claim}`);
|
|
106
|
+
if (typeof raw === 'string' || typeof raw === 'boolean' || typeof raw === 'number')
|
|
107
|
+
normalized[claim] = raw;
|
|
108
|
+
else if (Array.isArray(raw) && raw.every(item => typeof item === 'string'))
|
|
109
|
+
normalized[claim] = Object.freeze([...raw]);
|
|
110
|
+
else
|
|
111
|
+
throw new TypeError(`health_dcat_claim_value_invalid:${claim}`);
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
resourceType,
|
|
115
|
+
id: value.id,
|
|
116
|
+
meta: Object.freeze({ claims: Object.freeze(normalized) }),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const JsonLdClaimNames = Object.freeze({
|
|
120
|
+
[ClaimsHealthDcatDataset.identifier]: 'dct:identifier',
|
|
121
|
+
[ClaimsHealthDcatDataset.title]: 'dct:title',
|
|
122
|
+
[ClaimsHealthDcatDataset.description]: 'dct:description',
|
|
123
|
+
[ClaimsHealthDcatDataset.creator]: 'dct:creator',
|
|
124
|
+
[ClaimsHealthDcatDataset.publisher]: 'dct:publisher',
|
|
125
|
+
[ClaimsHealthDcatDataset.custodian]: 'geodcatap:custodian',
|
|
126
|
+
[ClaimsHealthDcatDataset.accessRights]: 'dct:accessRights',
|
|
127
|
+
[ClaimsHealthDcatDataset.applicableLegislation]: 'dcatap:applicableLegislation',
|
|
128
|
+
[ClaimsHealthDcatDataset.distribution]: 'dcat:distribution',
|
|
129
|
+
[ClaimsHealthDcatDataset.source]: 'dct:source',
|
|
130
|
+
[ClaimsHealthDcatDataset.provenance]: 'dct:provenance',
|
|
131
|
+
[ClaimsHealthDcatDataset.wasGeneratedBy]: 'prov:wasGeneratedBy',
|
|
132
|
+
[ClaimsHealthDcatDataset.personalData]: 'dpv:hasPersonalData',
|
|
133
|
+
[ClaimsHealthDcatDataset.healthCategory]: 'healthdcatap:healthCategory',
|
|
134
|
+
[ClaimsHealthDcatDataset.healthDataAccessBody]: 'healthdcatap:hdab',
|
|
135
|
+
[ClaimsHealthDcatDataset.structuredData]: 'healthdcatap:hasStructuredData',
|
|
136
|
+
[ClaimsHealthDcatDataset.variables]: 'healthdcatap:hasVariables',
|
|
137
|
+
[ClaimsHealthDcatDataset.authorizationReference]: 'prov:qualifiedAttribution',
|
|
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',
|
|
149
|
+
});
|
|
150
|
+
/** Explicitly projects internal flat claims to DCAT/HealthDCAT JSON-LD. */
|
|
151
|
+
export function projectHealthDcatResourceToJsonLd(resource) {
|
|
152
|
+
const normalized = normalizeHealthDcatResource(resource);
|
|
153
|
+
const output = {
|
|
154
|
+
'@context': Object.freeze({
|
|
155
|
+
dcat: 'http://www.w3.org/ns/dcat#',
|
|
156
|
+
dct: 'http://purl.org/dc/terms/',
|
|
157
|
+
dcatap: 'http://data.europa.eu/r5r/',
|
|
158
|
+
geodcatap: 'http://data.europa.eu/930/',
|
|
159
|
+
healthdcatap: 'http://healthdataportal.eu/ns/health#',
|
|
160
|
+
dpv: 'https://w3id.org/dpv#',
|
|
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/',
|
|
165
|
+
}),
|
|
166
|
+
'@id': normalized.id,
|
|
167
|
+
'@type': `dcat:${normalized.resourceType}`,
|
|
168
|
+
};
|
|
169
|
+
for (const [claim, value] of Object.entries(normalized.meta.claims)) {
|
|
170
|
+
const jsonLdName = JsonLdClaimNames[claim];
|
|
171
|
+
if (jsonLdName)
|
|
172
|
+
output[jsonLdName] = value;
|
|
173
|
+
}
|
|
174
|
+
return Object.freeze(output);
|
|
175
|
+
}
|
|
44
176
|
function required(value, field) {
|
|
45
177
|
const normalized = value.trim();
|
|
46
178
|
if (!normalized)
|
|
@@ -117,6 +249,111 @@ export function buildHealthDcatCatalog(input) {
|
|
|
117
249
|
};
|
|
118
250
|
return Object.freeze({ resourceType: 'Catalog', id: input.id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
|
|
119
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 modifiedAt = new Date(input.modifiedAt);
|
|
307
|
+
if (!Number.isFinite(modifiedAt.getTime()) || modifiedAt.toISOString() !== input.modifiedAt)
|
|
308
|
+
throw new TypeError('modifiedAt must be an ISO instant');
|
|
309
|
+
const linkedDataset = normalizeHealthDcatResource({
|
|
310
|
+
...dataset,
|
|
311
|
+
meta: { claims: { ...dataset.meta.claims, [ClaimsHealthDcatDataset.wasGeneratedBy]: researchStudyIdentifier } },
|
|
312
|
+
});
|
|
313
|
+
const recordId = required(input.catalogRecordIdentifier, 'catalogRecordIdentifier');
|
|
314
|
+
const record = normalizeHealthDcatResource({
|
|
315
|
+
resourceType: 'CatalogRecord',
|
|
316
|
+
id: recordId,
|
|
317
|
+
meta: { claims: {
|
|
318
|
+
[ClaimsHealthDcatCatalogRecord.identifier]: recordId,
|
|
319
|
+
[ClaimsHealthDcatCatalogRecord.primaryTopic]: `Dataset/${dataset.id}`,
|
|
320
|
+
[ClaimsHealthDcatCatalogRecord.status]: input.status,
|
|
321
|
+
[ClaimsHealthDcatCatalogRecord.modified]: input.modifiedAt,
|
|
322
|
+
[ClaimsHealthDcatCatalogRecord.conformsTo]: 'HealthDCAT-AP Release 7',
|
|
323
|
+
[ClaimsHealthDcatCatalogRecord.userSelected]: true,
|
|
324
|
+
} },
|
|
325
|
+
});
|
|
326
|
+
return Object.freeze({ dataset: linkedDataset, record });
|
|
327
|
+
}
|
|
328
|
+
/** Controller-only editorial transition; archiving removes external discovery only. */
|
|
329
|
+
export function updateHealthDcatCatalogRecordStatus(input) {
|
|
330
|
+
assertController(input);
|
|
331
|
+
const record = normalizeHealthDcatResource(input.record);
|
|
332
|
+
if (record.resourceType !== 'CatalogRecord')
|
|
333
|
+
throw new TypeError('health_dcat_catalog_record_required');
|
|
334
|
+
const timestamp = new Date(input.modifiedAt);
|
|
335
|
+
if (!Number.isFinite(timestamp.getTime()) || timestamp.toISOString() !== input.modifiedAt)
|
|
336
|
+
throw new TypeError('modifiedAt must be an ISO instant');
|
|
337
|
+
return normalizeHealthDcatResource({
|
|
338
|
+
...record,
|
|
339
|
+
meta: { claims: {
|
|
340
|
+
...record.meta.claims,
|
|
341
|
+
[ClaimsHealthDcatCatalogRecord.status]: input.status,
|
|
342
|
+
[ClaimsHealthDcatCatalogRecord.modified]: input.modifiedAt,
|
|
343
|
+
} },
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/** True only for records eligible for an external marketplace search result. */
|
|
347
|
+
export function isHealthDcatCatalogRecordExternallyDiscoverable(record) {
|
|
348
|
+
try {
|
|
349
|
+
const normalized = normalizeHealthDcatResource(record);
|
|
350
|
+
return normalized.resourceType === 'CatalogRecord'
|
|
351
|
+
&& normalized.meta.claims[ClaimsHealthDcatCatalogRecord.status] === 'published';
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
120
357
|
/** Creates an auditable tenant authorization for host-provided catalogue federation. */
|
|
121
358
|
export function buildHealthDcatFederationRequest(input) {
|
|
122
359
|
assertController(input);
|