@twin.org/dataspace-control-plane-service 0.0.3-next.26 → 0.0.3-next.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/dist/es/dataspaceControlPlaneRoutes.js +275 -9
- package/dist/es/dataspaceControlPlaneRoutes.js.map +1 -1
- package/dist/es/dataspaceControlPlaneService.js +272 -60
- package/dist/es/dataspaceControlPlaneService.js.map +1 -1
- package/dist/es/models/IDataspaceControlPlaneServiceConstructorOptions.js.map +1 -1
- package/dist/es/schema.js +2 -1
- package/dist/es/schema.js.map +1 -1
- package/dist/types/dataspaceControlPlaneService.d.ts +42 -3
- package/dist/types/models/IDataspaceControlPlaneServiceConstructorOptions.d.ts +10 -0
- package/docs/changelog.md +42 -28
- package/docs/reference/classes/DataspaceControlPlaneService.md +160 -1
- package/docs/reference/interfaces/IDataspaceControlPlaneServiceConstructorOptions.md +28 -0
- package/locales/en.json +9 -7
- package/package.json +7 -5
|
@@ -3,6 +3,7 @@ import { ArrayHelper, BaseError, ComponentFactory, Converter, GeneralError, Guar
|
|
|
3
3
|
import { JsonLdHelper } from "@twin.org/data-json-ld";
|
|
4
4
|
import { DataspaceAppFactory, TransferProcessRole } from "@twin.org/dataspace-models";
|
|
5
5
|
import { EngineCoreFactory } from "@twin.org/engine-models";
|
|
6
|
+
import { ComparisonOperator } from "@twin.org/entity";
|
|
6
7
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
7
8
|
import { OdrlPolicyHelper, PolicyRequesterFactory } from "@twin.org/rights-management-models";
|
|
8
9
|
import { DataspaceProtocolContexts, DataspaceProtocolContractNegotiationTypes, DataspaceProtocolEndpointType, DataspaceProtocolHelper, DataspaceProtocolTransferProcessStateType, DataspaceProtocolTransferProcessTypes } from "@twin.org/standards-dataspace-protocol";
|
|
@@ -69,6 +70,11 @@ export class DataspaceControlPlaneService {
|
|
|
69
70
|
* @internal
|
|
70
71
|
*/
|
|
71
72
|
_transferProcessStorage;
|
|
73
|
+
/**
|
|
74
|
+
* Entity storage for tenant-supplied Dataspace App Dataset entities.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
_dataspaceAppDatasetStorage;
|
|
72
78
|
/**
|
|
73
79
|
* The trust component for token verification and generation.
|
|
74
80
|
* @internal
|
|
@@ -101,6 +107,11 @@ export class DataspaceControlPlaneService {
|
|
|
101
107
|
* @internal
|
|
102
108
|
*/
|
|
103
109
|
_negotiationCallbacks;
|
|
110
|
+
/**
|
|
111
|
+
* The component type name for the hosting component.
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
_urlTransformerComponent;
|
|
104
115
|
/**
|
|
105
116
|
* Create a new instance of DataspaceControlPlaneService.
|
|
106
117
|
* @param options The options for the service.
|
|
@@ -118,12 +129,14 @@ export class DataspaceControlPlaneService {
|
|
|
118
129
|
// Retrieve Federated Catalogue component with default
|
|
119
130
|
this._federatedCatalogueComponent = ComponentFactory.get(options?.federatedCatalogueComponentType ?? "federated-catalogue");
|
|
120
131
|
this._transferProcessStorage = EntityStorageConnectorFactory.get(options?.transferProcessEntityStorageType ?? "transfer-process");
|
|
132
|
+
this._dataspaceAppDatasetStorage = EntityStorageConnectorFactory.get(options?.dataspaceAppDatasetEntityStorageType ?? "dataspace-app-dataset");
|
|
121
133
|
this._trustComponent = ComponentFactory.get(options?.trustComponentType ?? "trust");
|
|
122
134
|
this._overrideTrustGeneratorType = options?.config?.overrideTrustGeneratorType;
|
|
123
135
|
this._dataPlanePath = Is.stringValue(options?.config?.dataPlanePath)
|
|
124
136
|
? StringHelper.trimLeadingSlashes(options.config.dataPlanePath)
|
|
125
137
|
: undefined;
|
|
126
138
|
this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
|
|
139
|
+
this._urlTransformerComponent = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
|
|
127
140
|
this._negotiationCallbacks = new Map();
|
|
128
141
|
const internalCallback = {
|
|
129
142
|
onStateChanged: async (negotiationId, state, data) => {
|
|
@@ -206,7 +219,9 @@ export class DataspaceControlPlaneService {
|
|
|
206
219
|
/**
|
|
207
220
|
* The service needs to be started when the application is initialized.
|
|
208
221
|
* Populates the Federated Catalogue with datasets from registered apps
|
|
209
|
-
* and starts the stalled negotiation cleanup task.
|
|
222
|
+
* and starts the stalled negotiation cleanup task. Also captures the node
|
|
223
|
+
* identity from ContextIdStore when tenant-token encryption is configured
|
|
224
|
+
* (required to derive the vault key name `${nodeId}/${signingKeyName}`).
|
|
210
225
|
* @param nodeLoggingComponentType The node logging component type.
|
|
211
226
|
*/
|
|
212
227
|
async start(nodeLoggingComponentType) {
|
|
@@ -227,28 +242,20 @@ export class DataspaceControlPlaneService {
|
|
|
227
242
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
228
243
|
message: "populatingFederatedCatalogue"
|
|
229
244
|
});
|
|
230
|
-
// Get all registered apps
|
|
231
|
-
const appNames = DataspaceAppFactory.names();
|
|
232
|
-
await this._loggingComponent?.log({
|
|
233
|
-
level: "debug",
|
|
234
|
-
ts: Date.now(),
|
|
235
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
236
|
-
message: "discoveredApps",
|
|
237
|
-
data: { count: appNames.length, apps: appNames }
|
|
238
|
-
});
|
|
239
245
|
let registeredCount = 0;
|
|
240
246
|
let errorCount = 0;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
+
let totalDatasets = 0;
|
|
248
|
+
// Walk stored dataspace app datasets one page at a time and publish each in its
|
|
249
|
+
// owning tenant's context. Memory stays bounded to one page.
|
|
250
|
+
let cursor;
|
|
251
|
+
do {
|
|
252
|
+
const page = await this._dataspaceAppDatasetStorage.query(undefined, undefined, undefined, cursor);
|
|
253
|
+
if (Is.arrayValue(page.entities)) {
|
|
254
|
+
for (const entity of page.entities) {
|
|
255
|
+
const appDataset = entity;
|
|
256
|
+
totalDatasets++;
|
|
247
257
|
try {
|
|
248
|
-
|
|
249
|
-
const datasetWithPublisher = await this.ensurePublisher(dataset);
|
|
250
|
-
// Register with Federated Catalogue
|
|
251
|
-
await this._federatedCatalogueComponent.set(datasetWithPublisher);
|
|
258
|
+
await this.publishAppDataset(appDataset);
|
|
252
259
|
registeredCount++;
|
|
253
260
|
await this._loggingComponent?.log({
|
|
254
261
|
level: "debug",
|
|
@@ -256,8 +263,9 @@ export class DataspaceControlPlaneService {
|
|
|
256
263
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
257
264
|
message: "datasetRegistered",
|
|
258
265
|
data: {
|
|
259
|
-
datasetId:
|
|
260
|
-
|
|
266
|
+
datasetId: appDataset.id,
|
|
267
|
+
appId: appDataset.appId,
|
|
268
|
+
tenantId: appDataset.tenantId
|
|
261
269
|
}
|
|
262
270
|
});
|
|
263
271
|
}
|
|
@@ -267,27 +275,19 @@ export class DataspaceControlPlaneService {
|
|
|
267
275
|
level: "error",
|
|
268
276
|
ts: Date.now(),
|
|
269
277
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
270
|
-
message: "
|
|
278
|
+
message: "datasetPublishFailed",
|
|
271
279
|
error: BaseError.fromError(error),
|
|
272
280
|
data: {
|
|
273
|
-
|
|
274
|
-
|
|
281
|
+
datasetRecordId: appDataset.id,
|
|
282
|
+
appId: appDataset.appId,
|
|
283
|
+
tenantId: appDataset.tenantId
|
|
275
284
|
}
|
|
276
285
|
});
|
|
277
286
|
}
|
|
278
287
|
}
|
|
279
288
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
level: "error",
|
|
283
|
-
ts: Date.now(),
|
|
284
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
285
|
-
message: "appDatasetsRetrievalFailed",
|
|
286
|
-
error: BaseError.fromError(error),
|
|
287
|
-
data: { appName }
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
289
|
+
cursor = page.cursor;
|
|
290
|
+
} while (Is.stringValue(cursor));
|
|
291
291
|
await this._loggingComponent?.log({
|
|
292
292
|
level: "info",
|
|
293
293
|
ts: Date.now(),
|
|
@@ -296,7 +296,7 @@ export class DataspaceControlPlaneService {
|
|
|
296
296
|
data: {
|
|
297
297
|
registeredCount,
|
|
298
298
|
errorCount,
|
|
299
|
-
|
|
299
|
+
totalDatasets
|
|
300
300
|
}
|
|
301
301
|
});
|
|
302
302
|
if (this._taskScheduler) {
|
|
@@ -336,7 +336,7 @@ export class DataspaceControlPlaneService {
|
|
|
336
336
|
async requestTransfer(request, trustPayload) {
|
|
337
337
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "requestTransfer");
|
|
338
338
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(request));
|
|
339
|
-
if (validationFailures
|
|
339
|
+
if (Is.arrayValue(validationFailures)) {
|
|
340
340
|
await this._loggingComponent?.log({
|
|
341
341
|
level: "error",
|
|
342
342
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -445,7 +445,7 @@ export class DataspaceControlPlaneService {
|
|
|
445
445
|
async startTransfer(message, publicOrigin, trustPayload) {
|
|
446
446
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "startTransfer");
|
|
447
447
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
|
|
448
|
-
if (validationFailures
|
|
448
|
+
if (Is.arrayValue(validationFailures)) {
|
|
449
449
|
await this._loggingComponent?.log({
|
|
450
450
|
level: "error",
|
|
451
451
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -527,7 +527,12 @@ export class DataspaceControlPlaneService {
|
|
|
527
527
|
}
|
|
528
528
|
});
|
|
529
529
|
const tokenString = accessToken;
|
|
530
|
-
|
|
530
|
+
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
|
|
531
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
532
|
+
const tenantId = contextIds?.[ContextIdKeys.Tenant];
|
|
533
|
+
if (Is.stringValue(tenantId)) {
|
|
534
|
+
fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", tenantId);
|
|
535
|
+
}
|
|
531
536
|
response.dataAddress = {
|
|
532
537
|
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
533
538
|
endpointType: DataspaceProtocolEndpointType.HttpsQueryEndpoint,
|
|
@@ -607,7 +612,7 @@ export class DataspaceControlPlaneService {
|
|
|
607
612
|
async completeTransfer(message, trustPayload) {
|
|
608
613
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "completeTransfer");
|
|
609
614
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
|
|
610
|
-
if (validationFailures
|
|
615
|
+
if (Is.arrayValue(validationFailures)) {
|
|
611
616
|
await this._loggingComponent?.log({
|
|
612
617
|
level: "error",
|
|
613
618
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -662,7 +667,7 @@ export class DataspaceControlPlaneService {
|
|
|
662
667
|
async suspendTransfer(message, trustPayload) {
|
|
663
668
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "suspendTransfer");
|
|
664
669
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
|
|
665
|
-
if (validationFailures
|
|
670
|
+
if (Is.arrayValue(validationFailures)) {
|
|
666
671
|
await this._loggingComponent?.log({
|
|
667
672
|
level: "error",
|
|
668
673
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -718,7 +723,7 @@ export class DataspaceControlPlaneService {
|
|
|
718
723
|
async terminateTransfer(message, trustPayload) {
|
|
719
724
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "terminateTransfer");
|
|
720
725
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
|
|
721
|
-
if (validationFailures
|
|
726
|
+
if (Is.arrayValue(validationFailures)) {
|
|
722
727
|
await this._loggingComponent?.log({
|
|
723
728
|
level: "error",
|
|
724
729
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -1115,6 +1120,144 @@ export class DataspaceControlPlaneService {
|
|
|
1115
1120
|
dataAddress: entity.dataAddress
|
|
1116
1121
|
};
|
|
1117
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Register a dataset for a dataspace app, owned by the calling tenant.
|
|
1125
|
+
* @param id Optional explicit id. If omitted, derived from `dataset["@id"]`
|
|
1126
|
+
* or generated.
|
|
1127
|
+
* @param appId The dataspace app this dataset belongs to.
|
|
1128
|
+
* @param dataset The dataset payload.
|
|
1129
|
+
* @returns The resolved dataset id.
|
|
1130
|
+
*/
|
|
1131
|
+
async createAppDataset(id, appId, dataset) {
|
|
1132
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "appId", appId);
|
|
1133
|
+
Guards.object(DataspaceControlPlaneService.CLASS_NAME, "dataset", dataset);
|
|
1134
|
+
const resolvedId = id ??
|
|
1135
|
+
(Is.stringValue(dataset["@id"]) ? dataset["@id"] : RandomHelper.generateUuidV7("compact"));
|
|
1136
|
+
const tenantId = await this.resolveCallingTenantId();
|
|
1137
|
+
const nodeIdentity = await this.resolveNodeIdentity();
|
|
1138
|
+
const existing = await this._dataspaceAppDatasetStorage.get(resolvedId);
|
|
1139
|
+
if (!Is.empty(existing)) {
|
|
1140
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetAlreadyExists", {
|
|
1141
|
+
id: resolvedId
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
const now = new Date().toISOString();
|
|
1145
|
+
const entity = {
|
|
1146
|
+
id: resolvedId,
|
|
1147
|
+
nodeIdentity,
|
|
1148
|
+
tenantId,
|
|
1149
|
+
appId,
|
|
1150
|
+
dataset: this.stripDatasetId(dataset),
|
|
1151
|
+
dateCreated: now,
|
|
1152
|
+
dateModified: now
|
|
1153
|
+
};
|
|
1154
|
+
// Side effect first, primary storage last
|
|
1155
|
+
await this.publishAppDataset(entity);
|
|
1156
|
+
await this._dataspaceAppDatasetStorage.set(entity);
|
|
1157
|
+
return resolvedId;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Get a dataset record owned by the calling tenant.
|
|
1161
|
+
* @param id The stored dataset id.
|
|
1162
|
+
* @returns The stored dataset record.
|
|
1163
|
+
*/
|
|
1164
|
+
async getAppDataset(id) {
|
|
1165
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
|
|
1166
|
+
const tenantId = await this.resolveCallingTenantId();
|
|
1167
|
+
const entity = await this._dataspaceAppDatasetStorage.get(id);
|
|
1168
|
+
if (Is.empty(entity)) {
|
|
1169
|
+
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
|
|
1170
|
+
}
|
|
1171
|
+
if (entity.tenantId !== tenantId) {
|
|
1172
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
id: entity.id,
|
|
1176
|
+
appId: entity.appId,
|
|
1177
|
+
dataset: this.restampDatasetId(entity.dataset, entity.id),
|
|
1178
|
+
dateCreated: entity.dateCreated,
|
|
1179
|
+
dateModified: entity.dateModified
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* List the dataspace app datasets owned by the calling tenant.
|
|
1184
|
+
* @param cursor Optional pagination cursor.
|
|
1185
|
+
* @param limit Optional maximum number of entries to return.
|
|
1186
|
+
* @returns The stored datasets and the next-page cursor if more exist.
|
|
1187
|
+
*/
|
|
1188
|
+
async listAppDatasets(cursor, limit) {
|
|
1189
|
+
const tenantId = await this.resolveCallingTenantId();
|
|
1190
|
+
const page = await this._dataspaceAppDatasetStorage.query(Is.stringValue(tenantId)
|
|
1191
|
+
? {
|
|
1192
|
+
property: "tenantId",
|
|
1193
|
+
value: tenantId,
|
|
1194
|
+
comparison: ComparisonOperator.Equals
|
|
1195
|
+
}
|
|
1196
|
+
: undefined, undefined, undefined, cursor, limit);
|
|
1197
|
+
const entities = (page.entities ?? []).map(entity => ({
|
|
1198
|
+
id: entity.id ?? "",
|
|
1199
|
+
appId: entity.appId ?? "",
|
|
1200
|
+
dataset: this.restampDatasetId(entity.dataset ?? {}, entity.id ?? ""),
|
|
1201
|
+
dateCreated: entity.dateCreated ?? "",
|
|
1202
|
+
dateModified: entity.dateModified ?? ""
|
|
1203
|
+
}));
|
|
1204
|
+
return {
|
|
1205
|
+
entities,
|
|
1206
|
+
cursor: page.cursor
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Update a dataset record owned by the calling tenant.
|
|
1211
|
+
* @param id The stored dataset id.
|
|
1212
|
+
* @param appId The dataspace app this dataset belongs to.
|
|
1213
|
+
* @param dataset The dataset payload.
|
|
1214
|
+
*/
|
|
1215
|
+
async updateAppDataset(id, appId, dataset) {
|
|
1216
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
|
|
1217
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "appId", appId);
|
|
1218
|
+
Guards.object(DataspaceControlPlaneService.CLASS_NAME, "dataset", dataset);
|
|
1219
|
+
const tenantId = await this.resolveCallingTenantId();
|
|
1220
|
+
const existing = await this._dataspaceAppDatasetStorage.get(id);
|
|
1221
|
+
if (Is.empty(existing)) {
|
|
1222
|
+
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
|
|
1223
|
+
}
|
|
1224
|
+
if (existing.tenantId !== tenantId) {
|
|
1225
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
|
|
1226
|
+
}
|
|
1227
|
+
const updated = {
|
|
1228
|
+
...existing,
|
|
1229
|
+
appId,
|
|
1230
|
+
dataset: this.stripDatasetId(dataset),
|
|
1231
|
+
dateModified: new Date().toISOString()
|
|
1232
|
+
};
|
|
1233
|
+
// Side effect first, primary storage last
|
|
1234
|
+
await this.publishAppDataset(updated);
|
|
1235
|
+
await this._dataspaceAppDatasetStorage.set(updated);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Delete a dataspace app dataset owned by the calling tenant.
|
|
1239
|
+
* @param id The stored app dataset id.
|
|
1240
|
+
*/
|
|
1241
|
+
async deleteAppDataset(id) {
|
|
1242
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
|
|
1243
|
+
const tenantId = await this.resolveCallingTenantId();
|
|
1244
|
+
const existing = await this._dataspaceAppDatasetStorage.get(id);
|
|
1245
|
+
if (Is.empty(existing)) {
|
|
1246
|
+
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
|
|
1247
|
+
}
|
|
1248
|
+
if (existing.tenantId !== tenantId) {
|
|
1249
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
|
|
1250
|
+
}
|
|
1251
|
+
// Side effect first, primary storage last
|
|
1252
|
+
const wrappedContextIds = {
|
|
1253
|
+
...((await ContextIdStore.getContextIds()) ?? {}),
|
|
1254
|
+
[ContextIdKeys.Tenant]: existing.tenantId
|
|
1255
|
+
};
|
|
1256
|
+
await ContextIdStore.run(wrappedContextIds, async () => {
|
|
1257
|
+
await this._federatedCatalogueComponent.remove(id);
|
|
1258
|
+
});
|
|
1259
|
+
await this._dataspaceAppDatasetStorage.remove(id);
|
|
1260
|
+
}
|
|
1118
1261
|
// ============================================================================
|
|
1119
1262
|
// PRIVATE HELPER METHODS
|
|
1120
1263
|
// ============================================================================
|
|
@@ -1155,7 +1298,7 @@ export class DataspaceControlPlaneService {
|
|
|
1155
1298
|
}
|
|
1156
1299
|
}
|
|
1157
1300
|
}
|
|
1158
|
-
if (stalled
|
|
1301
|
+
if (Is.arrayValue(stalled)) {
|
|
1159
1302
|
await this._loggingComponent?.log({
|
|
1160
1303
|
level: "info",
|
|
1161
1304
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -1283,20 +1426,21 @@ export class DataspaceControlPlaneService {
|
|
|
1283
1426
|
agreementId: OdrlPolicyHelper.getUid(agreement)
|
|
1284
1427
|
});
|
|
1285
1428
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1429
|
+
// Top-level target identifies the dataset; rule-level targets are constraint
|
|
1430
|
+
// scopes (refinements, JSONPath filters, AssetCollections) and aren't datasets.
|
|
1431
|
+
const datasetTargets = OdrlPolicyHelper.getDatasetTargets(agreement);
|
|
1432
|
+
if (!Is.arrayValue(datasetTargets)) {
|
|
1288
1433
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "agreementTargetMissingUid", {
|
|
1289
1434
|
agreementId: OdrlPolicyHelper.getUid(agreement)
|
|
1290
1435
|
});
|
|
1291
1436
|
}
|
|
1292
|
-
|
|
1293
|
-
if (targetIds.length > 1) {
|
|
1437
|
+
if (datasetTargets.length > 1) {
|
|
1294
1438
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "agreementMultipleTargetsNotSupported", {
|
|
1295
1439
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1296
|
-
targetCount:
|
|
1440
|
+
targetCount: datasetTargets.length
|
|
1297
1441
|
});
|
|
1298
1442
|
}
|
|
1299
|
-
return
|
|
1443
|
+
return datasetTargets[0];
|
|
1300
1444
|
}
|
|
1301
1445
|
/**
|
|
1302
1446
|
* Validate that the dataset exists in the Federated Catalogue.
|
|
@@ -1478,12 +1622,12 @@ export class DataspaceControlPlaneService {
|
|
|
1478
1622
|
// the target is implicitly the Dataset. When the catalogue offer has no targets,
|
|
1479
1623
|
// skip the target comparison entirely (the agreement's target is the dataset itself).
|
|
1480
1624
|
// Only reject if both have explicit targets that don't overlap.
|
|
1481
|
-
if (offerTargets
|
|
1625
|
+
if (Is.arrayValue(offerTargets) && Is.arrayValue(agreementTargets)) {
|
|
1482
1626
|
if (!agreementTargets.some((agreementTarget) => offerTargets.includes(agreementTarget))) {
|
|
1483
1627
|
return false;
|
|
1484
1628
|
}
|
|
1485
1629
|
}
|
|
1486
|
-
else if (offerTargets
|
|
1630
|
+
else if (Is.arrayValue(offerTargets) && !Is.arrayValue(agreementTargets)) {
|
|
1487
1631
|
// Offer has targets but agreement doesn't — mismatch
|
|
1488
1632
|
return false;
|
|
1489
1633
|
}
|
|
@@ -1502,24 +1646,92 @@ export class DataspaceControlPlaneService {
|
|
|
1502
1646
|
return true;
|
|
1503
1647
|
}
|
|
1504
1648
|
/**
|
|
1505
|
-
*
|
|
1506
|
-
* @param dataset The dataset.
|
|
1507
|
-
* @returns The dataset
|
|
1649
|
+
* Populate the system-controlled fields of a dataset payload.
|
|
1650
|
+
* @param dataset The user-supplied dataset payload.
|
|
1651
|
+
* @returns The populated dataset.
|
|
1508
1652
|
* @internal
|
|
1509
1653
|
*/
|
|
1510
|
-
async
|
|
1654
|
+
async populateDefaults(dataset) {
|
|
1511
1655
|
if (dataset["dcterms:publisher"]) {
|
|
1512
1656
|
return dataset;
|
|
1513
1657
|
}
|
|
1514
1658
|
const contextIds = await ContextIdStore.getContextIds();
|
|
1515
1659
|
const orgId = contextIds?.[ContextIdKeys.Organization];
|
|
1516
1660
|
if (orgId) {
|
|
1517
|
-
return {
|
|
1518
|
-
...dataset,
|
|
1519
|
-
"dcterms:publisher": orgId
|
|
1520
|
-
};
|
|
1661
|
+
return { ...dataset, "dcterms:publisher": orgId };
|
|
1521
1662
|
}
|
|
1522
1663
|
return dataset;
|
|
1523
1664
|
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Strip `@id` from a dataset payload before storing it.
|
|
1667
|
+
* @param dataset The dataset payload.
|
|
1668
|
+
* @returns The dataset payload with `@id` removed.
|
|
1669
|
+
* @internal
|
|
1670
|
+
*/
|
|
1671
|
+
stripDatasetId(dataset) {
|
|
1672
|
+
const copy = { ...dataset };
|
|
1673
|
+
delete copy["@id"];
|
|
1674
|
+
return copy;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Re-stamp `@id` onto a stored payload blob using the entity primary key.
|
|
1678
|
+
* @param payload The stored payload blob (without `@id`).
|
|
1679
|
+
* @param id The entity id (becomes the dataset's `@id`).
|
|
1680
|
+
* @returns The dataset payload with `@id` populated.
|
|
1681
|
+
* @internal
|
|
1682
|
+
*/
|
|
1683
|
+
restampDatasetId(payload, id) {
|
|
1684
|
+
return { ...payload, "@id": id };
|
|
1685
|
+
}
|
|
1686
|
+
// DATASPACE APP DATASET HANDLERS
|
|
1687
|
+
/**
|
|
1688
|
+
* Publish a single dataspace app dataset to the federated catalogue in its owning tenant's context.
|
|
1689
|
+
* @param appDataset The stored dataspace app dataset entity.
|
|
1690
|
+
* @internal
|
|
1691
|
+
*/
|
|
1692
|
+
async publishAppDataset(appDataset) {
|
|
1693
|
+
// Override `Tenant` in the context wrap so federated catalogue captures the
|
|
1694
|
+
// appDataset's owning tenant. On single-tenant nodes the appDataset has no tenantId,
|
|
1695
|
+
// so the assignment writes `Tenant: undefined` — equivalent to no override.
|
|
1696
|
+
const wrappedContextIds = {
|
|
1697
|
+
...((await ContextIdStore.getContextIds()) ?? {}),
|
|
1698
|
+
[ContextIdKeys.Tenant]: appDataset.tenantId
|
|
1699
|
+
};
|
|
1700
|
+
await ContextIdStore.run(wrappedContextIds, async () => {
|
|
1701
|
+
const app = DataspaceAppFactory.get(appDataset.appId);
|
|
1702
|
+
const datasetPayload = this.restampDatasetId(appDataset.dataset, appDataset.id);
|
|
1703
|
+
const overrideHandler = app.datasetsHandled?.bind(app);
|
|
1704
|
+
const rawDatasets = overrideHandler
|
|
1705
|
+
? await overrideHandler(datasetPayload, appDataset.tenantId ?? "")
|
|
1706
|
+
: [datasetPayload];
|
|
1707
|
+
const datasets = await Promise.all(rawDatasets.map(async (d) => this.populateDefaults(d)));
|
|
1708
|
+
for (const dataset of datasets) {
|
|
1709
|
+
await this._federatedCatalogueComponent.set(dataset);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Resolve the calling tenant from the request context.
|
|
1715
|
+
* @returns The owning tenant id, or undefined for single-tenant nodes.
|
|
1716
|
+
* @internal
|
|
1717
|
+
*/
|
|
1718
|
+
async resolveCallingTenantId() {
|
|
1719
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
1720
|
+
const tenantId = contextIds?.[ContextIdKeys.Tenant];
|
|
1721
|
+
return Is.stringValue(tenantId) ? tenantId : undefined;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Resolve the node identity from the current context, throwing if absent.
|
|
1725
|
+
* @returns The owning node identity.
|
|
1726
|
+
* @internal
|
|
1727
|
+
*/
|
|
1728
|
+
async resolveNodeIdentity() {
|
|
1729
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
1730
|
+
const nodeId = contextIds?.[ContextIdKeys.Node];
|
|
1731
|
+
if (!Is.stringValue(nodeId)) {
|
|
1732
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetNodeContextRequired");
|
|
1733
|
+
}
|
|
1734
|
+
return nodeId;
|
|
1735
|
+
}
|
|
1524
1736
|
}
|
|
1525
1737
|
//# sourceMappingURL=dataspaceControlPlaneService.js.map
|