@twin.org/auditable-item-graph-service 0.0.3-next.8 → 0.9.0-next.2
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 +3 -1
- package/dist/es/auditableItemGraphRoutes.js +309 -8
- package/dist/es/auditableItemGraphRoutes.js.map +1 -1
- package/dist/es/auditableItemGraphService.js +576 -95
- package/dist/es/auditableItemGraphService.js.map +1 -1
- package/dist/es/entities/auditableItemGraphAlias.js +8 -0
- package/dist/es/entities/auditableItemGraphAlias.js.map +1 -1
- package/dist/es/entities/auditableItemGraphChangeset.js +8 -0
- package/dist/es/entities/auditableItemGraphChangeset.js.map +1 -1
- package/dist/es/entities/auditableItemGraphVertex.js +9 -1
- package/dist/es/entities/auditableItemGraphVertex.js.map +1 -1
- package/dist/es/models/IAuditableItemGraphServiceConfig.js.map +1 -1
- package/dist/es/models/IAuditableItemGraphServiceConstructorOptions.js.map +1 -1
- package/dist/es/models/IAuditableItemGraphServiceContext.js +2 -0
- package/dist/es/models/IAuditableItemGraphServiceContext.js.map +1 -1
- package/dist/es/restEntryPoints.js +3 -0
- package/dist/es/restEntryPoints.js.map +1 -1
- package/dist/types/auditableItemGraphRoutes.d.ts +34 -2
- package/dist/types/auditableItemGraphService.d.ts +50 -71
- package/dist/types/entities/auditableItemGraphAlias.d.ts +4 -0
- package/dist/types/entities/auditableItemGraphChangeset.d.ts +4 -0
- package/dist/types/entities/auditableItemGraphVertex.d.ts +5 -1
- package/dist/types/models/IAuditableItemGraphServiceConfig.d.ts +4 -0
- package/dist/types/models/IAuditableItemGraphServiceConstructorOptions.d.ts +4 -0
- package/dist/types/models/IAuditableItemGraphServiceContext.d.ts +6 -3
- package/dist/types/restEntryPoints.d.ts +3 -0
- package/docs/changelog.md +437 -84
- package/docs/examples.md +241 -1
- package/docs/open-api/spec.json +845 -268
- package/docs/reference/classes/AuditableItemGraphAlias.md +18 -10
- package/docs/reference/classes/AuditableItemGraphChangeset.md +16 -8
- package/docs/reference/classes/AuditableItemGraphEdge.md +10 -10
- package/docs/reference/classes/AuditableItemGraphPatch.md +6 -6
- package/docs/reference/classes/AuditableItemGraphResource.md +9 -9
- package/docs/reference/classes/AuditableItemGraphService.md +135 -57
- package/docs/reference/classes/AuditableItemGraphVertex.md +26 -18
- package/docs/reference/functions/auditableItemGraphRemoveProof.md +31 -0
- package/docs/reference/functions/auditableItemGraphUpdate.md +1 -1
- package/docs/reference/functions/auditableItemGraphUpdatePartial.md +31 -0
- package/docs/reference/functions/auditableItemGraphVersionGet.md +31 -0
- package/docs/reference/functions/auditableItemGraphVersionList.md +31 -0
- package/docs/reference/index.md +4 -0
- package/docs/reference/interfaces/IAuditableItemGraphServiceConfig.md +8 -0
- package/docs/reference/interfaces/IAuditableItemGraphServiceConstructorOptions.md +18 -10
- package/docs/reference/variables/restEntryPoints.md +2 -0
- package/locales/en.json +6 -2
- package/package.json +6 -6
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Copyright 2024 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
-
import { AuditableItemGraphContexts, AuditableItemGraphTopics, AuditableItemGraphTypes, VerifyDepth } from "@twin.org/auditable-item-graph-models";
|
|
4
|
-
import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
5
|
-
import { ArrayHelper, ComponentFactory, GeneralError, Guards, Is, JsonHelper, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@twin.org/core";
|
|
6
|
-
import {
|
|
3
|
+
import { AuditableItemGraphContexts, AuditableItemGraphDataTypes, AuditableItemGraphMetricIds, AuditableItemGraphMetrics, AuditableItemGraphTopics, AuditableItemGraphTypes, VerifyDepth } from "@twin.org/auditable-item-graph-models";
|
|
4
|
+
import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
5
|
+
import { ArrayHelper, Coerce, ComponentFactory, GeneralError, Guards, Is, JsonHelper, Mutex, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@twin.org/core";
|
|
6
|
+
import { DataTypeHelper } from "@twin.org/data-core";
|
|
7
|
+
import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
|
|
7
8
|
import { ComparisonOperator, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
8
9
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
9
10
|
import { ImmutableProofContexts, ImmutableProofFailure, ImmutableProofTypes } from "@twin.org/immutable-proof-models";
|
|
10
11
|
import { SchemaOrgContexts, SchemaOrgDataTypes, SchemaOrgTypes } from "@twin.org/standards-schema-org";
|
|
12
|
+
import { MetricHelper } from "@twin.org/telemetry-models";
|
|
11
13
|
/**
|
|
12
14
|
* Class for performing auditable item graph operations.
|
|
13
15
|
*/
|
|
@@ -60,6 +62,16 @@ export class AuditableItemGraphService {
|
|
|
60
62
|
* @internal
|
|
61
63
|
*/
|
|
62
64
|
_eventBusComponent;
|
|
65
|
+
/**
|
|
66
|
+
* The telemetry component.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
_telemetryComponent;
|
|
70
|
+
/**
|
|
71
|
+
* The timeout in milliseconds when acquiring a mutex lock.
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
_mutexTimeoutMs;
|
|
63
75
|
/**
|
|
64
76
|
* Create a new instance of AuditableItemGraphService.
|
|
65
77
|
* @param options The dependencies for the auditable item graph connector.
|
|
@@ -68,10 +80,12 @@ export class AuditableItemGraphService {
|
|
|
68
80
|
this._immutableProofComponent = ComponentFactory.get(options?.immutableProofComponentType ?? "immutable-proof");
|
|
69
81
|
this._vertexStorage = EntityStorageConnectorFactory.get(options?.vertexEntityStorageType ?? "auditable-item-graph-vertex");
|
|
70
82
|
this._changesetStorage = EntityStorageConnectorFactory.get(options?.changesetEntityStorageType ?? "auditable-item-graph-changeset");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
this._eventBusComponent = ComponentFactory.getIfExists(options?.eventBusComponentType);
|
|
84
|
+
this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
|
|
85
|
+
this._mutexTimeoutMs = Coerce.integer(options?.config?.mutexTimeoutMs);
|
|
74
86
|
SchemaOrgDataTypes.registerRedirects();
|
|
87
|
+
AuditableItemGraphDataTypes.registerTypes();
|
|
88
|
+
JsonLdDataTypes.registerTypes();
|
|
75
89
|
}
|
|
76
90
|
/**
|
|
77
91
|
* Returns the class name of the component.
|
|
@@ -80,6 +94,16 @@ export class AuditableItemGraphService {
|
|
|
80
94
|
className() {
|
|
81
95
|
return AuditableItemGraphService.CLASS_NAME;
|
|
82
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Register all AIG metrics with the telemetry component.
|
|
99
|
+
* @returns A promise that resolves when all metrics have been registered.
|
|
100
|
+
*/
|
|
101
|
+
async start() {
|
|
102
|
+
if (Is.undefined(this._telemetryComponent)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await MetricHelper.createMetrics(this._telemetryComponent, AuditableItemGraphMetrics);
|
|
106
|
+
}
|
|
83
107
|
/**
|
|
84
108
|
* Create a new graph vertex.
|
|
85
109
|
* @param vertex The vertex to create.
|
|
@@ -92,20 +116,29 @@ export class AuditableItemGraphService {
|
|
|
92
116
|
async create(vertex) {
|
|
93
117
|
Guards.object(AuditableItemGraphService.CLASS_NAME, "vertex", vertex);
|
|
94
118
|
const contextIds = await ContextIdStore.getContextIds();
|
|
119
|
+
ContextIdHelper.guard(contextIds, ContextIdKeys.Organization);
|
|
95
120
|
try {
|
|
121
|
+
const id = RandomHelper.generateUuidV7("compact");
|
|
122
|
+
const schemaValidationFailures = [];
|
|
123
|
+
await DataTypeHelper.validate("vertex", `${AuditableItemGraphContexts.Namespace}${AuditableItemGraphTypes.Vertex}`, {
|
|
124
|
+
...vertex,
|
|
125
|
+
id
|
|
126
|
+
}, schemaValidationFailures);
|
|
127
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex", schemaValidationFailures);
|
|
96
128
|
if (Is.object(vertex.annotationObject)) {
|
|
97
129
|
const validationFailures = [];
|
|
98
130
|
await JsonLdHelper.validate(vertex.annotationObject, validationFailures);
|
|
99
131
|
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
|
|
100
132
|
}
|
|
101
|
-
const
|
|
133
|
+
const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
|
|
102
134
|
const context = {
|
|
103
135
|
now: new Date(Date.now()).toISOString(),
|
|
104
|
-
|
|
136
|
+
organizationIdentity: ownerOrganizationId,
|
|
137
|
+
userIdentity: contextIds?.[ContextIdKeys.User]
|
|
105
138
|
};
|
|
106
139
|
const vertexModel = {
|
|
107
140
|
id,
|
|
108
|
-
organizationIdentity: contextIds
|
|
141
|
+
organizationIdentity: contextIds[ContextIdKeys.Organization],
|
|
109
142
|
dateCreated: context.now
|
|
110
143
|
};
|
|
111
144
|
const originalEntity = ObjectHelper.clone(vertexModel);
|
|
@@ -115,11 +148,13 @@ export class AuditableItemGraphService {
|
|
|
115
148
|
await this.updateEdgeList(context, vertexModel, vertex.edges);
|
|
116
149
|
delete originalEntity.aliasIndex;
|
|
117
150
|
delete originalEntity.resourceTypeIndex;
|
|
118
|
-
await this.addChangeset(context, originalEntity, vertexModel, true);
|
|
151
|
+
await this.addChangeset(context, originalEntity, vertexModel, true, 0);
|
|
152
|
+
vertexModel.version = 0;
|
|
119
153
|
await this._vertexStorage.set({
|
|
120
154
|
...vertexModel,
|
|
121
155
|
...this.buildIndexes(vertexModel)
|
|
122
156
|
});
|
|
157
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesCreated);
|
|
123
158
|
const fullId = new Urn(AuditableItemGraphService.NAMESPACE, id).toString();
|
|
124
159
|
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexCreated, { id: fullId });
|
|
125
160
|
return fullId;
|
|
@@ -128,6 +163,116 @@ export class AuditableItemGraphService {
|
|
|
128
163
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "createFailed", undefined, error);
|
|
129
164
|
}
|
|
130
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Update a graph vertex (PUT — full replacement of vertex state).
|
|
168
|
+
* Concurrent updates for the same vertex are serialized via `Mutex` on the vertex id.
|
|
169
|
+
* @param vertex The vertex to update.
|
|
170
|
+
* @returns A promise that resolves when the vertex has been updated.
|
|
171
|
+
*/
|
|
172
|
+
async update(vertex) {
|
|
173
|
+
Guards.object(AuditableItemGraphService.CLASS_NAME, "vertex", vertex);
|
|
174
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "vertex.id", vertex.id);
|
|
175
|
+
const vertexId = this.parseVertexId(vertex.id);
|
|
176
|
+
await Mutex.lock(vertexId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
|
|
177
|
+
try {
|
|
178
|
+
try {
|
|
179
|
+
const schemaValidationFailures = [];
|
|
180
|
+
await DataTypeHelper.validate("vertex", `${AuditableItemGraphContexts.Namespace}${AuditableItemGraphTypes.Vertex}`, vertex, schemaValidationFailures);
|
|
181
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex", schemaValidationFailures);
|
|
182
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
183
|
+
if (Is.empty(vertexEntity)) {
|
|
184
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", vertex.id);
|
|
185
|
+
}
|
|
186
|
+
if (Is.object(vertex.annotationObject)) {
|
|
187
|
+
const validationFailures = [];
|
|
188
|
+
await JsonLdHelper.validate(vertex.annotationObject, validationFailures);
|
|
189
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
|
|
190
|
+
}
|
|
191
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
192
|
+
const ownerOrganizationId = vertexEntity.organizationIdentity ??
|
|
193
|
+
contextIds?.[ContextIdKeys.UserOrganization] ??
|
|
194
|
+
contextIds?.[ContextIdKeys.Organization];
|
|
195
|
+
const context = {
|
|
196
|
+
now: new Date(Date.now()).toISOString(),
|
|
197
|
+
organizationIdentity: ownerOrganizationId,
|
|
198
|
+
userIdentity: contextIds?.[ContextIdKeys.User]
|
|
199
|
+
};
|
|
200
|
+
delete vertexEntity.aliasIndex;
|
|
201
|
+
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
202
|
+
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
203
|
+
newEntity.annotationObject = vertex.annotationObject;
|
|
204
|
+
await this.updateAliasList(context, newEntity, vertex.aliases);
|
|
205
|
+
await this.updateResourceList(context, newEntity, vertex.resources);
|
|
206
|
+
await this.updateEdgeList(context, newEntity, vertex.edges);
|
|
207
|
+
await this.persistVertexChanges(context, vertexId, vertex.id, originalEntity, newEntity);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "updatingFailed", undefined, error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
Mutex.unlock(vertexId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Partially update a graph vertex (PATCH — explicit list patches; only defined properties applied).
|
|
219
|
+
* Serialized with `update` via `Mutex` on the same vertex id within this instance.
|
|
220
|
+
* @param partial The partial vertex update.
|
|
221
|
+
* @returns A promise that resolves when the partial update has been applied.
|
|
222
|
+
*/
|
|
223
|
+
async updatePartial(partial) {
|
|
224
|
+
Guards.object(AuditableItemGraphService.CLASS_NAME, "partial", partial);
|
|
225
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "partial.id", partial.id);
|
|
226
|
+
const vertexId = this.parseVertexId(partial.id);
|
|
227
|
+
await Mutex.lock(vertexId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
|
|
228
|
+
try {
|
|
229
|
+
try {
|
|
230
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
231
|
+
if (Is.empty(vertexEntity)) {
|
|
232
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", partial.id);
|
|
233
|
+
}
|
|
234
|
+
if (partial.annotationObject !== undefined && Is.object(partial.annotationObject)) {
|
|
235
|
+
const validationFailures = [];
|
|
236
|
+
await JsonLdHelper.validate(partial.annotationObject, validationFailures);
|
|
237
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "partial.annotationObject", validationFailures);
|
|
238
|
+
}
|
|
239
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
240
|
+
const ownerOrganizationId = vertexEntity.organizationIdentity ??
|
|
241
|
+
contextIds?.[ContextIdKeys.UserOrganization] ??
|
|
242
|
+
contextIds?.[ContextIdKeys.Organization];
|
|
243
|
+
const context = {
|
|
244
|
+
now: new Date(Date.now()).toISOString(),
|
|
245
|
+
organizationIdentity: ownerOrganizationId,
|
|
246
|
+
userIdentity: contextIds?.[ContextIdKeys.User]
|
|
247
|
+
};
|
|
248
|
+
delete vertexEntity.aliasIndex;
|
|
249
|
+
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
250
|
+
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
251
|
+
if (partial.annotationObject !== undefined) {
|
|
252
|
+
newEntity.annotationObject = partial.annotationObject;
|
|
253
|
+
}
|
|
254
|
+
if (partial.aliasPatches !== undefined) {
|
|
255
|
+
const aliasPatch = this.validateListPatch("partial.aliasPatches", partial.aliasPatches);
|
|
256
|
+
await this.applyAliasPatch(context, newEntity, aliasPatch);
|
|
257
|
+
}
|
|
258
|
+
if (partial.resourcePatches !== undefined) {
|
|
259
|
+
const resourcePatch = this.validateListPatch("partial.resourcePatches", partial.resourcePatches);
|
|
260
|
+
await this.applyResourcePatch(context, newEntity, resourcePatch);
|
|
261
|
+
}
|
|
262
|
+
if (partial.edgePatches !== undefined) {
|
|
263
|
+
const edgePatch = this.validateListPatch("partial.edgePatches", partial.edgePatches);
|
|
264
|
+
await this.applyEdgePatch(context, newEntity, edgePatch);
|
|
265
|
+
}
|
|
266
|
+
await this.persistVertexChanges(context, vertexId, partial.id, originalEntity, newEntity);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "updatingFailed", undefined, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
Mutex.unlock(vertexId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
131
276
|
/**
|
|
132
277
|
* Get a graph vertex.
|
|
133
278
|
* @param id The id of the vertex to get.
|
|
@@ -217,6 +362,14 @@ export class AuditableItemGraphService {
|
|
|
217
362
|
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
218
363
|
}
|
|
219
364
|
const chunk = await this.verifyChangesetChunk(vertexId, options?.verifySignatureDepth ?? VerifyDepth.None, cursor, limit);
|
|
365
|
+
if ((options?.verifySignatureDepth ?? VerifyDepth.None) !== VerifyDepth.None) {
|
|
366
|
+
if (chunk.verified) {
|
|
367
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
220
373
|
const changesetList = {
|
|
221
374
|
"@context": [
|
|
222
375
|
SchemaOrgContexts.Context,
|
|
@@ -270,6 +423,14 @@ export class AuditableItemGraphService {
|
|
|
270
423
|
if (verifySignatureDepth !== VerifyDepth.None) {
|
|
271
424
|
changesetModel["@context"]?.push(ImmutableProofContexts.Context);
|
|
272
425
|
changesetModel.verification = await this.verifyChangesetSignature(changesetModel);
|
|
426
|
+
if (changesetModel.verification?.verified) {
|
|
427
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed, {
|
|
431
|
+
failureReason: changesetModel.verification?.failure
|
|
432
|
+
});
|
|
433
|
+
}
|
|
273
434
|
}
|
|
274
435
|
const result = await JsonLdProcessor.compact(changesetModel, changesetModel["@context"]);
|
|
275
436
|
return result;
|
|
@@ -279,70 +440,68 @@ export class AuditableItemGraphService {
|
|
|
279
440
|
}
|
|
280
441
|
}
|
|
281
442
|
/**
|
|
282
|
-
*
|
|
283
|
-
* @param
|
|
284
|
-
* @param
|
|
285
|
-
* @
|
|
286
|
-
* @
|
|
287
|
-
* @param vertex.resources The resources attached to the vertex.
|
|
288
|
-
* @param vertex.edges The edges connected to the vertex.
|
|
289
|
-
* @returns Nothing.
|
|
443
|
+
* Get a graph vertex at a specific version.
|
|
444
|
+
* @param id The id of the vertex.
|
|
445
|
+
* @param version The version number to retrieve.
|
|
446
|
+
* @returns The vertex reconstructed at that version.
|
|
447
|
+
* @throws NotFoundError if the vertex or version is not found.
|
|
290
448
|
*/
|
|
291
|
-
async
|
|
292
|
-
Guards.
|
|
293
|
-
Guards.
|
|
294
|
-
const
|
|
295
|
-
const urnParsed = Urn.fromValidString(vertex.id);
|
|
449
|
+
async getVersion(id, version) {
|
|
450
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
451
|
+
Guards.integer(AuditableItemGraphService.CLASS_NAME, "version", version);
|
|
452
|
+
const urnParsed = Urn.fromValidString(id);
|
|
296
453
|
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
297
454
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
|
|
298
455
|
namespace: AuditableItemGraphService.NAMESPACE,
|
|
299
|
-
id
|
|
456
|
+
id
|
|
300
457
|
});
|
|
301
458
|
}
|
|
302
459
|
try {
|
|
303
460
|
const vertexId = urnParsed.namespaceSpecific(0);
|
|
304
461
|
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
305
462
|
if (Is.empty(vertexEntity)) {
|
|
306
|
-
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound",
|
|
463
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
307
464
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
|
|
465
|
+
const currentVersion = vertexEntity.version ?? 0;
|
|
466
|
+
if (version > currentVersion || version < 0) {
|
|
467
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "versionNotFound", version.toString());
|
|
312
468
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
469
|
+
// Short circuit if requesting the current version to avoid unnecessary changeset retrieval and patching
|
|
470
|
+
if (version === currentVersion) {
|
|
471
|
+
const vertexModel = this.vertexEntityToJsonLd(vertexEntity);
|
|
472
|
+
vertexModel.version = version;
|
|
473
|
+
return await JsonLdProcessor.compact(vertexModel, vertexModel["@context"]);
|
|
474
|
+
}
|
|
475
|
+
const changesets = await this.internalGetChangesets(vertexId, {
|
|
476
|
+
maxVersion: version
|
|
477
|
+
});
|
|
478
|
+
let entityState = {
|
|
479
|
+
id: vertexEntity.id,
|
|
480
|
+
dateCreated: vertexEntity.dateCreated,
|
|
481
|
+
organizationIdentity: vertexEntity.organizationIdentity
|
|
316
482
|
};
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
320
|
-
newEntity.annotationObject = vertex.annotationObject;
|
|
321
|
-
await this.updateAliasList(context, newEntity, vertex.aliases);
|
|
322
|
-
await this.updateResourceList(context, newEntity, vertex.resources);
|
|
323
|
-
await this.updateEdgeList(context, newEntity, vertex.edges);
|
|
324
|
-
const patches = await this.addChangeset(context, originalEntity, newEntity, false);
|
|
325
|
-
if (patches.length > 0) {
|
|
326
|
-
newEntity.dateModified = context.now;
|
|
327
|
-
const indexes = this.buildIndexes(newEntity);
|
|
328
|
-
await this._vertexStorage.set({
|
|
329
|
-
...newEntity,
|
|
330
|
-
...indexes
|
|
331
|
-
});
|
|
332
|
-
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertex.id, patches });
|
|
483
|
+
for (const changeset of changesets) {
|
|
484
|
+
entityState = JsonHelper.patch(entityState, changeset.patches);
|
|
333
485
|
}
|
|
486
|
+
const vertexModel = this.vertexEntityToJsonLd(entityState);
|
|
487
|
+
vertexModel.version = version;
|
|
488
|
+
const result = await JsonLdProcessor.compact(vertexModel, vertexModel["@context"]);
|
|
489
|
+
return result;
|
|
334
490
|
}
|
|
335
491
|
catch (error) {
|
|
336
|
-
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "
|
|
492
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionFailed", undefined, error);
|
|
337
493
|
}
|
|
338
494
|
}
|
|
339
495
|
/**
|
|
340
|
-
*
|
|
341
|
-
* @param id The id of the vertex
|
|
342
|
-
* @
|
|
496
|
+
* Get all versions of a graph vertex.
|
|
497
|
+
* @param id The id of the vertex.
|
|
498
|
+
* @param options Additional options for the operation.
|
|
499
|
+
* @param options.after Only return versions created after this ISO 8601 timestamp (exclusive).
|
|
500
|
+
* @param options.before Only return versions created before this ISO 8601 timestamp (exclusive).
|
|
501
|
+
* @returns The list of vertex versions.
|
|
343
502
|
* @throws NotFoundError if the vertex is not found.
|
|
344
503
|
*/
|
|
345
|
-
async
|
|
504
|
+
async getVersions(id, options) {
|
|
346
505
|
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
347
506
|
const urnParsed = Urn.fromValidString(id);
|
|
348
507
|
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
@@ -353,6 +512,52 @@ export class AuditableItemGraphService {
|
|
|
353
512
|
}
|
|
354
513
|
try {
|
|
355
514
|
const vertexId = urnParsed.namespaceSpecific(0);
|
|
515
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
516
|
+
if (Is.empty(vertexEntity)) {
|
|
517
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
518
|
+
}
|
|
519
|
+
const beforeDate = Coerce.dateTime(options?.before);
|
|
520
|
+
const afterDate = Coerce.dateTime(options?.after);
|
|
521
|
+
const allChangesets = await this.internalGetChangesets(vertexId, {
|
|
522
|
+
before: beforeDate?.toISOString()
|
|
523
|
+
});
|
|
524
|
+
const versions = [];
|
|
525
|
+
for (const changeset of allChangesets) {
|
|
526
|
+
const changesetDate = Coerce.dateTime(changeset.dateCreated);
|
|
527
|
+
const afterExcluded = !Is.empty(afterDate) && !Is.empty(changesetDate) && changesetDate <= afterDate;
|
|
528
|
+
if (!afterExcluded) {
|
|
529
|
+
versions.push({
|
|
530
|
+
version: changeset.version ?? 0,
|
|
531
|
+
dateCreated: changeset.dateCreated
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const versionList = {
|
|
536
|
+
"@context": [
|
|
537
|
+
SchemaOrgContexts.Context,
|
|
538
|
+
AuditableItemGraphContexts.Context,
|
|
539
|
+
AuditableItemGraphContexts.ContextCommon
|
|
540
|
+
],
|
|
541
|
+
type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexVersionList],
|
|
542
|
+
[SchemaOrgTypes.ItemListElement]: versions
|
|
543
|
+
};
|
|
544
|
+
return await JsonLdProcessor.compact(versionList, versionList["@context"]);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionsFailed", undefined, error);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Remove the proof for an item.
|
|
552
|
+
* @param id The id of the vertex to remove the proof from.
|
|
553
|
+
* @returns A promise that resolves when the proof has been removed from all changesets.
|
|
554
|
+
* @throws NotFoundError if the vertex is not found.
|
|
555
|
+
*/
|
|
556
|
+
async removeProof(id) {
|
|
557
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
558
|
+
const vertexId = this.parseVertexId(id);
|
|
559
|
+
await Mutex.lock(vertexId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
|
|
560
|
+
try {
|
|
356
561
|
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
357
562
|
if (Is.empty(vertexEntity)) {
|
|
358
563
|
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
@@ -371,7 +576,7 @@ export class AuditableItemGraphService {
|
|
|
371
576
|
], undefined, changesetsResult?.cursor);
|
|
372
577
|
for (const changeset of changesetsResult.entities) {
|
|
373
578
|
if (Is.stringValue(changeset.proofId)) {
|
|
374
|
-
await this._immutableProofComponent.
|
|
579
|
+
await this._immutableProofComponent.removeNotarization(changeset.proofId);
|
|
375
580
|
delete changeset.proofId;
|
|
376
581
|
await this._changesetStorage.set(changeset);
|
|
377
582
|
}
|
|
@@ -379,7 +584,10 @@ export class AuditableItemGraphService {
|
|
|
379
584
|
} while (Is.stringValue(changesetsResult.cursor));
|
|
380
585
|
}
|
|
381
586
|
catch (error) {
|
|
382
|
-
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "
|
|
587
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "removeProofFailed", undefined, error);
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
Mutex.unlock(vertexId);
|
|
383
591
|
}
|
|
384
592
|
}
|
|
385
593
|
/**
|
|
@@ -388,7 +596,7 @@ export class AuditableItemGraphService {
|
|
|
388
596
|
* @param options.id The optional id to look for.
|
|
389
597
|
* @param options.idMode Look in id, alias or both, defaults to both.
|
|
390
598
|
* @param options.idExact Find only exact matches, default to false meaning partial matching.
|
|
391
|
-
* @param options.
|
|
599
|
+
* @param options.resourceTypes Include vertices with specific resource types.
|
|
392
600
|
* @param conditions Conditions to use in the query.
|
|
393
601
|
* @param orderBy The order for the results, defaults to created.
|
|
394
602
|
* @param orderByDirection The direction for the order, defaults to desc.
|
|
@@ -406,46 +614,62 @@ export class AuditableItemGraphService {
|
|
|
406
614
|
"aliases",
|
|
407
615
|
"annotationObject"
|
|
408
616
|
];
|
|
409
|
-
const
|
|
617
|
+
const andGroups = [];
|
|
410
618
|
const orderProperty = orderBy ?? "dateCreated";
|
|
411
619
|
const orderDirection = orderByDirection ?? SortDirection.Descending;
|
|
412
620
|
const idExact = options?.idExact ?? false;
|
|
621
|
+
if (!Is.empty(conditions)) {
|
|
622
|
+
andGroups.push(conditions);
|
|
623
|
+
}
|
|
413
624
|
const idOrAlias = options?.id;
|
|
414
625
|
if (Is.stringValue(idOrAlias)) {
|
|
415
626
|
const idMode = options?.idMode ?? "both";
|
|
627
|
+
const idComparators = [];
|
|
416
628
|
if (idMode === "id" || idMode === "both") {
|
|
417
|
-
|
|
629
|
+
idComparators.push({
|
|
418
630
|
property: "id",
|
|
419
631
|
comparison: idExact ? ComparisonOperator.Equals : ComparisonOperator.Includes,
|
|
420
632
|
value: idOrAlias
|
|
421
633
|
});
|
|
422
634
|
}
|
|
423
635
|
if (idMode === "alias" || idMode === "both") {
|
|
424
|
-
|
|
636
|
+
idComparators.push({
|
|
425
637
|
property: "aliasIndex",
|
|
426
638
|
comparison: ComparisonOperator.Includes,
|
|
427
639
|
value: idExact ? `||${idOrAlias.toLowerCase()}||` : idOrAlias.toLowerCase()
|
|
428
640
|
});
|
|
429
641
|
}
|
|
642
|
+
if (idComparators.length === 1) {
|
|
643
|
+
andGroups.push(idComparators[0]);
|
|
644
|
+
}
|
|
645
|
+
else if (idComparators.length > 1) {
|
|
646
|
+
andGroups.push({ logicalOperator: LogicalOperator.Or, conditions: idComparators });
|
|
647
|
+
}
|
|
430
648
|
}
|
|
431
|
-
if (Is.arrayValue(options?.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
649
|
+
if (Is.arrayValue(options?.resourceTypes)) {
|
|
650
|
+
const resourceComparators = options.resourceTypes.map(rt => ({
|
|
651
|
+
property: "resourceTypeIndex",
|
|
652
|
+
comparison: ComparisonOperator.Includes,
|
|
653
|
+
value: `||${rt.toLowerCase()}||`
|
|
654
|
+
}));
|
|
655
|
+
if (resourceComparators.length === 1) {
|
|
656
|
+
andGroups.push(resourceComparators[0]);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
andGroups.push({
|
|
660
|
+
logicalOperator: LogicalOperator.Or,
|
|
661
|
+
conditions: resourceComparators
|
|
437
662
|
});
|
|
438
663
|
}
|
|
439
664
|
}
|
|
440
665
|
if (!propertiesToReturn.includes("id")) {
|
|
441
666
|
propertiesToReturn.unshift("id");
|
|
442
667
|
}
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
: undefined, [
|
|
668
|
+
const finalConditions = {
|
|
669
|
+
logicalOperator: LogicalOperator.And,
|
|
670
|
+
conditions: andGroups
|
|
671
|
+
};
|
|
672
|
+
const results = await this._vertexStorage.query(finalConditions, [
|
|
449
673
|
{
|
|
450
674
|
property: orderProperty,
|
|
451
675
|
sortDirection: orderDirection
|
|
@@ -462,6 +686,10 @@ export class AuditableItemGraphService {
|
|
|
462
686
|
[SchemaOrgTypes.ItemListElement]: models
|
|
463
687
|
};
|
|
464
688
|
const result = await JsonLdProcessor.compact(vertexList, vertexList["@context"]);
|
|
689
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.QueriesExecuted, {
|
|
690
|
+
resultCount: models.length,
|
|
691
|
+
hasMore: Is.stringValue(results.cursor)
|
|
692
|
+
});
|
|
465
693
|
return {
|
|
466
694
|
entries: result,
|
|
467
695
|
cursor: results.cursor
|
|
@@ -471,6 +699,69 @@ export class AuditableItemGraphService {
|
|
|
471
699
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "queryingFailed", undefined, error);
|
|
472
700
|
}
|
|
473
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Parse and validate a vertex URN; return the compact storage id.
|
|
704
|
+
* @param id The vertex URN.
|
|
705
|
+
* @returns The compact vertex id.
|
|
706
|
+
* @throws {GeneralError} If the namespace does not match the expected namespace.
|
|
707
|
+
* @internal
|
|
708
|
+
*/
|
|
709
|
+
parseVertexId(id) {
|
|
710
|
+
const urnParsed = Urn.fromValidString(id);
|
|
711
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
712
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
|
|
713
|
+
namespace: AuditableItemGraphService.NAMESPACE,
|
|
714
|
+
id
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
return urnParsed.namespaceSpecific(0);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Validate that a PATCH sub-list value is a list patch object, not a bare array.
|
|
721
|
+
* @param propertyName The property name for error reporting.
|
|
722
|
+
* @param patch The patch value.
|
|
723
|
+
* @returns The validated list patch.
|
|
724
|
+
* @throws {GeneralError} If the patch value is a bare array instead of a list patch object.
|
|
725
|
+
* @internal
|
|
726
|
+
*/
|
|
727
|
+
validateListPatch(propertyName, patch) {
|
|
728
|
+
if (Is.array(patch)) {
|
|
729
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "listPatchInvalidFormat", {
|
|
730
|
+
property: propertyName
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
Guards.object(AuditableItemGraphService.CLASS_NAME, propertyName, patch);
|
|
734
|
+
return patch;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Persist vertex changes after update or partial update.
|
|
738
|
+
* @param context The context for the operation.
|
|
739
|
+
* @param vertexId The compact vertex id.
|
|
740
|
+
* @param vertexUrn The vertex URN for events.
|
|
741
|
+
* @param originalEntity The entity before changes.
|
|
742
|
+
* @param newEntity The entity after changes.
|
|
743
|
+
* @returns A promise that resolves when the changes have been persisted and events published.
|
|
744
|
+
* @internal
|
|
745
|
+
*/
|
|
746
|
+
async persistVertexChanges(context, vertexId, vertexUrn, originalEntity, newEntity) {
|
|
747
|
+
const nextVersion = Is.empty(originalEntity.version)
|
|
748
|
+
? (await this.internalGetChangesets(vertexId)).length
|
|
749
|
+
: originalEntity.version + 1;
|
|
750
|
+
const patches = await this.addChangeset(context, originalEntity, newEntity, false, nextVersion);
|
|
751
|
+
if (patches.length > 0) {
|
|
752
|
+
newEntity.dateModified = context.now;
|
|
753
|
+
newEntity.version = nextVersion;
|
|
754
|
+
const indexes = this.buildIndexes(newEntity);
|
|
755
|
+
await this._vertexStorage.set({
|
|
756
|
+
...newEntity,
|
|
757
|
+
...indexes
|
|
758
|
+
});
|
|
759
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
|
|
760
|
+
patchCount: patches.length
|
|
761
|
+
});
|
|
762
|
+
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertexUrn, patches });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
474
765
|
/**
|
|
475
766
|
* Map the vertex entity to JSON-LD.
|
|
476
767
|
* @param vertexEntity The vertex entity.
|
|
@@ -587,24 +878,62 @@ export class AuditableItemGraphService {
|
|
|
587
878
|
patchFrom: p.from,
|
|
588
879
|
patchValue: p.value
|
|
589
880
|
})),
|
|
590
|
-
proofId: changesetEntity.proofId
|
|
881
|
+
proofId: changesetEntity.proofId,
|
|
882
|
+
version: changesetEntity.version
|
|
591
883
|
};
|
|
592
884
|
return model;
|
|
593
885
|
}
|
|
594
886
|
/**
|
|
595
|
-
*
|
|
887
|
+
* Fetch all changesets for a vertex in ascending date order.
|
|
888
|
+
* @param vertexId The internal vertex id.
|
|
889
|
+
* @param options Optional filtering options.
|
|
890
|
+
* @param options.before Only fetch changesets created strictly before this ISO 8601 timestamp.
|
|
891
|
+
* @param options.maxVersion Only fetch changesets with version <= this value.
|
|
892
|
+
* @returns All changeset entities sorted ascending by dateCreated.
|
|
893
|
+
* @internal
|
|
894
|
+
*/
|
|
895
|
+
async internalGetChangesets(vertexId, options) {
|
|
896
|
+
const all = [];
|
|
897
|
+
let cursor;
|
|
898
|
+
const conditions = [
|
|
899
|
+
{ property: "vertexId", value: vertexId, comparison: ComparisonOperator.Equals }
|
|
900
|
+
];
|
|
901
|
+
if (Is.stringValue(options?.before)) {
|
|
902
|
+
conditions.push({
|
|
903
|
+
property: "dateCreated",
|
|
904
|
+
value: options.before,
|
|
905
|
+
comparison: ComparisonOperator.LessThan
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
if (!Is.empty(options?.maxVersion)) {
|
|
909
|
+
conditions.push({
|
|
910
|
+
property: "version",
|
|
911
|
+
value: options.maxVersion,
|
|
912
|
+
comparison: ComparisonOperator.LessThanOrEqual
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
do {
|
|
916
|
+
const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }], undefined, cursor);
|
|
917
|
+
all.push(...result.entities);
|
|
918
|
+
cursor = result.cursor;
|
|
919
|
+
} while (Is.stringValue(cursor));
|
|
920
|
+
return all;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Replace the aliases of a vertex model (PUT).
|
|
596
924
|
* @param context The context for the operation.
|
|
597
925
|
* @param vertex The vertex.
|
|
598
|
-
* @param aliases The
|
|
926
|
+
* @param aliases The new active alias set.
|
|
927
|
+
* @returns A promise that resolves when the alias list has been replaced.
|
|
599
928
|
* @internal
|
|
600
929
|
*/
|
|
601
930
|
async updateAliasList(context, vertex, aliases) {
|
|
602
931
|
const active = vertex.aliases?.filter(a => Is.empty(a.dateDeleted)) ?? [];
|
|
603
|
-
// The active aliases that are not in the update list should be marked as deleted.
|
|
604
932
|
if (Is.arrayValue(active)) {
|
|
605
933
|
for (const alias of active) {
|
|
606
934
|
if (!aliases?.find(a => a.id === alias.id)) {
|
|
607
935
|
alias.dateDeleted = context.now;
|
|
936
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
|
|
608
937
|
}
|
|
609
938
|
}
|
|
610
939
|
}
|
|
@@ -614,18 +943,44 @@ export class AuditableItemGraphService {
|
|
|
614
943
|
}
|
|
615
944
|
}
|
|
616
945
|
}
|
|
946
|
+
/**
|
|
947
|
+
* Apply alias patch operations (PATCH).
|
|
948
|
+
* @param context The context for the operation.
|
|
949
|
+
* @param vertex The vertex.
|
|
950
|
+
* @param patch The alias patch.
|
|
951
|
+
* @returns A promise that resolves when the alias patch has been applied.
|
|
952
|
+
* @internal
|
|
953
|
+
*/
|
|
954
|
+
async applyAliasPatch(context, vertex, patch) {
|
|
955
|
+
if (Is.arrayValue(patch.remove)) {
|
|
956
|
+
for (const removeId of patch.remove) {
|
|
957
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
|
|
958
|
+
const alias = vertex.aliases?.find(a => a.id === removeId && Is.empty(a.dateDeleted));
|
|
959
|
+
if (!Is.empty(alias)) {
|
|
960
|
+
alias.dateDeleted = context.now;
|
|
961
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (Is.arrayValue(patch.add)) {
|
|
966
|
+
for (const alias of patch.add) {
|
|
967
|
+
await this.updateAlias(context, vertex, alias);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
617
971
|
/**
|
|
618
972
|
* Update an alias in the vertex.
|
|
619
973
|
* @param context The context for the operation.
|
|
620
974
|
* @param vertex The vertex.
|
|
621
975
|
* @param alias The alias.
|
|
976
|
+
* @returns A promise that resolves when the alias has been added or updated.
|
|
622
977
|
* @internal
|
|
623
978
|
*/
|
|
624
979
|
async updateAlias(context, vertex, alias) {
|
|
625
980
|
Guards.object(AuditableItemGraphService.CLASS_NAME, "alias", alias);
|
|
626
981
|
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "alias.id", alias.id);
|
|
627
982
|
if (alias.unique ?? false) {
|
|
628
|
-
const existingVertices = await this.findMatchingVertices(
|
|
983
|
+
const existingVertices = await this.findMatchingVertices(vertex.id, alias.id);
|
|
629
984
|
if (existingVertices) {
|
|
630
985
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "aliasNotUnique", {
|
|
631
986
|
aliasId: alias.id
|
|
@@ -646,23 +1001,29 @@ export class AuditableItemGraphService {
|
|
|
646
1001
|
id: alias.id,
|
|
647
1002
|
aliasFormat: alias.aliasFormat,
|
|
648
1003
|
dateCreated: context.now,
|
|
649
|
-
annotationObject: alias.annotationObject
|
|
1004
|
+
annotationObject: alias.annotationObject,
|
|
1005
|
+
unique: alias.unique
|
|
650
1006
|
};
|
|
651
1007
|
vertex.aliases.push(model);
|
|
1008
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesAdded);
|
|
652
1009
|
}
|
|
653
1010
|
else if (existing.aliasFormat !== alias.aliasFormat ||
|
|
654
|
-
!ObjectHelper.equal(existing.annotationObject, alias.annotationObject, false)
|
|
1011
|
+
!ObjectHelper.equal(existing.annotationObject, alias.annotationObject, false) ||
|
|
1012
|
+
existing.unique !== alias.unique) {
|
|
655
1013
|
// Existing alias found, update the annotationObject.
|
|
656
1014
|
existing.dateModified = context.now;
|
|
657
1015
|
existing.aliasFormat = alias.aliasFormat;
|
|
658
1016
|
existing.annotationObject = alias.annotationObject;
|
|
1017
|
+
existing.unique = alias.unique;
|
|
1018
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesModified);
|
|
659
1019
|
}
|
|
660
1020
|
}
|
|
661
1021
|
/**
|
|
662
|
-
*
|
|
1022
|
+
* Replace the resources of a vertex (PUT).
|
|
663
1023
|
* @param context The context for the operation.
|
|
664
1024
|
* @param vertex The vertex.
|
|
665
|
-
* @param resources The
|
|
1025
|
+
* @param resources The new active resource set.
|
|
1026
|
+
* @returns A promise that resolves when the resource list has been replaced.
|
|
666
1027
|
* @internal
|
|
667
1028
|
*/
|
|
668
1029
|
async updateResourceList(context, vertex, resources) {
|
|
@@ -677,11 +1038,11 @@ export class AuditableItemGraphService {
|
|
|
677
1038
|
}
|
|
678
1039
|
}
|
|
679
1040
|
const active = vertex.resources?.filter(r => Is.empty(r.dateDeleted)) ?? [];
|
|
680
|
-
// The active resources that are not in the update list should be marked as deleted.
|
|
681
1041
|
if (Is.arrayValue(active)) {
|
|
682
1042
|
for (const resource of active) {
|
|
683
1043
|
if (!resources?.find(a => this.getResourceId(a) === this.getResourceId(resource))) {
|
|
684
1044
|
resource.dateDeleted = context.now;
|
|
1045
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
|
|
685
1046
|
}
|
|
686
1047
|
}
|
|
687
1048
|
}
|
|
@@ -691,11 +1052,37 @@ export class AuditableItemGraphService {
|
|
|
691
1052
|
}
|
|
692
1053
|
}
|
|
693
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Apply resource patch operations (PATCH).
|
|
1057
|
+
* @param context The context for the operation.
|
|
1058
|
+
* @param vertex The vertex.
|
|
1059
|
+
* @param patch The resource patch.
|
|
1060
|
+
* @returns A promise that resolves when the resource patch has been applied.
|
|
1061
|
+
* @internal
|
|
1062
|
+
*/
|
|
1063
|
+
async applyResourcePatch(context, vertex, patch) {
|
|
1064
|
+
if (Is.arrayValue(patch.remove)) {
|
|
1065
|
+
for (const removeId of patch.remove) {
|
|
1066
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
|
|
1067
|
+
const resource = vertex.resources?.find(r => this.getResourceId(r) === removeId && Is.empty(r.dateDeleted));
|
|
1068
|
+
if (!Is.empty(resource)) {
|
|
1069
|
+
resource.dateDeleted = context.now;
|
|
1070
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (Is.arrayValue(patch.add)) {
|
|
1075
|
+
for (const resource of patch.add) {
|
|
1076
|
+
await this.updateResource(context, vertex, resource);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
694
1080
|
/**
|
|
695
1081
|
* Add a resource to the vertex.
|
|
696
1082
|
* @param context The context for the operation.
|
|
697
1083
|
* @param vertex The vertex.
|
|
698
1084
|
* @param resource The resource.
|
|
1085
|
+
* @returns A promise that resolves when the resource has been added or updated.
|
|
699
1086
|
* @internal
|
|
700
1087
|
*/
|
|
701
1088
|
async updateResource(context, vertex, resource) {
|
|
@@ -716,27 +1103,32 @@ export class AuditableItemGraphService {
|
|
|
716
1103
|
resourceObject: resource.resourceObject
|
|
717
1104
|
};
|
|
718
1105
|
vertex.resources.push(model);
|
|
1106
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesAdded);
|
|
719
1107
|
}
|
|
720
1108
|
else if (!ObjectHelper.equal(existing.resourceObject, resource.resourceObject, false)) {
|
|
721
1109
|
// Existing resource found, update the resourceObject.
|
|
722
1110
|
existing.dateModified = context.now;
|
|
723
1111
|
existing.resourceObject = resource.resourceObject;
|
|
1112
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesModified);
|
|
724
1113
|
}
|
|
725
1114
|
}
|
|
726
1115
|
/**
|
|
727
|
-
*
|
|
1116
|
+
* Replace the edges of a vertex (PUT).
|
|
728
1117
|
* @param context The context for the operation.
|
|
729
1118
|
* @param vertex The vertex.
|
|
730
|
-
* @param edges The
|
|
1119
|
+
* @param edges The new active edge set.
|
|
1120
|
+
* @returns A promise that resolves when the edge list has been replaced.
|
|
731
1121
|
* @internal
|
|
732
1122
|
*/
|
|
733
1123
|
async updateEdgeList(context, vertex, edges) {
|
|
1124
|
+
// `active` shares element refs with `vertex.edges` (the cloned newEntity); mutating `edge` below is intended.
|
|
734
1125
|
const active = vertex.edges?.filter(e => Is.empty(e.dateDeleted)) ?? [];
|
|
735
|
-
// The active edges that are not in the update list should be marked as deleted.
|
|
736
1126
|
if (Is.arrayValue(active)) {
|
|
737
1127
|
for (const edge of active) {
|
|
738
|
-
if (!edges?.find(e =>
|
|
1128
|
+
if (!edges?.find(e => this.edgeMatchesStoredEdge(e, edge.id) ||
|
|
1129
|
+
this.edgeMatchesActiveEdgeByRelationship(e, edge))) {
|
|
739
1130
|
edge.dateDeleted = context.now;
|
|
1131
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
|
|
740
1132
|
}
|
|
741
1133
|
}
|
|
742
1134
|
}
|
|
@@ -746,11 +1138,37 @@ export class AuditableItemGraphService {
|
|
|
746
1138
|
}
|
|
747
1139
|
}
|
|
748
1140
|
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Apply edge patch operations (PATCH).
|
|
1143
|
+
* @param context The context for the operation.
|
|
1144
|
+
* @param vertex The vertex.
|
|
1145
|
+
* @param patch The edge patch.
|
|
1146
|
+
* @returns A promise that resolves when the edge patch has been applied.
|
|
1147
|
+
* @internal
|
|
1148
|
+
*/
|
|
1149
|
+
async applyEdgePatch(context, vertex, patch) {
|
|
1150
|
+
if (Is.arrayValue(patch.remove)) {
|
|
1151
|
+
for (const removeId of patch.remove) {
|
|
1152
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
|
|
1153
|
+
const edge = vertex.edges?.find(e => Is.empty(e.dateDeleted) && this.edgeRemoveIdMatches(e.id, removeId));
|
|
1154
|
+
if (!Is.empty(edge)) {
|
|
1155
|
+
edge.dateDeleted = context.now;
|
|
1156
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (Is.arrayValue(patch.add)) {
|
|
1161
|
+
for (const edge of patch.add) {
|
|
1162
|
+
await this.updateEdge(context, vertex, edge);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
749
1166
|
/**
|
|
750
1167
|
* Add an edge to the vertex.
|
|
751
1168
|
* @param context The context for the operation.
|
|
752
1169
|
* @param vertex The vertex.
|
|
753
1170
|
* @param edge The edge.
|
|
1171
|
+
* @returns A promise that resolves when the edge has been added or updated.
|
|
754
1172
|
* @internal
|
|
755
1173
|
*/
|
|
756
1174
|
async updateEdge(context, vertex, edge) {
|
|
@@ -773,7 +1191,10 @@ export class AuditableItemGraphService {
|
|
|
773
1191
|
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "edge.annotationObject", validationFailures);
|
|
774
1192
|
let findId = Is.stringValue(edge.id) ? this.reduceEdgeId(edge.id) : undefined;
|
|
775
1193
|
if (Is.empty(findId)) {
|
|
776
|
-
|
|
1194
|
+
const existingActive = vertex.edges?.find(e => Is.empty(e.dateDeleted) &&
|
|
1195
|
+
e.targetId === edge.targetId &&
|
|
1196
|
+
ArrayHelper.matches(e.edgeRelationships, edge.edgeRelationships));
|
|
1197
|
+
findId = existingActive?.id ?? RandomHelper.generateUuidV7("compact");
|
|
777
1198
|
}
|
|
778
1199
|
// Try to find an existing edge with the same id.
|
|
779
1200
|
const existing = vertex.edges?.find(r => r.id === findId);
|
|
@@ -788,6 +1209,7 @@ export class AuditableItemGraphService {
|
|
|
788
1209
|
edgeRelationships: edge.edgeRelationships
|
|
789
1210
|
};
|
|
790
1211
|
vertex.edges.push(model);
|
|
1212
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesAdded);
|
|
791
1213
|
}
|
|
792
1214
|
else if (existing.targetId !== edge.targetId ||
|
|
793
1215
|
!ArrayHelper.matches(existing.edgeRelationships, edge.edgeRelationships) ||
|
|
@@ -797,6 +1219,7 @@ export class AuditableItemGraphService {
|
|
|
797
1219
|
existing.dateModified = context.now;
|
|
798
1220
|
existing.edgeRelationships = edge.edgeRelationships;
|
|
799
1221
|
existing.annotationObject = edge.annotationObject;
|
|
1222
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesModified);
|
|
800
1223
|
}
|
|
801
1224
|
}
|
|
802
1225
|
/**
|
|
@@ -805,10 +1228,11 @@ export class AuditableItemGraphService {
|
|
|
805
1228
|
* @param original The original vertex.
|
|
806
1229
|
* @param updated The updated vertex.
|
|
807
1230
|
* @param isNew Whether this is a new item.
|
|
1231
|
+
* @param version The version number of the vertex after this changeset.
|
|
808
1232
|
* @returns True if there were changes.
|
|
809
1233
|
* @internal
|
|
810
1234
|
*/
|
|
811
|
-
async addChangeset(context, original, updated, isNew) {
|
|
1235
|
+
async addChangeset(context, original, updated, isNew, version) {
|
|
812
1236
|
const patches = JsonHelper.diff(original, updated);
|
|
813
1237
|
// If there is a diff set or this is the first time the item is created.
|
|
814
1238
|
if (patches.length > 0 || isNew) {
|
|
@@ -816,16 +1240,24 @@ export class AuditableItemGraphService {
|
|
|
816
1240
|
id: RandomHelper.generateUuidV7("compact"),
|
|
817
1241
|
vertexId: updated.id,
|
|
818
1242
|
dateCreated: context.now,
|
|
819
|
-
userIdentity: context.
|
|
820
|
-
patches
|
|
1243
|
+
userIdentity: context.userIdentity,
|
|
1244
|
+
patches,
|
|
1245
|
+
version
|
|
821
1246
|
};
|
|
822
1247
|
// Create the JSON-LD object we want to use for the proof
|
|
823
1248
|
// this is a subset of fixed properties from the changeset object.
|
|
824
1249
|
const reducedChangesetJsonLd = this.changesetEntityToJsonLd(original.id, ObjectHelper.pick(changesetEntity, AuditableItemGraphService._PROOF_KEYS_CHANGESET));
|
|
825
|
-
// Create the proof for the changeset object
|
|
826
|
-
|
|
827
|
-
//
|
|
1250
|
+
// Create the proof for the changeset object only when the vertex has
|
|
1251
|
+
// an owning organisation. Vertices created by inbound unauthenticated
|
|
1252
|
+
// activities (e.g. DSP push via skipAuth inbox) have no org context at
|
|
1253
|
+
// creation time and must not require a proof until a local org claims
|
|
1254
|
+
// ownership through an authenticated interaction.
|
|
1255
|
+
if (Is.stringValue(updated.organizationIdentity)) {
|
|
1256
|
+
changesetEntity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(reducedChangesetJsonLd));
|
|
1257
|
+
}
|
|
1258
|
+
// Link the storage id to the changeset
|
|
828
1259
|
await this._changesetStorage.set(changesetEntity);
|
|
1260
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ChangesetsCreated);
|
|
829
1261
|
return patches;
|
|
830
1262
|
}
|
|
831
1263
|
return [];
|
|
@@ -834,6 +1266,7 @@ export class AuditableItemGraphService {
|
|
|
834
1266
|
* Verify the changesets of a vertex.
|
|
835
1267
|
* @param vertex The vertex to verify.
|
|
836
1268
|
* @param verifySignatureDepth How many signatures to verify.
|
|
1269
|
+
* @returns The verified flag and list of changesets.
|
|
837
1270
|
* @internal
|
|
838
1271
|
*/
|
|
839
1272
|
async verifyChangesets(vertex, verifySignatureDepth) {
|
|
@@ -850,6 +1283,12 @@ export class AuditableItemGraphService {
|
|
|
850
1283
|
}
|
|
851
1284
|
changesets.push(...chunk.changesets);
|
|
852
1285
|
} while (Is.stringValue(cursor));
|
|
1286
|
+
if (verified) {
|
|
1287
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
|
|
1291
|
+
}
|
|
853
1292
|
return {
|
|
854
1293
|
verified,
|
|
855
1294
|
changesets
|
|
@@ -904,6 +1343,7 @@ export class AuditableItemGraphService {
|
|
|
904
1343
|
* Verify the signature of a changeset and add the verification result to the changeset JSON-LD.
|
|
905
1344
|
* @param storedChangeset The changeset to verify.
|
|
906
1345
|
* @returns Whether the changeset is verified.
|
|
1346
|
+
* @internal
|
|
907
1347
|
*/
|
|
908
1348
|
async verifyChangesetSignature(storedChangeset) {
|
|
909
1349
|
let verification;
|
|
@@ -927,6 +1367,7 @@ export class AuditableItemGraphService {
|
|
|
927
1367
|
* @param resource.id The id of the resource.
|
|
928
1368
|
* @param resource.resourceObject The resource object.
|
|
929
1369
|
* @returns The resource id if it can find one.
|
|
1370
|
+
* @internal
|
|
930
1371
|
*/
|
|
931
1372
|
getResourceId(resource) {
|
|
932
1373
|
return (resource.id ??
|
|
@@ -961,13 +1402,12 @@ export class AuditableItemGraphService {
|
|
|
961
1402
|
}
|
|
962
1403
|
/**
|
|
963
1404
|
* Find vertices with matching aliases.
|
|
964
|
-
* @param context The context for the operation.
|
|
965
1405
|
* @param vertexId The id of the vertex to exclude from the search.
|
|
966
1406
|
* @param aliasId The alias id to try and find.
|
|
967
1407
|
* @returns True if any other vertices have matching aliases.
|
|
968
1408
|
* @internal
|
|
969
1409
|
*/
|
|
970
|
-
async findMatchingVertices(
|
|
1410
|
+
async findMatchingVertices(vertexId, aliasId) {
|
|
971
1411
|
const results = await this._vertexStorage.query({
|
|
972
1412
|
conditions: [
|
|
973
1413
|
{
|
|
@@ -985,6 +1425,47 @@ export class AuditableItemGraphService {
|
|
|
985
1425
|
});
|
|
986
1426
|
return results.entities.length > 0;
|
|
987
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Whether an incoming edge matches a stored edge id.
|
|
1430
|
+
* @param incoming The incoming edge.
|
|
1431
|
+
* @param storedEdgeId The compact stored edge id.
|
|
1432
|
+
* @returns True if the incoming edge matches the stored edge id.
|
|
1433
|
+
* @internal
|
|
1434
|
+
*/
|
|
1435
|
+
edgeMatchesStoredEdge(incoming, storedEdgeId) {
|
|
1436
|
+
return Is.stringValue(incoming.id) && this.reduceEdgeId(incoming.id) === storedEdgeId;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Whether a PATCH remove id matches a stored compact edge id (full URN or compact).
|
|
1440
|
+
* @param storedEdgeId The compact stored edge id.
|
|
1441
|
+
* @param removeId The id from the patch remove list.
|
|
1442
|
+
* @returns True if the remove id identifies the stored edge.
|
|
1443
|
+
* @internal
|
|
1444
|
+
*/
|
|
1445
|
+
edgeRemoveIdMatches(storedEdgeId, removeId) {
|
|
1446
|
+
if (storedEdgeId === removeId) {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
return this.reduceEdgeId(removeId) === storedEdgeId;
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Whether an incoming id-less edge matches an active stored edge by target and relationships.
|
|
1458
|
+
* @param incoming The incoming edge.
|
|
1459
|
+
* @param stored The stored edge.
|
|
1460
|
+
* @returns True if they match.
|
|
1461
|
+
* @internal
|
|
1462
|
+
*/
|
|
1463
|
+
edgeMatchesActiveEdgeByRelationship(incoming, stored) {
|
|
1464
|
+
return (Is.empty(incoming.id) &&
|
|
1465
|
+
Is.empty(stored.dateDeleted) &&
|
|
1466
|
+
incoming.targetId === stored.targetId &&
|
|
1467
|
+
ArrayHelper.matches(incoming.edgeRelationships, stored.edgeRelationships));
|
|
1468
|
+
}
|
|
988
1469
|
/**
|
|
989
1470
|
* Reduce the edge ID from a URN.
|
|
990
1471
|
* @param urn The URN to reduce.
|