@twin.org/auditable-item-graph-service 0.0.3-next.2 → 0.0.3-next.21
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 +609 -44
- package/dist/es/auditableItemGraphRoutes.js.map +1 -1
- package/dist/es/auditableItemGraphService.js +714 -159
- 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 +8 -0
- package/dist/es/entities/auditableItemGraphVertex.js.map +1 -1
- package/dist/es/models/IAuditableItemGraphServiceConstructorOptions.js.map +1 -1
- package/dist/types/auditableItemGraphRoutes.d.ts +42 -2
- package/dist/types/auditableItemGraphService.d.ts +81 -55
- 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 +4 -0
- package/dist/types/models/IAuditableItemGraphServiceConstructorOptions.d.ts +4 -0
- package/docs/changelog.md +370 -71
- package/docs/examples.md +241 -1
- package/docs/open-api/spec.json +1218 -220
- 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 +221 -59
- package/docs/reference/classes/AuditableItemGraphVertex.md +26 -18
- package/docs/reference/functions/auditableItemGraphChangesetGet.md +31 -0
- package/docs/reference/functions/auditableItemGraphChangesetList.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 +5 -0
- package/docs/reference/interfaces/IAuditableItemGraphServiceConstructorOptions.md +18 -10
- package/locales/en.json +7 -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";
|
|
3
|
+
import { AuditableItemGraphContexts, AuditableItemGraphDataTypes, AuditableItemGraphMetricIds, AuditableItemGraphMetrics, AuditableItemGraphTopics, AuditableItemGraphTypes, VerifyDepth } from "@twin.org/auditable-item-graph-models";
|
|
4
4
|
import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
5
|
-
import { ArrayHelper,
|
|
6
|
-
import {
|
|
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,11 @@ export class AuditableItemGraphService {
|
|
|
60
62
|
* @internal
|
|
61
63
|
*/
|
|
62
64
|
_eventBusComponent;
|
|
65
|
+
/**
|
|
66
|
+
* The telemetry component.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
_telemetryComponent;
|
|
63
70
|
/**
|
|
64
71
|
* Create a new instance of AuditableItemGraphService.
|
|
65
72
|
* @param options The dependencies for the auditable item graph connector.
|
|
@@ -68,10 +75,11 @@ export class AuditableItemGraphService {
|
|
|
68
75
|
this._immutableProofComponent = ComponentFactory.get(options?.immutableProofComponentType ?? "immutable-proof");
|
|
69
76
|
this._vertexStorage = EntityStorageConnectorFactory.get(options?.vertexEntityStorageType ?? "auditable-item-graph-vertex");
|
|
70
77
|
this._changesetStorage = EntityStorageConnectorFactory.get(options?.changesetEntityStorageType ?? "auditable-item-graph-changeset");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
78
|
+
this._eventBusComponent = ComponentFactory.getIfExists(options?.eventBusComponentType);
|
|
79
|
+
this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
|
|
74
80
|
SchemaOrgDataTypes.registerRedirects();
|
|
81
|
+
AuditableItemGraphDataTypes.registerTypes();
|
|
82
|
+
JsonLdDataTypes.registerTypes();
|
|
75
83
|
}
|
|
76
84
|
/**
|
|
77
85
|
* Returns the class name of the component.
|
|
@@ -80,6 +88,15 @@ export class AuditableItemGraphService {
|
|
|
80
88
|
className() {
|
|
81
89
|
return AuditableItemGraphService.CLASS_NAME;
|
|
82
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Register all AIG metrics with the telemetry component.
|
|
93
|
+
*/
|
|
94
|
+
async start() {
|
|
95
|
+
if (Is.undefined(this._telemetryComponent)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await MetricHelper.createMetrics(this._telemetryComponent, AuditableItemGraphMetrics);
|
|
99
|
+
}
|
|
83
100
|
/**
|
|
84
101
|
* Create a new graph vertex.
|
|
85
102
|
* @param vertex The vertex to create.
|
|
@@ -93,12 +110,18 @@ export class AuditableItemGraphService {
|
|
|
93
110
|
Guards.object(AuditableItemGraphService.CLASS_NAME, "vertex", vertex);
|
|
94
111
|
const contextIds = await ContextIdStore.getContextIds();
|
|
95
112
|
try {
|
|
113
|
+
const id = RandomHelper.generateUuidV7("compact");
|
|
114
|
+
const schemaValidationFailures = [];
|
|
115
|
+
await DataTypeHelper.validate("vertex", `${AuditableItemGraphContexts.Namespace}${AuditableItemGraphTypes.Vertex}`, {
|
|
116
|
+
...vertex,
|
|
117
|
+
id
|
|
118
|
+
}, schemaValidationFailures);
|
|
119
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex", schemaValidationFailures);
|
|
96
120
|
if (Is.object(vertex.annotationObject)) {
|
|
97
121
|
const validationFailures = [];
|
|
98
122
|
await JsonLdHelper.validate(vertex.annotationObject, validationFailures);
|
|
99
123
|
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
|
|
100
124
|
}
|
|
101
|
-
const id = Converter.bytesToHex(RandomHelper.generate(32), false);
|
|
102
125
|
const context = {
|
|
103
126
|
now: new Date(Date.now()).toISOString(),
|
|
104
127
|
contextIds
|
|
@@ -115,11 +138,13 @@ export class AuditableItemGraphService {
|
|
|
115
138
|
await this.updateEdgeList(context, vertexModel, vertex.edges);
|
|
116
139
|
delete originalEntity.aliasIndex;
|
|
117
140
|
delete originalEntity.resourceTypeIndex;
|
|
118
|
-
await this.addChangeset(context, originalEntity, vertexModel, true);
|
|
141
|
+
await this.addChangeset(context, originalEntity, vertexModel, true, 0);
|
|
142
|
+
vertexModel.version = 0;
|
|
119
143
|
await this._vertexStorage.set({
|
|
120
144
|
...vertexModel,
|
|
121
145
|
...this.buildIndexes(vertexModel)
|
|
122
146
|
});
|
|
147
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesCreated);
|
|
123
148
|
const fullId = new Urn(AuditableItemGraphService.NAMESPACE, id).toString();
|
|
124
149
|
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexCreated, { id: fullId });
|
|
125
150
|
return fullId;
|
|
@@ -128,12 +153,124 @@ export class AuditableItemGraphService {
|
|
|
128
153
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "createFailed", undefined, error);
|
|
129
154
|
}
|
|
130
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Update a graph vertex (PUT — full replacement of vertex state).
|
|
158
|
+
* Concurrent updates for the same vertex are serialized via `Mutex` on the vertex id.
|
|
159
|
+
* @param vertex The vertex to update.
|
|
160
|
+
* @returns Nothing.
|
|
161
|
+
*/
|
|
162
|
+
async update(vertex) {
|
|
163
|
+
Guards.object(AuditableItemGraphService.CLASS_NAME, "vertex", vertex);
|
|
164
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "vertex.id", vertex.id);
|
|
165
|
+
const vertexId = this.parseVertexId(vertex.id);
|
|
166
|
+
await Mutex.lock(vertexId, { throwOnTimeout: true });
|
|
167
|
+
try {
|
|
168
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
169
|
+
try {
|
|
170
|
+
const schemaValidationFailures = [];
|
|
171
|
+
await DataTypeHelper.validate("vertex", `${AuditableItemGraphContexts.Namespace}${AuditableItemGraphTypes.Vertex}`, vertex, schemaValidationFailures);
|
|
172
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex", schemaValidationFailures);
|
|
173
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
174
|
+
if (Is.empty(vertexEntity)) {
|
|
175
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", vertex.id);
|
|
176
|
+
}
|
|
177
|
+
if (Is.object(vertex.annotationObject)) {
|
|
178
|
+
const validationFailures = [];
|
|
179
|
+
await JsonLdHelper.validate(vertex.annotationObject, validationFailures);
|
|
180
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
|
|
181
|
+
}
|
|
182
|
+
const context = {
|
|
183
|
+
now: new Date(Date.now()).toISOString(),
|
|
184
|
+
contextIds
|
|
185
|
+
};
|
|
186
|
+
delete vertexEntity.aliasIndex;
|
|
187
|
+
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
188
|
+
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
189
|
+
// Capture org from context if vertex doesn't have one yet.
|
|
190
|
+
// Enables ownership transition: when an authenticated user first
|
|
191
|
+
// interacts with a vertex that was received without an org, the org
|
|
192
|
+
// is set here and recorded as a patch in the changeset proof.
|
|
193
|
+
if (!Is.stringValue(newEntity.organizationIdentity)) {
|
|
194
|
+
newEntity.organizationIdentity = context.contextIds?.[ContextIdKeys.Organization];
|
|
195
|
+
}
|
|
196
|
+
newEntity.annotationObject = vertex.annotationObject;
|
|
197
|
+
await this.updateAliasList(context, newEntity, vertex.aliases);
|
|
198
|
+
await this.updateResourceList(context, newEntity, vertex.resources);
|
|
199
|
+
await this.updateEdgeList(context, newEntity, vertex.edges);
|
|
200
|
+
await this.persistVertexChanges(context, vertexId, vertex.id, originalEntity, newEntity);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "updatingFailed", undefined, error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
Mutex.unlock(vertexId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Partially update a graph vertex (PATCH — explicit list patches; only defined properties applied).
|
|
212
|
+
* Serialized with `update` via `Mutex` on the same vertex id within this instance.
|
|
213
|
+
* @param partial The partial vertex update.
|
|
214
|
+
* @returns Nothing.
|
|
215
|
+
*/
|
|
216
|
+
async updatePartial(partial) {
|
|
217
|
+
Guards.object(AuditableItemGraphService.CLASS_NAME, "partial", partial);
|
|
218
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "partial.id", partial.id);
|
|
219
|
+
const vertexId = this.parseVertexId(partial.id);
|
|
220
|
+
await Mutex.lock(vertexId, { throwOnTimeout: true });
|
|
221
|
+
try {
|
|
222
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
223
|
+
try {
|
|
224
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
225
|
+
if (Is.empty(vertexEntity)) {
|
|
226
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", partial.id);
|
|
227
|
+
}
|
|
228
|
+
if (partial.annotationObject !== undefined && Is.object(partial.annotationObject)) {
|
|
229
|
+
const validationFailures = [];
|
|
230
|
+
await JsonLdHelper.validate(partial.annotationObject, validationFailures);
|
|
231
|
+
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "partial.annotationObject", validationFailures);
|
|
232
|
+
}
|
|
233
|
+
const context = {
|
|
234
|
+
now: new Date(Date.now()).toISOString(),
|
|
235
|
+
contextIds
|
|
236
|
+
};
|
|
237
|
+
delete vertexEntity.aliasIndex;
|
|
238
|
+
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
239
|
+
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
240
|
+
// Capture org from context if vertex doesn't have one yet.
|
|
241
|
+
if (!Is.stringValue(newEntity.organizationIdentity)) {
|
|
242
|
+
newEntity.organizationIdentity = context.contextIds?.[ContextIdKeys.Organization];
|
|
243
|
+
}
|
|
244
|
+
if (partial.annotationObject !== undefined) {
|
|
245
|
+
newEntity.annotationObject = partial.annotationObject;
|
|
246
|
+
}
|
|
247
|
+
if (partial.aliasPatches !== undefined) {
|
|
248
|
+
const aliasPatch = this.validateListPatch("partial.aliasPatches", partial.aliasPatches);
|
|
249
|
+
await this.applyAliasPatch(context, newEntity, aliasPatch);
|
|
250
|
+
}
|
|
251
|
+
if (partial.resourcePatches !== undefined) {
|
|
252
|
+
const resourcePatch = this.validateListPatch("partial.resourcePatches", partial.resourcePatches);
|
|
253
|
+
await this.applyResourcePatch(context, newEntity, resourcePatch);
|
|
254
|
+
}
|
|
255
|
+
if (partial.edgePatches !== undefined) {
|
|
256
|
+
const edgePatch = this.validateListPatch("partial.edgePatches", partial.edgePatches);
|
|
257
|
+
await this.applyEdgePatch(context, newEntity, edgePatch);
|
|
258
|
+
}
|
|
259
|
+
await this.persistVertexChanges(context, vertexId, partial.id, originalEntity, newEntity);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "updatingFailed", undefined, error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
Mutex.unlock(vertexId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
131
269
|
/**
|
|
132
270
|
* Get a graph vertex.
|
|
133
271
|
* @param id The id of the vertex to get.
|
|
134
272
|
* @param options Additional options for the get operation.
|
|
135
273
|
* @param options.includeDeleted Whether to include deleted/updated aliases, resource, edges, defaults to false.
|
|
136
|
-
* @param options.includeChangesets Whether to include the changesets of the vertex, defaults to false.
|
|
137
274
|
* @param options.verifySignatureDepth How many signatures to verify, defaults to "none".
|
|
138
275
|
* @returns The vertex if found.
|
|
139
276
|
* @throws NotFoundError if the vertex is not found.
|
|
@@ -154,17 +291,13 @@ export class AuditableItemGraphService {
|
|
|
154
291
|
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
155
292
|
}
|
|
156
293
|
const vertexModel = this.vertexEntityToJsonLd(vertexEntity);
|
|
157
|
-
const
|
|
158
|
-
const verifySignatureDepth = options?.verifySignatureDepth ?? "none";
|
|
294
|
+
const verifySignatureDepth = options?.verifySignatureDepth ?? VerifyDepth.None;
|
|
159
295
|
let verified;
|
|
160
|
-
let changesets;
|
|
161
296
|
if (verifySignatureDepth === VerifyDepth.Current ||
|
|
162
|
-
verifySignatureDepth === VerifyDepth.All
|
|
163
|
-
includeChangesets) {
|
|
297
|
+
verifySignatureDepth === VerifyDepth.All) {
|
|
164
298
|
const verifyResult = await this.verifyChangesets(vertexModel, verifySignatureDepth);
|
|
165
299
|
verified = verifyResult.verified;
|
|
166
|
-
|
|
167
|
-
vertexModel["@context"].push(ImmutableProofContexts.Namespace);
|
|
300
|
+
vertexModel["@context"].push(ImmutableProofContexts.Context);
|
|
168
301
|
}
|
|
169
302
|
if (!(options?.includeDeleted ?? false)) {
|
|
170
303
|
if (Is.arrayValue(vertexModel.aliases)) {
|
|
@@ -186,9 +319,6 @@ export class AuditableItemGraphService {
|
|
|
186
319
|
}
|
|
187
320
|
}
|
|
188
321
|
}
|
|
189
|
-
if (includeChangesets) {
|
|
190
|
-
vertexModel.changesets = changesets;
|
|
191
|
-
}
|
|
192
322
|
if (verifySignatureDepth !== VerifyDepth.None) {
|
|
193
323
|
vertexModel.verified = verified;
|
|
194
324
|
}
|
|
@@ -200,70 +330,171 @@ export class AuditableItemGraphService {
|
|
|
200
330
|
}
|
|
201
331
|
}
|
|
202
332
|
/**
|
|
203
|
-
*
|
|
204
|
-
* @param
|
|
205
|
-
* @param
|
|
206
|
-
* @param
|
|
207
|
-
* @param
|
|
208
|
-
* @param
|
|
209
|
-
* @
|
|
210
|
-
* @
|
|
333
|
+
* Get a graph vertex changeset list.
|
|
334
|
+
* @param id The id of the vertex to get.
|
|
335
|
+
* @param cursor The optional cursor to get next chunk.
|
|
336
|
+
* @param limit Limit the number of entities to return.
|
|
337
|
+
* @param options Additional options for the get operation.
|
|
338
|
+
* @param options.verifySignatureDepth How many signatures to verify, defaults to "none".
|
|
339
|
+
* @returns The vertex if found.
|
|
340
|
+
* @throws NotFoundError if the vertex is not found.
|
|
211
341
|
*/
|
|
212
|
-
async
|
|
213
|
-
Guards.
|
|
214
|
-
|
|
215
|
-
const contextIds = await ContextIdStore.getContextIds();
|
|
216
|
-
const urnParsed = Urn.fromValidString(vertex.id);
|
|
342
|
+
async getChangesets(id, cursor, limit, options) {
|
|
343
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
344
|
+
const urnParsed = Urn.fromValidString(id);
|
|
217
345
|
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
218
346
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
|
|
219
347
|
namespace: AuditableItemGraphService.NAMESPACE,
|
|
220
|
-
id
|
|
348
|
+
id
|
|
221
349
|
});
|
|
222
350
|
}
|
|
223
351
|
try {
|
|
224
352
|
const vertexId = urnParsed.namespaceSpecific(0);
|
|
225
353
|
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
226
354
|
if (Is.empty(vertexEntity)) {
|
|
227
|
-
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound",
|
|
355
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
228
356
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
357
|
+
const chunk = await this.verifyChangesetChunk(vertexId, options?.verifySignatureDepth ?? VerifyDepth.None, cursor, limit);
|
|
358
|
+
if ((options?.verifySignatureDepth ?? VerifyDepth.None) !== VerifyDepth.None) {
|
|
359
|
+
if (chunk.verified) {
|
|
360
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
|
|
364
|
+
}
|
|
233
365
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
366
|
+
const changesetList = {
|
|
367
|
+
"@context": [
|
|
368
|
+
SchemaOrgContexts.Context,
|
|
369
|
+
AuditableItemGraphContexts.Context,
|
|
370
|
+
AuditableItemGraphContexts.ContextCommon
|
|
371
|
+
],
|
|
372
|
+
type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.ChangesetList],
|
|
373
|
+
[SchemaOrgTypes.ItemListElement]: chunk.changesets
|
|
374
|
+
};
|
|
375
|
+
const result = await JsonLdProcessor.compact(changesetList, changesetList["@context"]);
|
|
376
|
+
return {
|
|
377
|
+
changesets: result,
|
|
378
|
+
cursor: chunk.cursor
|
|
237
379
|
};
|
|
238
|
-
delete vertexEntity.aliasIndex;
|
|
239
|
-
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
240
|
-
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
241
|
-
newEntity.annotationObject = vertex.annotationObject;
|
|
242
|
-
await this.updateAliasList(context, newEntity, vertex.aliases);
|
|
243
|
-
await this.updateResourceList(context, newEntity, vertex.resources);
|
|
244
|
-
await this.updateEdgeList(context, newEntity, vertex.edges);
|
|
245
|
-
const patches = await this.addChangeset(context, originalEntity, newEntity, false);
|
|
246
|
-
if (patches.length > 0) {
|
|
247
|
-
newEntity.dateModified = context.now;
|
|
248
|
-
const indexes = this.buildIndexes(newEntity);
|
|
249
|
-
await this._vertexStorage.set({
|
|
250
|
-
...newEntity,
|
|
251
|
-
...indexes
|
|
252
|
-
});
|
|
253
|
-
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertex.id, patches });
|
|
254
|
-
}
|
|
255
380
|
}
|
|
256
381
|
catch (error) {
|
|
257
|
-
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "
|
|
382
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
|
|
258
383
|
}
|
|
259
384
|
}
|
|
260
385
|
/**
|
|
261
|
-
*
|
|
386
|
+
* Get a graph vertex changeset.
|
|
262
387
|
* @param id The id of the vertex to get.
|
|
263
|
-
* @
|
|
388
|
+
* @param options Additional options for the get operation.
|
|
389
|
+
* @param options.verifySignatureDepth How many signatures to verify, defaults to "none".
|
|
390
|
+
* @returns The vertex if found.
|
|
264
391
|
* @throws NotFoundError if the vertex is not found.
|
|
265
392
|
*/
|
|
266
|
-
async
|
|
393
|
+
async getChangeset(id, options) {
|
|
394
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
395
|
+
const urnParsed = Urn.fromValidString(id);
|
|
396
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
397
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
|
|
398
|
+
namespace: AuditableItemGraphService.NAMESPACE,
|
|
399
|
+
id
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const namespaceSpecificParts = urnParsed.namespaceSpecificParts();
|
|
404
|
+
const vertexId = namespaceSpecificParts[0];
|
|
405
|
+
const changesetId = namespaceSpecificParts[2];
|
|
406
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
407
|
+
if (Is.empty(vertexEntity)) {
|
|
408
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
409
|
+
}
|
|
410
|
+
const changesetEntity = await this._changesetStorage.get(changesetId);
|
|
411
|
+
if (Is.empty(changesetEntity)) {
|
|
412
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "changesetNotFound", id);
|
|
413
|
+
}
|
|
414
|
+
const changesetModel = this.changesetEntityToJsonLd(vertexId, changesetEntity);
|
|
415
|
+
const verifySignatureDepth = options?.verifySignatureDepth ?? VerifyDepth.None;
|
|
416
|
+
if (verifySignatureDepth !== VerifyDepth.None) {
|
|
417
|
+
changesetModel["@context"]?.push(ImmutableProofContexts.Context);
|
|
418
|
+
changesetModel.verification = await this.verifyChangesetSignature(changesetModel);
|
|
419
|
+
if (changesetModel.verification?.verified) {
|
|
420
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed, {
|
|
424
|
+
failureReason: changesetModel.verification?.failure
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const result = await JsonLdProcessor.compact(changesetModel, changesetModel["@context"]);
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get a graph vertex at a specific version.
|
|
437
|
+
* @param id The id of the vertex.
|
|
438
|
+
* @param version The version number to retrieve.
|
|
439
|
+
* @returns The vertex reconstructed at that version.
|
|
440
|
+
* @throws NotFoundError if the vertex or version is not found.
|
|
441
|
+
*/
|
|
442
|
+
async getVersion(id, version) {
|
|
443
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
444
|
+
Guards.integer(AuditableItemGraphService.CLASS_NAME, "version", version);
|
|
445
|
+
const urnParsed = Urn.fromValidString(id);
|
|
446
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
447
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
|
|
448
|
+
namespace: AuditableItemGraphService.NAMESPACE,
|
|
449
|
+
id
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const vertexId = urnParsed.namespaceSpecific(0);
|
|
454
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
455
|
+
if (Is.empty(vertexEntity)) {
|
|
456
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
457
|
+
}
|
|
458
|
+
const currentVersion = vertexEntity.version ?? 0;
|
|
459
|
+
if (version > currentVersion || version < 0) {
|
|
460
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "versionNotFound", version.toString());
|
|
461
|
+
}
|
|
462
|
+
// Short circuit if requesting the current version to avoid unnecessary changeset retrieval and patching
|
|
463
|
+
if (version === currentVersion) {
|
|
464
|
+
const vertexModel = this.vertexEntityToJsonLd(vertexEntity);
|
|
465
|
+
vertexModel.version = version;
|
|
466
|
+
return await JsonLdProcessor.compact(vertexModel, vertexModel["@context"]);
|
|
467
|
+
}
|
|
468
|
+
const changesets = await this.internalGetChangesets(vertexId, {
|
|
469
|
+
maxVersion: version
|
|
470
|
+
});
|
|
471
|
+
let entityState = {
|
|
472
|
+
id: vertexEntity.id,
|
|
473
|
+
dateCreated: vertexEntity.dateCreated,
|
|
474
|
+
organizationIdentity: vertexEntity.organizationIdentity
|
|
475
|
+
};
|
|
476
|
+
for (const changeset of changesets) {
|
|
477
|
+
entityState = JsonHelper.patch(entityState, changeset.patches);
|
|
478
|
+
}
|
|
479
|
+
const vertexModel = this.vertexEntityToJsonLd(entityState);
|
|
480
|
+
vertexModel.version = version;
|
|
481
|
+
const result = await JsonLdProcessor.compact(vertexModel, vertexModel["@context"]);
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionFailed", undefined, error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get all versions of a graph vertex.
|
|
490
|
+
* @param id The id of the vertex.
|
|
491
|
+
* @param options Additional options for the operation.
|
|
492
|
+
* @param options.after Only return versions created after this ISO 8601 timestamp (exclusive).
|
|
493
|
+
* @param options.before Only return versions created before this ISO 8601 timestamp (exclusive).
|
|
494
|
+
* @returns The list of vertex versions.
|
|
495
|
+
* @throws NotFoundError if the vertex is not found.
|
|
496
|
+
*/
|
|
497
|
+
async getVersions(id, options) {
|
|
267
498
|
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
268
499
|
const urnParsed = Urn.fromValidString(id);
|
|
269
500
|
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
@@ -274,6 +505,52 @@ export class AuditableItemGraphService {
|
|
|
274
505
|
}
|
|
275
506
|
try {
|
|
276
507
|
const vertexId = urnParsed.namespaceSpecific(0);
|
|
508
|
+
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
509
|
+
if (Is.empty(vertexEntity)) {
|
|
510
|
+
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
511
|
+
}
|
|
512
|
+
const beforeDate = Coerce.dateTime(options?.before);
|
|
513
|
+
const afterDate = Coerce.dateTime(options?.after);
|
|
514
|
+
const allChangesets = await this.internalGetChangesets(vertexId, {
|
|
515
|
+
before: beforeDate?.toISOString()
|
|
516
|
+
});
|
|
517
|
+
const versions = [];
|
|
518
|
+
for (const changeset of allChangesets) {
|
|
519
|
+
const changesetDate = Coerce.dateTime(changeset.dateCreated);
|
|
520
|
+
const afterExcluded = !Is.empty(afterDate) && !Is.empty(changesetDate) && changesetDate <= afterDate;
|
|
521
|
+
if (!afterExcluded) {
|
|
522
|
+
versions.push({
|
|
523
|
+
version: changeset.version ?? 0,
|
|
524
|
+
dateCreated: changeset.dateCreated
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const versionList = {
|
|
529
|
+
"@context": [
|
|
530
|
+
SchemaOrgContexts.Context,
|
|
531
|
+
AuditableItemGraphContexts.Context,
|
|
532
|
+
AuditableItemGraphContexts.ContextCommon
|
|
533
|
+
],
|
|
534
|
+
type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexVersionList],
|
|
535
|
+
[SchemaOrgTypes.ItemListElement]: versions
|
|
536
|
+
};
|
|
537
|
+
return await JsonLdProcessor.compact(versionList, versionList["@context"]);
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionsFailed", undefined, error);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Remove the proof for an item.
|
|
545
|
+
* @param id The id of the vertex to remove the proof from.
|
|
546
|
+
* @returns Nothing.
|
|
547
|
+
* @throws NotFoundError if the vertex is not found.
|
|
548
|
+
*/
|
|
549
|
+
async removeProof(id) {
|
|
550
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
|
|
551
|
+
const vertexId = this.parseVertexId(id);
|
|
552
|
+
await Mutex.lock(vertexId, { throwOnTimeout: true });
|
|
553
|
+
try {
|
|
277
554
|
const vertexEntity = await this._vertexStorage.get(vertexId);
|
|
278
555
|
if (Is.empty(vertexEntity)) {
|
|
279
556
|
throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
|
|
@@ -292,7 +569,7 @@ export class AuditableItemGraphService {
|
|
|
292
569
|
], undefined, changesetsResult?.cursor);
|
|
293
570
|
for (const changeset of changesetsResult.entities) {
|
|
294
571
|
if (Is.stringValue(changeset.proofId)) {
|
|
295
|
-
await this._immutableProofComponent.
|
|
572
|
+
await this._immutableProofComponent.removeNotarization(changeset.proofId);
|
|
296
573
|
delete changeset.proofId;
|
|
297
574
|
await this._changesetStorage.set(changeset);
|
|
298
575
|
}
|
|
@@ -300,7 +577,10 @@ export class AuditableItemGraphService {
|
|
|
300
577
|
} while (Is.stringValue(changesetsResult.cursor));
|
|
301
578
|
}
|
|
302
579
|
catch (error) {
|
|
303
|
-
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "
|
|
580
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "removeProofFailed", undefined, error);
|
|
581
|
+
}
|
|
582
|
+
finally {
|
|
583
|
+
Mutex.unlock(vertexId);
|
|
304
584
|
}
|
|
305
585
|
}
|
|
306
586
|
/**
|
|
@@ -309,7 +589,7 @@ export class AuditableItemGraphService {
|
|
|
309
589
|
* @param options.id The optional id to look for.
|
|
310
590
|
* @param options.idMode Look in id, alias or both, defaults to both.
|
|
311
591
|
* @param options.idExact Find only exact matches, default to false meaning partial matching.
|
|
312
|
-
* @param options.
|
|
592
|
+
* @param options.resourceTypes Include vertices with specific resource types.
|
|
313
593
|
* @param conditions Conditions to use in the query.
|
|
314
594
|
* @param orderBy The order for the results, defaults to created.
|
|
315
595
|
* @param orderByDirection The direction for the order, defaults to desc.
|
|
@@ -349,8 +629,8 @@ export class AuditableItemGraphService {
|
|
|
349
629
|
});
|
|
350
630
|
}
|
|
351
631
|
}
|
|
352
|
-
if (Is.arrayValue(options?.
|
|
353
|
-
for (const resourceType of options.
|
|
632
|
+
if (Is.arrayValue(options?.resourceTypes)) {
|
|
633
|
+
for (const resourceType of options.resourceTypes) {
|
|
354
634
|
combinedConditions.push({
|
|
355
635
|
property: "resourceTypeIndex",
|
|
356
636
|
comparison: ComparisonOperator.Includes,
|
|
@@ -375,21 +655,87 @@ export class AuditableItemGraphService {
|
|
|
375
655
|
const models = results.entities.map(e => this.vertexEntityToJsonLd(e));
|
|
376
656
|
const vertexList = {
|
|
377
657
|
"@context": [
|
|
378
|
-
SchemaOrgContexts.
|
|
379
|
-
AuditableItemGraphContexts.
|
|
380
|
-
AuditableItemGraphContexts.
|
|
658
|
+
SchemaOrgContexts.Context,
|
|
659
|
+
AuditableItemGraphContexts.Context,
|
|
660
|
+
AuditableItemGraphContexts.ContextCommon
|
|
381
661
|
],
|
|
382
662
|
type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexList],
|
|
383
|
-
[SchemaOrgTypes.ItemListElement]: models
|
|
384
|
-
[SchemaOrgTypes.NextItem]: results.cursor
|
|
663
|
+
[SchemaOrgTypes.ItemListElement]: models
|
|
385
664
|
};
|
|
386
665
|
const result = await JsonLdProcessor.compact(vertexList, vertexList["@context"]);
|
|
387
|
-
|
|
666
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.QueriesExecuted, {
|
|
667
|
+
resultCount: models.length,
|
|
668
|
+
hasMore: Is.stringValue(results.cursor)
|
|
669
|
+
});
|
|
670
|
+
return {
|
|
671
|
+
entries: result,
|
|
672
|
+
cursor: results.cursor
|
|
673
|
+
};
|
|
388
674
|
}
|
|
389
675
|
catch (error) {
|
|
390
676
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "queryingFailed", undefined, error);
|
|
391
677
|
}
|
|
392
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Parse and validate a vertex URN; return the compact storage id.
|
|
681
|
+
* @param id The vertex URN.
|
|
682
|
+
* @returns The compact vertex id.
|
|
683
|
+
* @internal
|
|
684
|
+
*/
|
|
685
|
+
parseVertexId(id) {
|
|
686
|
+
const urnParsed = Urn.fromValidString(id);
|
|
687
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
|
|
688
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
|
|
689
|
+
namespace: AuditableItemGraphService.NAMESPACE,
|
|
690
|
+
id
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return urnParsed.namespaceSpecific(0);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Validate that a PATCH sub-list value is a list patch object, not a bare array.
|
|
697
|
+
* @param propertyName The property name for error reporting.
|
|
698
|
+
* @param patch The patch value.
|
|
699
|
+
* @returns The validated list patch.
|
|
700
|
+
* @internal
|
|
701
|
+
*/
|
|
702
|
+
validateListPatch(propertyName, patch) {
|
|
703
|
+
if (Is.array(patch)) {
|
|
704
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "listPatchInvalidFormat", {
|
|
705
|
+
property: propertyName
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
Guards.object(AuditableItemGraphService.CLASS_NAME, propertyName, patch);
|
|
709
|
+
return patch;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Persist vertex changes after update or partial update.
|
|
713
|
+
* @param context The context for the operation.
|
|
714
|
+
* @param vertexId The compact vertex id.
|
|
715
|
+
* @param vertexUrn The vertex URN for events.
|
|
716
|
+
* @param originalEntity The entity before changes.
|
|
717
|
+
* @param newEntity The entity after changes.
|
|
718
|
+
* @internal
|
|
719
|
+
*/
|
|
720
|
+
async persistVertexChanges(context, vertexId, vertexUrn, originalEntity, newEntity) {
|
|
721
|
+
const nextVersion = Is.empty(originalEntity.version)
|
|
722
|
+
? (await this.internalGetChangesets(vertexId)).length
|
|
723
|
+
: originalEntity.version + 1;
|
|
724
|
+
const patches = await this.addChangeset(context, originalEntity, newEntity, false, nextVersion);
|
|
725
|
+
if (patches.length > 0) {
|
|
726
|
+
newEntity.dateModified = context.now;
|
|
727
|
+
newEntity.version = nextVersion;
|
|
728
|
+
const indexes = this.buildIndexes(newEntity);
|
|
729
|
+
await this._vertexStorage.set({
|
|
730
|
+
...newEntity,
|
|
731
|
+
...indexes
|
|
732
|
+
});
|
|
733
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
|
|
734
|
+
patchCount: patches.length
|
|
735
|
+
});
|
|
736
|
+
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertexUrn, patches });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
393
739
|
/**
|
|
394
740
|
* Map the vertex entity to JSON-LD.
|
|
395
741
|
* @param vertexEntity The vertex entity.
|
|
@@ -399,9 +745,9 @@ export class AuditableItemGraphService {
|
|
|
399
745
|
vertexEntityToJsonLd(vertexEntity) {
|
|
400
746
|
const model = {
|
|
401
747
|
"@context": [
|
|
402
|
-
AuditableItemGraphContexts.
|
|
403
|
-
AuditableItemGraphContexts.
|
|
404
|
-
SchemaOrgContexts.
|
|
748
|
+
AuditableItemGraphContexts.Context,
|
|
749
|
+
AuditableItemGraphContexts.ContextCommon,
|
|
750
|
+
SchemaOrgContexts.Context
|
|
405
751
|
],
|
|
406
752
|
type: AuditableItemGraphTypes.Vertex,
|
|
407
753
|
id: new Urn(AuditableItemGraphService.NAMESPACE, vertexEntity.id).toString(),
|
|
@@ -415,9 +761,9 @@ export class AuditableItemGraphService {
|
|
|
415
761
|
for (const aliasEntity of vertexEntity.aliases) {
|
|
416
762
|
const aliasModel = {
|
|
417
763
|
"@context": [
|
|
418
|
-
AuditableItemGraphContexts.
|
|
419
|
-
AuditableItemGraphContexts.
|
|
420
|
-
SchemaOrgContexts.
|
|
764
|
+
AuditableItemGraphContexts.Context,
|
|
765
|
+
AuditableItemGraphContexts.ContextCommon,
|
|
766
|
+
SchemaOrgContexts.Context
|
|
421
767
|
],
|
|
422
768
|
type: AuditableItemGraphTypes.Alias,
|
|
423
769
|
id: aliasEntity.id,
|
|
@@ -435,9 +781,9 @@ export class AuditableItemGraphService {
|
|
|
435
781
|
for (const resourceEntity of vertexEntity.resources) {
|
|
436
782
|
const resourceModel = {
|
|
437
783
|
"@context": [
|
|
438
|
-
AuditableItemGraphContexts.
|
|
439
|
-
AuditableItemGraphContexts.
|
|
440
|
-
SchemaOrgContexts.
|
|
784
|
+
AuditableItemGraphContexts.Context,
|
|
785
|
+
AuditableItemGraphContexts.ContextCommon,
|
|
786
|
+
SchemaOrgContexts.Context
|
|
441
787
|
],
|
|
442
788
|
type: AuditableItemGraphTypes.Resource,
|
|
443
789
|
id: resourceEntity.id,
|
|
@@ -454,9 +800,9 @@ export class AuditableItemGraphService {
|
|
|
454
800
|
for (const edgeEntity of vertexEntity.edges) {
|
|
455
801
|
const edgeModel = {
|
|
456
802
|
"@context": [
|
|
457
|
-
AuditableItemGraphContexts.
|
|
458
|
-
AuditableItemGraphContexts.
|
|
459
|
-
SchemaOrgContexts.
|
|
803
|
+
AuditableItemGraphContexts.Context,
|
|
804
|
+
AuditableItemGraphContexts.ContextCommon,
|
|
805
|
+
SchemaOrgContexts.Context
|
|
460
806
|
],
|
|
461
807
|
type: AuditableItemGraphTypes.Edge,
|
|
462
808
|
id: this.fullEdgeId(vertexEntity.id, edgeEntity.id),
|
|
@@ -482,9 +828,9 @@ export class AuditableItemGraphService {
|
|
|
482
828
|
changesetEntityToJsonLd(vertexId, changesetEntity) {
|
|
483
829
|
const model = {
|
|
484
830
|
"@context": [
|
|
485
|
-
AuditableItemGraphContexts.
|
|
486
|
-
AuditableItemGraphContexts.
|
|
487
|
-
SchemaOrgContexts.
|
|
831
|
+
AuditableItemGraphContexts.Context,
|
|
832
|
+
AuditableItemGraphContexts.ContextCommon,
|
|
833
|
+
SchemaOrgContexts.Context
|
|
488
834
|
],
|
|
489
835
|
type: AuditableItemGraphTypes.Changeset,
|
|
490
836
|
id: new Urn(AuditableItemGraphService.NAMESPACE, [
|
|
@@ -496,9 +842,9 @@ export class AuditableItemGraphService {
|
|
|
496
842
|
userIdentity: changesetEntity.userIdentity,
|
|
497
843
|
patches: changesetEntity.patches.map(p => ({
|
|
498
844
|
"@context": [
|
|
499
|
-
AuditableItemGraphContexts.
|
|
500
|
-
AuditableItemGraphContexts.
|
|
501
|
-
SchemaOrgContexts.
|
|
845
|
+
AuditableItemGraphContexts.Context,
|
|
846
|
+
AuditableItemGraphContexts.ContextCommon,
|
|
847
|
+
SchemaOrgContexts.Context
|
|
502
848
|
],
|
|
503
849
|
type: AuditableItemGraphTypes.PatchOperation,
|
|
504
850
|
patchOperation: p.op,
|
|
@@ -506,24 +852,61 @@ export class AuditableItemGraphService {
|
|
|
506
852
|
patchFrom: p.from,
|
|
507
853
|
patchValue: p.value
|
|
508
854
|
})),
|
|
509
|
-
proofId: changesetEntity.proofId
|
|
855
|
+
proofId: changesetEntity.proofId,
|
|
856
|
+
version: changesetEntity.version
|
|
510
857
|
};
|
|
511
858
|
return model;
|
|
512
859
|
}
|
|
513
860
|
/**
|
|
514
|
-
*
|
|
861
|
+
* Fetch all changesets for a vertex in ascending date order.
|
|
862
|
+
* @param vertexId The internal vertex id.
|
|
863
|
+
* @param options Optional filtering options.
|
|
864
|
+
* @param options.before Only fetch changesets created strictly before this ISO 8601 timestamp.
|
|
865
|
+
* @param options.maxVersion Only fetch changesets with version <= this value.
|
|
866
|
+
* @returns All changeset entities sorted ascending by dateCreated.
|
|
867
|
+
* @internal
|
|
868
|
+
*/
|
|
869
|
+
async internalGetChangesets(vertexId, options) {
|
|
870
|
+
const all = [];
|
|
871
|
+
let cursor;
|
|
872
|
+
const conditions = [
|
|
873
|
+
{ property: "vertexId", value: vertexId, comparison: ComparisonOperator.Equals }
|
|
874
|
+
];
|
|
875
|
+
if (Is.stringValue(options?.before)) {
|
|
876
|
+
conditions.push({
|
|
877
|
+
property: "dateCreated",
|
|
878
|
+
value: options.before,
|
|
879
|
+
comparison: ComparisonOperator.LessThan
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (!Is.empty(options?.maxVersion)) {
|
|
883
|
+
conditions.push({
|
|
884
|
+
property: "version",
|
|
885
|
+
value: options.maxVersion,
|
|
886
|
+
comparison: ComparisonOperator.LessThanOrEqual
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
do {
|
|
890
|
+
const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }], undefined, cursor);
|
|
891
|
+
all.push(...result.entities);
|
|
892
|
+
cursor = result.cursor;
|
|
893
|
+
} while (Is.stringValue(cursor));
|
|
894
|
+
return all;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Replace the aliases of a vertex model (PUT).
|
|
515
898
|
* @param context The context for the operation.
|
|
516
899
|
* @param vertex The vertex.
|
|
517
|
-
* @param aliases The
|
|
900
|
+
* @param aliases The new active alias set.
|
|
518
901
|
* @internal
|
|
519
902
|
*/
|
|
520
903
|
async updateAliasList(context, vertex, aliases) {
|
|
521
904
|
const active = vertex.aliases?.filter(a => Is.empty(a.dateDeleted)) ?? [];
|
|
522
|
-
// The active aliases that are not in the update list should be marked as deleted.
|
|
523
905
|
if (Is.arrayValue(active)) {
|
|
524
906
|
for (const alias of active) {
|
|
525
907
|
if (!aliases?.find(a => a.id === alias.id)) {
|
|
526
908
|
alias.dateDeleted = context.now;
|
|
909
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
|
|
527
910
|
}
|
|
528
911
|
}
|
|
529
912
|
}
|
|
@@ -533,6 +916,30 @@ export class AuditableItemGraphService {
|
|
|
533
916
|
}
|
|
534
917
|
}
|
|
535
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Apply alias patch operations (PATCH).
|
|
921
|
+
* @param context The context for the operation.
|
|
922
|
+
* @param vertex The vertex.
|
|
923
|
+
* @param patch The alias patch.
|
|
924
|
+
* @internal
|
|
925
|
+
*/
|
|
926
|
+
async applyAliasPatch(context, vertex, patch) {
|
|
927
|
+
if (Is.arrayValue(patch.remove)) {
|
|
928
|
+
for (const removeId of patch.remove) {
|
|
929
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
|
|
930
|
+
const alias = vertex.aliases?.find(a => a.id === removeId && Is.empty(a.dateDeleted));
|
|
931
|
+
if (!Is.empty(alias)) {
|
|
932
|
+
alias.dateDeleted = context.now;
|
|
933
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (Is.arrayValue(patch.add)) {
|
|
938
|
+
for (const alias of patch.add) {
|
|
939
|
+
await this.updateAlias(context, vertex, alias);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
536
943
|
/**
|
|
537
944
|
* Update an alias in the vertex.
|
|
538
945
|
* @param context The context for the operation.
|
|
@@ -544,7 +951,7 @@ export class AuditableItemGraphService {
|
|
|
544
951
|
Guards.object(AuditableItemGraphService.CLASS_NAME, "alias", alias);
|
|
545
952
|
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "alias.id", alias.id);
|
|
546
953
|
if (alias.unique ?? false) {
|
|
547
|
-
const existingVertices = await this.findMatchingVertices(
|
|
954
|
+
const existingVertices = await this.findMatchingVertices(vertex.id, alias.id);
|
|
548
955
|
if (existingVertices) {
|
|
549
956
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "aliasNotUnique", {
|
|
550
957
|
aliasId: alias.id
|
|
@@ -565,23 +972,28 @@ export class AuditableItemGraphService {
|
|
|
565
972
|
id: alias.id,
|
|
566
973
|
aliasFormat: alias.aliasFormat,
|
|
567
974
|
dateCreated: context.now,
|
|
568
|
-
annotationObject: alias.annotationObject
|
|
975
|
+
annotationObject: alias.annotationObject,
|
|
976
|
+
unique: alias.unique
|
|
569
977
|
};
|
|
570
978
|
vertex.aliases.push(model);
|
|
979
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesAdded);
|
|
571
980
|
}
|
|
572
981
|
else if (existing.aliasFormat !== alias.aliasFormat ||
|
|
573
|
-
!ObjectHelper.equal(existing.annotationObject, alias.annotationObject, false)
|
|
982
|
+
!ObjectHelper.equal(existing.annotationObject, alias.annotationObject, false) ||
|
|
983
|
+
existing.unique !== alias.unique) {
|
|
574
984
|
// Existing alias found, update the annotationObject.
|
|
575
985
|
existing.dateModified = context.now;
|
|
576
986
|
existing.aliasFormat = alias.aliasFormat;
|
|
577
987
|
existing.annotationObject = alias.annotationObject;
|
|
988
|
+
existing.unique = alias.unique;
|
|
989
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesModified);
|
|
578
990
|
}
|
|
579
991
|
}
|
|
580
992
|
/**
|
|
581
|
-
*
|
|
993
|
+
* Replace the resources of a vertex (PUT).
|
|
582
994
|
* @param context The context for the operation.
|
|
583
995
|
* @param vertex The vertex.
|
|
584
|
-
* @param resources The
|
|
996
|
+
* @param resources The new active resource set.
|
|
585
997
|
* @internal
|
|
586
998
|
*/
|
|
587
999
|
async updateResourceList(context, vertex, resources) {
|
|
@@ -596,11 +1008,11 @@ export class AuditableItemGraphService {
|
|
|
596
1008
|
}
|
|
597
1009
|
}
|
|
598
1010
|
const active = vertex.resources?.filter(r => Is.empty(r.dateDeleted)) ?? [];
|
|
599
|
-
// The active resources that are not in the update list should be marked as deleted.
|
|
600
1011
|
if (Is.arrayValue(active)) {
|
|
601
1012
|
for (const resource of active) {
|
|
602
1013
|
if (!resources?.find(a => this.getResourceId(a) === this.getResourceId(resource))) {
|
|
603
1014
|
resource.dateDeleted = context.now;
|
|
1015
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
|
|
604
1016
|
}
|
|
605
1017
|
}
|
|
606
1018
|
}
|
|
@@ -610,6 +1022,30 @@ export class AuditableItemGraphService {
|
|
|
610
1022
|
}
|
|
611
1023
|
}
|
|
612
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Apply resource patch operations (PATCH).
|
|
1027
|
+
* @param context The context for the operation.
|
|
1028
|
+
* @param vertex The vertex.
|
|
1029
|
+
* @param patch The resource patch.
|
|
1030
|
+
* @internal
|
|
1031
|
+
*/
|
|
1032
|
+
async applyResourcePatch(context, vertex, patch) {
|
|
1033
|
+
if (Is.arrayValue(patch.remove)) {
|
|
1034
|
+
for (const removeId of patch.remove) {
|
|
1035
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
|
|
1036
|
+
const resource = vertex.resources?.find(r => this.getResourceId(r) === removeId && Is.empty(r.dateDeleted));
|
|
1037
|
+
if (!Is.empty(resource)) {
|
|
1038
|
+
resource.dateDeleted = context.now;
|
|
1039
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (Is.arrayValue(patch.add)) {
|
|
1044
|
+
for (const resource of patch.add) {
|
|
1045
|
+
await this.updateResource(context, vertex, resource);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
613
1049
|
/**
|
|
614
1050
|
* Add a resource to the vertex.
|
|
615
1051
|
* @param context The context for the operation.
|
|
@@ -635,27 +1071,31 @@ export class AuditableItemGraphService {
|
|
|
635
1071
|
resourceObject: resource.resourceObject
|
|
636
1072
|
};
|
|
637
1073
|
vertex.resources.push(model);
|
|
1074
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesAdded);
|
|
638
1075
|
}
|
|
639
1076
|
else if (!ObjectHelper.equal(existing.resourceObject, resource.resourceObject, false)) {
|
|
640
1077
|
// Existing resource found, update the resourceObject.
|
|
641
1078
|
existing.dateModified = context.now;
|
|
642
1079
|
existing.resourceObject = resource.resourceObject;
|
|
1080
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesModified);
|
|
643
1081
|
}
|
|
644
1082
|
}
|
|
645
1083
|
/**
|
|
646
|
-
*
|
|
1084
|
+
* Replace the edges of a vertex (PUT).
|
|
647
1085
|
* @param context The context for the operation.
|
|
648
1086
|
* @param vertex The vertex.
|
|
649
|
-
* @param edges The
|
|
1087
|
+
* @param edges The new active edge set.
|
|
650
1088
|
* @internal
|
|
651
1089
|
*/
|
|
652
1090
|
async updateEdgeList(context, vertex, edges) {
|
|
1091
|
+
// `active` shares element refs with `vertex.edges` (the cloned newEntity); mutating `edge` below is intended.
|
|
653
1092
|
const active = vertex.edges?.filter(e => Is.empty(e.dateDeleted)) ?? [];
|
|
654
|
-
// The active edges that are not in the update list should be marked as deleted.
|
|
655
1093
|
if (Is.arrayValue(active)) {
|
|
656
1094
|
for (const edge of active) {
|
|
657
|
-
if (!edges?.find(e =>
|
|
1095
|
+
if (!edges?.find(e => this.edgeMatchesStoredEdge(e, edge.id) ||
|
|
1096
|
+
this.edgeMatchesActiveEdgeByRelationship(e, edge))) {
|
|
658
1097
|
edge.dateDeleted = context.now;
|
|
1098
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
|
|
659
1099
|
}
|
|
660
1100
|
}
|
|
661
1101
|
}
|
|
@@ -665,6 +1105,30 @@ export class AuditableItemGraphService {
|
|
|
665
1105
|
}
|
|
666
1106
|
}
|
|
667
1107
|
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Apply edge patch operations (PATCH).
|
|
1110
|
+
* @param context The context for the operation.
|
|
1111
|
+
* @param vertex The vertex.
|
|
1112
|
+
* @param patch The edge patch.
|
|
1113
|
+
* @internal
|
|
1114
|
+
*/
|
|
1115
|
+
async applyEdgePatch(context, vertex, patch) {
|
|
1116
|
+
if (Is.arrayValue(patch.remove)) {
|
|
1117
|
+
for (const removeId of patch.remove) {
|
|
1118
|
+
Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
|
|
1119
|
+
const edge = vertex.edges?.find(e => Is.empty(e.dateDeleted) && this.edgeRemoveIdMatches(e.id, removeId));
|
|
1120
|
+
if (!Is.empty(edge)) {
|
|
1121
|
+
edge.dateDeleted = context.now;
|
|
1122
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (Is.arrayValue(patch.add)) {
|
|
1127
|
+
for (const edge of patch.add) {
|
|
1128
|
+
await this.updateEdge(context, vertex, edge);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
668
1132
|
/**
|
|
669
1133
|
* Add an edge to the vertex.
|
|
670
1134
|
* @param context The context for the operation.
|
|
@@ -692,7 +1156,10 @@ export class AuditableItemGraphService {
|
|
|
692
1156
|
Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "edge.annotationObject", validationFailures);
|
|
693
1157
|
let findId = Is.stringValue(edge.id) ? this.reduceEdgeId(edge.id) : undefined;
|
|
694
1158
|
if (Is.empty(findId)) {
|
|
695
|
-
|
|
1159
|
+
const existingActive = vertex.edges?.find(e => Is.empty(e.dateDeleted) &&
|
|
1160
|
+
e.targetId === edge.targetId &&
|
|
1161
|
+
ArrayHelper.matches(e.edgeRelationships, edge.edgeRelationships));
|
|
1162
|
+
findId = existingActive?.id ?? RandomHelper.generateUuidV7("compact");
|
|
696
1163
|
}
|
|
697
1164
|
// Try to find an existing edge with the same id.
|
|
698
1165
|
const existing = vertex.edges?.find(r => r.id === findId);
|
|
@@ -707,6 +1174,7 @@ export class AuditableItemGraphService {
|
|
|
707
1174
|
edgeRelationships: edge.edgeRelationships
|
|
708
1175
|
};
|
|
709
1176
|
vertex.edges.push(model);
|
|
1177
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesAdded);
|
|
710
1178
|
}
|
|
711
1179
|
else if (existing.targetId !== edge.targetId ||
|
|
712
1180
|
!ArrayHelper.matches(existing.edgeRelationships, edge.edgeRelationships) ||
|
|
@@ -716,6 +1184,7 @@ export class AuditableItemGraphService {
|
|
|
716
1184
|
existing.dateModified = context.now;
|
|
717
1185
|
existing.edgeRelationships = edge.edgeRelationships;
|
|
718
1186
|
existing.annotationObject = edge.annotationObject;
|
|
1187
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesModified);
|
|
719
1188
|
}
|
|
720
1189
|
}
|
|
721
1190
|
/**
|
|
@@ -724,27 +1193,36 @@ export class AuditableItemGraphService {
|
|
|
724
1193
|
* @param original The original vertex.
|
|
725
1194
|
* @param updated The updated vertex.
|
|
726
1195
|
* @param isNew Whether this is a new item.
|
|
1196
|
+
* @param version The version number of the vertex after this changeset.
|
|
727
1197
|
* @returns True if there were changes.
|
|
728
1198
|
* @internal
|
|
729
1199
|
*/
|
|
730
|
-
async addChangeset(context, original, updated, isNew) {
|
|
1200
|
+
async addChangeset(context, original, updated, isNew, version) {
|
|
731
1201
|
const patches = JsonHelper.diff(original, updated);
|
|
732
1202
|
// If there is a diff set or this is the first time the item is created.
|
|
733
1203
|
if (patches.length > 0 || isNew) {
|
|
734
1204
|
const changesetEntity = {
|
|
735
|
-
id:
|
|
1205
|
+
id: RandomHelper.generateUuidV7("compact"),
|
|
736
1206
|
vertexId: updated.id,
|
|
737
1207
|
dateCreated: context.now,
|
|
738
1208
|
userIdentity: context.contextIds?.[ContextIdKeys.User],
|
|
739
|
-
patches
|
|
1209
|
+
patches,
|
|
1210
|
+
version
|
|
740
1211
|
};
|
|
741
1212
|
// Create the JSON-LD object we want to use for the proof
|
|
742
1213
|
// this is a subset of fixed properties from the changeset object.
|
|
743
1214
|
const reducedChangesetJsonLd = this.changesetEntityToJsonLd(original.id, ObjectHelper.pick(changesetEntity, AuditableItemGraphService._PROOF_KEYS_CHANGESET));
|
|
744
|
-
// Create the proof for the changeset object
|
|
745
|
-
|
|
746
|
-
//
|
|
1215
|
+
// Create the proof for the changeset object only when the vertex has
|
|
1216
|
+
// an owning organisation. Vertices created by inbound unauthenticated
|
|
1217
|
+
// activities (e.g. DSP push via skipAuth inbox) have no org context at
|
|
1218
|
+
// creation time and must not require a proof until a local org claims
|
|
1219
|
+
// ownership through an authenticated interaction.
|
|
1220
|
+
if (Is.stringValue(updated.organizationIdentity)) {
|
|
1221
|
+
changesetEntity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(reducedChangesetJsonLd));
|
|
1222
|
+
}
|
|
1223
|
+
// Link the storage id to the changeset
|
|
747
1224
|
await this._changesetStorage.set(changesetEntity);
|
|
1225
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ChangesetsCreated);
|
|
748
1226
|
return patches;
|
|
749
1227
|
}
|
|
750
1228
|
return [];
|
|
@@ -753,63 +1231,99 @@ export class AuditableItemGraphService {
|
|
|
753
1231
|
* Verify the changesets of a vertex.
|
|
754
1232
|
* @param vertex The vertex to verify.
|
|
755
1233
|
* @param verifySignatureDepth How many signatures to verify.
|
|
756
|
-
* @param contextIds The context ids to perform the operation with.
|
|
757
1234
|
* @internal
|
|
758
1235
|
*/
|
|
759
|
-
async verifyChangesets(vertex, verifySignatureDepth
|
|
1236
|
+
async verifyChangesets(vertex, verifySignatureDepth) {
|
|
760
1237
|
const changesets = [];
|
|
761
|
-
let changesetsResult;
|
|
762
1238
|
let verified = true;
|
|
763
|
-
const
|
|
1239
|
+
const vertexIdUrn = Urn.fromValidString(vertex.id);
|
|
1240
|
+
const vertexId = vertexIdUrn.namespaceSpecific();
|
|
1241
|
+
let cursor;
|
|
764
1242
|
do {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}, [
|
|
770
|
-
{
|
|
771
|
-
property: "dateCreated",
|
|
772
|
-
sortDirection: SortDirection.Ascending
|
|
773
|
-
}
|
|
774
|
-
], undefined, changesetsResult?.cursor);
|
|
775
|
-
const storedChangesets = changesetsResult.entities;
|
|
776
|
-
if (Is.arrayValue(storedChangesets)) {
|
|
777
|
-
for (let i = 0; i < storedChangesets.length; i++) {
|
|
778
|
-
const storedChangeset = storedChangesets[i];
|
|
779
|
-
const storedChangesetJsonLd = this.changesetEntityToJsonLd(vertexId.namespaceSpecific(), storedChangeset);
|
|
780
|
-
changesets.push(storedChangesetJsonLd);
|
|
781
|
-
// If we are verifying all signatures
|
|
782
|
-
// or this is the last changeset (cursor is empty)
|
|
783
|
-
// and the changeset has a proofId, then verify the proof.
|
|
784
|
-
if (verifySignatureDepth === VerifyDepth.All ||
|
|
785
|
-
(verifySignatureDepth === VerifyDepth.Current &&
|
|
786
|
-
!Is.stringValue(changesetsResult.cursor) &&
|
|
787
|
-
i === storedChangesets.length - 1)) {
|
|
788
|
-
if (!Is.stringValue(storedChangeset.proofId)) {
|
|
789
|
-
verified = false;
|
|
790
|
-
storedChangesetJsonLd.verification = {
|
|
791
|
-
"@context": ImmutableProofContexts.Namespace,
|
|
792
|
-
type: ImmutableProofTypes.ImmutableProofVerification,
|
|
793
|
-
verified: false,
|
|
794
|
-
failure: ImmutableProofFailure.ProofMissing
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
// Verify the proof for the changeset object
|
|
799
|
-
storedChangesetJsonLd.verification = await this._immutableProofComponent.verify(storedChangeset.proofId);
|
|
800
|
-
if (!storedChangesetJsonLd.verification.verified) {
|
|
801
|
-
verified = false;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
1243
|
+
const chunk = await this.verifyChangesetChunk(vertexId, verifySignatureDepth, cursor);
|
|
1244
|
+
cursor = chunk.cursor;
|
|
1245
|
+
if (!chunk.verified) {
|
|
1246
|
+
verified = false;
|
|
806
1247
|
}
|
|
807
|
-
|
|
1248
|
+
changesets.push(...chunk.changesets);
|
|
1249
|
+
} while (Is.stringValue(cursor));
|
|
1250
|
+
if (verified) {
|
|
1251
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
|
|
1255
|
+
}
|
|
808
1256
|
return {
|
|
809
1257
|
verified,
|
|
810
1258
|
changesets
|
|
811
1259
|
};
|
|
812
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Verify a chunk of changesets for a vertex.
|
|
1263
|
+
* @param vertexId The id of the vertex to verify the changesets for.
|
|
1264
|
+
* @param verifySignatureDepth How many signatures to verify.
|
|
1265
|
+
* @param cursor The cursor to request the next chunk of changesets.
|
|
1266
|
+
* @param limit The maximum number of changesets to verify in this chunk.
|
|
1267
|
+
* @returns The changesets and whether they were verified.
|
|
1268
|
+
* @internal
|
|
1269
|
+
*/
|
|
1270
|
+
async verifyChangesetChunk(vertexId, verifySignatureDepth, cursor, limit) {
|
|
1271
|
+
const changesets = [];
|
|
1272
|
+
let verified = true;
|
|
1273
|
+
const changesetsResult = await this._changesetStorage.query({
|
|
1274
|
+
property: "vertexId",
|
|
1275
|
+
value: vertexId,
|
|
1276
|
+
comparison: ComparisonOperator.Equals
|
|
1277
|
+
}, [
|
|
1278
|
+
{
|
|
1279
|
+
property: "dateCreated",
|
|
1280
|
+
sortDirection: SortDirection.Ascending
|
|
1281
|
+
}
|
|
1282
|
+
], undefined, cursor, limit);
|
|
1283
|
+
const storedChangesets = changesetsResult.entities;
|
|
1284
|
+
if (Is.arrayValue(storedChangesets)) {
|
|
1285
|
+
for (let i = 0; i < storedChangesets.length; i++) {
|
|
1286
|
+
const storedChangeset = storedChangesets[i];
|
|
1287
|
+
const storedChangesetJsonLd = this.changesetEntityToJsonLd(vertexId, storedChangeset);
|
|
1288
|
+
changesets.push(storedChangesetJsonLd);
|
|
1289
|
+
// If we are verifying all signatures
|
|
1290
|
+
// or this is the last changeset (cursor is empty)
|
|
1291
|
+
// and the changeset has a proofId, then verify the proof.
|
|
1292
|
+
if (verifySignatureDepth === VerifyDepth.All ||
|
|
1293
|
+
(verifySignatureDepth === VerifyDepth.Current &&
|
|
1294
|
+
!Is.stringValue(changesetsResult.cursor) &&
|
|
1295
|
+
i === storedChangesets.length - 1)) {
|
|
1296
|
+
storedChangesetJsonLd.verification =
|
|
1297
|
+
await this.verifyChangesetSignature(storedChangesetJsonLd);
|
|
1298
|
+
if (storedChangesetJsonLd.verification?.verified !== true) {
|
|
1299
|
+
verified = false;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return { changesets, verified, cursor: changesetsResult.cursor };
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Verify the signature of a changeset and add the verification result to the changeset JSON-LD.
|
|
1308
|
+
* @param storedChangeset The changeset to verify.
|
|
1309
|
+
* @returns Whether the changeset is verified.
|
|
1310
|
+
*/
|
|
1311
|
+
async verifyChangesetSignature(storedChangeset) {
|
|
1312
|
+
let verification;
|
|
1313
|
+
if (!Is.stringValue(storedChangeset.proofId)) {
|
|
1314
|
+
verification = {
|
|
1315
|
+
"@context": ImmutableProofContexts.Context,
|
|
1316
|
+
type: ImmutableProofTypes.ImmutableProofVerification,
|
|
1317
|
+
verified: false,
|
|
1318
|
+
failure: ImmutableProofFailure.ProofMissing
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
// Verify the proof for the changeset object
|
|
1323
|
+
verification = await this._immutableProofComponent.verify(storedChangeset.proofId);
|
|
1324
|
+
}
|
|
1325
|
+
return verification;
|
|
1326
|
+
}
|
|
813
1327
|
/**
|
|
814
1328
|
* Get the resource id from a resource object.
|
|
815
1329
|
* @param resource The resource.
|
|
@@ -856,7 +1370,7 @@ export class AuditableItemGraphService {
|
|
|
856
1370
|
* @returns True if any other vertices have matching aliases.
|
|
857
1371
|
* @internal
|
|
858
1372
|
*/
|
|
859
|
-
async findMatchingVertices(
|
|
1373
|
+
async findMatchingVertices(vertexId, aliasId) {
|
|
860
1374
|
const results = await this._vertexStorage.query({
|
|
861
1375
|
conditions: [
|
|
862
1376
|
{
|
|
@@ -874,6 +1388,47 @@ export class AuditableItemGraphService {
|
|
|
874
1388
|
});
|
|
875
1389
|
return results.entities.length > 0;
|
|
876
1390
|
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Whether an incoming edge matches a stored edge id.
|
|
1393
|
+
* @param incoming The incoming edge.
|
|
1394
|
+
* @param storedEdgeId The compact stored edge id.
|
|
1395
|
+
* @returns True if the incoming edge matches the stored edge id.
|
|
1396
|
+
* @internal
|
|
1397
|
+
*/
|
|
1398
|
+
edgeMatchesStoredEdge(incoming, storedEdgeId) {
|
|
1399
|
+
return Is.stringValue(incoming.id) && this.reduceEdgeId(incoming.id) === storedEdgeId;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Whether a PATCH remove id matches a stored compact edge id (full URN or compact).
|
|
1403
|
+
* @param storedEdgeId The compact stored edge id.
|
|
1404
|
+
* @param removeId The id from the patch remove list.
|
|
1405
|
+
* @returns True if the remove id identifies the stored edge.
|
|
1406
|
+
* @internal
|
|
1407
|
+
*/
|
|
1408
|
+
edgeRemoveIdMatches(storedEdgeId, removeId) {
|
|
1409
|
+
if (storedEdgeId === removeId) {
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
return this.reduceEdgeId(removeId) === storedEdgeId;
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Whether an incoming id-less edge matches an active stored edge by target and relationships.
|
|
1421
|
+
* @param incoming The incoming edge.
|
|
1422
|
+
* @param stored The stored edge.
|
|
1423
|
+
* @returns True if they match.
|
|
1424
|
+
* @internal
|
|
1425
|
+
*/
|
|
1426
|
+
edgeMatchesActiveEdgeByRelationship(incoming, stored) {
|
|
1427
|
+
return (Is.empty(incoming.id) &&
|
|
1428
|
+
Is.empty(stored.dateDeleted) &&
|
|
1429
|
+
incoming.targetId === stored.targetId &&
|
|
1430
|
+
ArrayHelper.matches(incoming.edgeRelationships, stored.edgeRelationships));
|
|
1431
|
+
}
|
|
877
1432
|
/**
|
|
878
1433
|
* Reduce the edge ID from a URN.
|
|
879
1434
|
* @param urn The URN to reduce.
|