@3sixty/auditable-item-graph-service 0.0.0-stage → 0.20.0-next.0

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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +26 -2
  3. package/dist/es/auditableItemGraphRoutes.js +1136 -0
  4. package/dist/es/auditableItemGraphRoutes.js.map +1 -0
  5. package/dist/es/auditableItemGraphService.js +1913 -0
  6. package/dist/es/auditableItemGraphService.js.map +1 -0
  7. package/dist/es/entities/auditableItemGraphAlias.js +70 -0
  8. package/dist/es/entities/auditableItemGraphAlias.js.map +1 -0
  9. package/dist/es/entities/auditableItemGraphChangeset.js +79 -0
  10. package/dist/es/entities/auditableItemGraphChangeset.js.map +1 -0
  11. package/dist/es/entities/auditableItemGraphEdge.js +70 -0
  12. package/dist/es/entities/auditableItemGraphEdge.js.map +1 -0
  13. package/dist/es/entities/auditableItemGraphPatch.js +45 -0
  14. package/dist/es/entities/auditableItemGraphPatch.js.map +1 -0
  15. package/dist/es/entities/auditableItemGraphResource.js +54 -0
  16. package/dist/es/entities/auditableItemGraphResource.js.map +1 -0
  17. package/dist/es/entities/auditableItemGraphVertex.js +97 -0
  18. package/dist/es/entities/auditableItemGraphVertex.js.map +1 -0
  19. package/dist/es/entities/auditableItemGraphVertexIndex.js +95 -0
  20. package/dist/es/entities/auditableItemGraphVertexIndex.js.map +1 -0
  21. package/dist/es/entities/auditableItemGraphVertexV0.js +107 -0
  22. package/dist/es/entities/auditableItemGraphVertexV0.js.map +1 -0
  23. package/dist/es/entities/auditableItemGraphVertexV1.js +113 -0
  24. package/dist/es/entities/auditableItemGraphVertexV1.js.map +1 -0
  25. package/dist/es/index.js +19 -0
  26. package/dist/es/index.js.map +1 -0
  27. package/dist/es/models/IAuditableItemGraphServiceConfig.js +4 -0
  28. package/dist/es/models/IAuditableItemGraphServiceConfig.js.map +1 -0
  29. package/dist/es/models/IAuditableItemGraphServiceConstructorOptions.js +2 -0
  30. package/dist/es/models/IAuditableItemGraphServiceConstructorOptions.js.map +1 -0
  31. package/dist/es/models/IAuditableItemGraphServiceContext.js +4 -0
  32. package/dist/es/models/IAuditableItemGraphServiceContext.js.map +1 -0
  33. package/dist/es/restEntryPoints.js +13 -0
  34. package/dist/es/restEntryPoints.js.map +1 -0
  35. package/dist/es/schema.js +27 -0
  36. package/dist/es/schema.js.map +1 -0
  37. package/dist/es/utils/auditableItemGraphVertexIndexHelper.js +57 -0
  38. package/dist/es/utils/auditableItemGraphVertexIndexHelper.js.map +1 -0
  39. package/dist/types/auditableItemGraphRoutes.d.ts +94 -0
  40. package/dist/types/auditableItemGraphService.d.ts +164 -0
  41. package/dist/types/entities/auditableItemGraphAlias.d.ts +34 -0
  42. package/dist/types/entities/auditableItemGraphChangeset.d.ts +34 -0
  43. package/dist/types/entities/auditableItemGraphEdge.d.ts +34 -0
  44. package/dist/types/entities/auditableItemGraphPatch.d.ts +21 -0
  45. package/dist/types/entities/auditableItemGraphResource.d.ts +26 -0
  46. package/dist/types/entities/auditableItemGraphVertex.d.ts +50 -0
  47. package/dist/types/entities/auditableItemGraphVertexIndex.d.ts +39 -0
  48. package/dist/types/entities/auditableItemGraphVertexV0.d.ts +53 -0
  49. package/dist/types/entities/auditableItemGraphVertexV1.d.ts +58 -0
  50. package/dist/types/index.d.ts +16 -0
  51. package/dist/types/models/IAuditableItemGraphServiceConfig.d.ts +9 -0
  52. package/dist/types/models/IAuditableItemGraphServiceConstructorOptions.d.ts +38 -0
  53. package/dist/types/models/IAuditableItemGraphServiceContext.d.ts +17 -0
  54. package/dist/types/restEntryPoints.d.ts +5 -0
  55. package/dist/types/schema.d.ts +4 -0
  56. package/dist/types/utils/auditableItemGraphVertexIndexHelper.d.ts +25 -0
  57. package/docs/changelog.md +0 -0
  58. package/docs/examples.md +273 -0
  59. package/docs/open-api/spec.json +2190 -0
  60. package/docs/reference/classes/AuditableItemGraphAlias.md +69 -0
  61. package/docs/reference/classes/AuditableItemGraphChangeset.md +69 -0
  62. package/docs/reference/classes/AuditableItemGraphEdge.md +69 -0
  63. package/docs/reference/classes/AuditableItemGraphPatch.md +45 -0
  64. package/docs/reference/classes/AuditableItemGraphResource.md +53 -0
  65. package/docs/reference/classes/AuditableItemGraphService.md +539 -0
  66. package/docs/reference/classes/AuditableItemGraphVertex.md +93 -0
  67. package/docs/reference/classes/AuditableItemGraphVertexIndex.md +75 -0
  68. package/docs/reference/classes/AuditableItemGraphVertexIndexHelper.md +84 -0
  69. package/docs/reference/classes/AuditableItemGraphVertexV0.md +101 -0
  70. package/docs/reference/classes/AuditableItemGraphVertexV1.md +109 -0
  71. package/docs/reference/functions/auditableItemGraphChangesetGet.md +31 -0
  72. package/docs/reference/functions/auditableItemGraphChangesetList.md +31 -0
  73. package/docs/reference/functions/auditableItemGraphCreate.md +37 -0
  74. package/docs/reference/functions/auditableItemGraphGet.md +31 -0
  75. package/docs/reference/functions/auditableItemGraphList.md +31 -0
  76. package/docs/reference/functions/auditableItemGraphRemoveProof.md +31 -0
  77. package/docs/reference/functions/auditableItemGraphUpdate.md +31 -0
  78. package/docs/reference/functions/auditableItemGraphUpdatePartial.md +31 -0
  79. package/docs/reference/functions/auditableItemGraphVersionGet.md +31 -0
  80. package/docs/reference/functions/auditableItemGraphVersionList.md +31 -0
  81. package/docs/reference/functions/generateRestRoutesAuditableItemGraph.md +25 -0
  82. package/docs/reference/functions/initSchema.md +9 -0
  83. package/docs/reference/index.md +40 -0
  84. package/docs/reference/interfaces/IAuditableItemGraphServiceConfig.md +11 -0
  85. package/docs/reference/interfaces/IAuditableItemGraphServiceConstructorOptions.md +83 -0
  86. package/docs/reference/variables/restEntryPoints.md +5 -0
  87. package/docs/reference/variables/tagsAuditableItemGraph.md +5 -0
  88. package/locales/en.json +34 -0
  89. package/package.json +64 -5
@@ -0,0 +1,1913 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { HealthCategory, HealthStatus } from "@3sixty/api-models";
4
+ import { AuditableItemGraphAuditMode, AuditableItemGraphContexts, AuditableItemGraphDataTypes, AuditableItemGraphMetricIds, AuditableItemGraphMetrics, AuditableItemGraphTopics, AuditableItemGraphTypes, VerifyDepth } from "@3sixty/auditable-item-graph-models";
5
+ import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@3sixty/context";
6
+ import { ArrayHelper, BaseError, Coerce, ComponentFactory, GeneralError, Guards, Is, JsonHelper, Mutex, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@3sixty/core";
7
+ import { DataTypeHelper } from "@3sixty/data-core";
8
+ import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@3sixty/data-json-ld";
9
+ import { ComparisonOperator, LogicalOperator, SortDirection } from "@3sixty/entity";
10
+ import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
11
+ import { ImmutableProofContexts, ImmutableProofFailure, ImmutableProofTypes } from "@3sixty/immutable-proof-models";
12
+ import { SchemaOrgContexts, SchemaOrgDataTypes, SchemaOrgTypes } from "@3sixty/standards-schema-org";
13
+ import { MetricHelper } from "@3sixty/telemetry-models";
14
+ import { AuditableItemGraphVertexIndexHelper } from "./utils/auditableItemGraphVertexIndexHelper.js";
15
+ /**
16
+ * Class for performing auditable item graph operations.
17
+ */
18
+ export class AuditableItemGraphService {
19
+ /**
20
+ * Runtime name for the class.
21
+ */
22
+ static CLASS_NAME = "AuditableItemGraphService";
23
+ /**
24
+ * The namespace for the service.
25
+ * @internal
26
+ */
27
+ static NAMESPACE = "aig";
28
+ /**
29
+ * The namespace for the service changeset.
30
+ */
31
+ static NAMESPACE_CHANGESET = "changeset";
32
+ /**
33
+ * The namespace for the service edge.
34
+ */
35
+ static NAMESPACE_EDGE = "edge";
36
+ /**
37
+ * The index type for the id of the vertex itself.
38
+ * @internal
39
+ */
40
+ static _INDEX_TYPE_VERTEX = "vertex";
41
+ /**
42
+ * The index type for vertex aliases.
43
+ * @internal
44
+ */
45
+ static _INDEX_TYPE_ALIAS = "alias";
46
+ /**
47
+ * The index type for vertex resource types.
48
+ * @internal
49
+ */
50
+ static _INDEX_TYPE_RESOURCE_TYPE = "resourceType";
51
+ /**
52
+ * The keys to pick when creating the proof for the stream.
53
+ * @internal
54
+ */
55
+ static _PROOF_KEYS_CHANGESET = [
56
+ "id",
57
+ "vertexId",
58
+ "userIdentity",
59
+ "dateCreated",
60
+ "patches"
61
+ ];
62
+ /**
63
+ * The immutable proof component.
64
+ * @internal
65
+ */
66
+ _immutableProofComponent;
67
+ /**
68
+ * The entity storage for vertices.
69
+ * @internal
70
+ */
71
+ _vertexStorage;
72
+ /**
73
+ * The entity storage for vertex indices.
74
+ * @internal
75
+ */
76
+ _vertexIndexStorage;
77
+ /**
78
+ * The entity storage for changesets.
79
+ * @internal
80
+ */
81
+ _changesetStorage;
82
+ /**
83
+ * The event bus component.
84
+ * @internal
85
+ */
86
+ _eventBusComponent;
87
+ /**
88
+ * The telemetry component.
89
+ * @internal
90
+ */
91
+ _telemetryComponent;
92
+ /**
93
+ * The timeout in milliseconds when acquiring a mutex lock.
94
+ * @internal
95
+ */
96
+ _mutexTimeoutMs;
97
+ /**
98
+ * Create a new instance of AuditableItemGraphService.
99
+ * @param options The dependencies for the auditable item graph connector.
100
+ */
101
+ constructor(options) {
102
+ this._immutableProofComponent = ComponentFactory.get(options?.immutableProofComponentType ?? "immutable-proof");
103
+ this._vertexStorage = EntityStorageConnectorFactory.get(options?.vertexEntityStorageType ?? "auditable-item-graph-vertex");
104
+ this._vertexIndexStorage = EntityStorageConnectorFactory.get(options?.vertexIndexEntityStorageType ?? "auditable-item-graph-vertex-index");
105
+ this._changesetStorage = EntityStorageConnectorFactory.get(options?.changesetEntityStorageType ?? "auditable-item-graph-changeset");
106
+ this._eventBusComponent = ComponentFactory.getIfExists(options?.eventBusComponentType);
107
+ this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
108
+ this._mutexTimeoutMs = Coerce.integer(options?.config?.mutexTimeoutMs);
109
+ SchemaOrgDataTypes.registerRedirects();
110
+ AuditableItemGraphDataTypes.registerTypes();
111
+ JsonLdDataTypes.registerTypes();
112
+ }
113
+ /**
114
+ * Returns the class name of the component.
115
+ * @returns The class name of the component.
116
+ */
117
+ className() {
118
+ return AuditableItemGraphService.CLASS_NAME;
119
+ }
120
+ /**
121
+ * Runs a set/get/remove cycle against the vertex entity storage using the organisation identity
122
+ * from the current context.
123
+ * @param callback The callback to invoke when a deferred health result is ready.
124
+ * @returns The health status of the service.
125
+ */
126
+ async healthApplication(callback) {
127
+ const contextIds = (await ContextIdStore.getContextIds()) ?? {};
128
+ const orgId = contextIds[ContextIdKeys.Organization];
129
+ if (!Is.stringValue(orgId)) {
130
+ return [];
131
+ }
132
+ try {
133
+ const healthVertex = {
134
+ id: RandomHelper.generateUuidV7("compact"),
135
+ organizationIdentity: orgId,
136
+ dateCreated: new Date().toISOString()
137
+ };
138
+ await this._vertexStorage.set(healthVertex);
139
+ await this._vertexStorage.get(healthVertex.id);
140
+ await this._vertexStorage.remove(healthVertex.id);
141
+ return [
142
+ {
143
+ source: AuditableItemGraphService.CLASS_NAME,
144
+ category: HealthCategory.Application,
145
+ status: HealthStatus.Ok,
146
+ description: "healthDescription"
147
+ }
148
+ ];
149
+ }
150
+ catch (error) {
151
+ return [
152
+ {
153
+ source: AuditableItemGraphService.CLASS_NAME,
154
+ category: HealthCategory.Application,
155
+ status: HealthStatus.Error,
156
+ description: "healthDescription",
157
+ message: "getVertexFailed",
158
+ error: BaseError.fromError(error)
159
+ }
160
+ ];
161
+ }
162
+ }
163
+ /**
164
+ * Register all AIG metrics with the telemetry component.
165
+ * @returns A promise that resolves when all metrics have been registered.
166
+ */
167
+ async start() {
168
+ if (Is.undefined(this._telemetryComponent)) {
169
+ return;
170
+ }
171
+ await MetricHelper.createMetrics(this._telemetryComponent, AuditableItemGraphMetrics);
172
+ }
173
+ /**
174
+ * Create a new graph vertex.
175
+ * @param vertex The vertex to create.
176
+ * @param vertex.auditMode How the mutations of the vertex are recorded, defaults to audited.
177
+ * @param vertex.annotationObject The annotation object for the vertex as JSON-LD.
178
+ * @param vertex.aliases Alternative aliases that can be used to identify the vertex.
179
+ * @param vertex.resources The resources attached to the vertex.
180
+ * @param vertex.edges The edges connected to the vertex.
181
+ * @returns The id of the new graph item.
182
+ */
183
+ async create(vertex) {
184
+ Guards.object(AuditableItemGraphService.CLASS_NAME, "vertex", vertex);
185
+ if (Is.notEmpty(vertex.auditMode)) {
186
+ Guards.arrayOneOf(AuditableItemGraphService.CLASS_NAME, "vertex.auditMode", vertex.auditMode, Object.values(AuditableItemGraphAuditMode));
187
+ }
188
+ const contextIds = await ContextIdStore.getContextIds();
189
+ ContextIdHelper.guard(contextIds, ContextIdKeys.Organization);
190
+ try {
191
+ const id = RandomHelper.generateUuidV7("compact");
192
+ const schemaValidationFailures = [];
193
+ await DataTypeHelper.validate("vertex", `${AuditableItemGraphContexts.Namespace}${AuditableItemGraphTypes.Vertex}`, {
194
+ ...vertex,
195
+ id
196
+ }, schemaValidationFailures);
197
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex", schemaValidationFailures);
198
+ if (Is.object(vertex.annotationObject)) {
199
+ const validationFailures = [];
200
+ await JsonLdHelper.validate(vertex.annotationObject, validationFailures);
201
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
202
+ }
203
+ const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
204
+ const context = {
205
+ now: new Date(Date.now()).toISOString(),
206
+ organizationIdentity: ownerOrganizationId,
207
+ userIdentity: contextIds?.[ContextIdKeys.User]
208
+ };
209
+ const vertexModel = {
210
+ id,
211
+ organizationIdentity: contextIds[ContextIdKeys.Organization],
212
+ dateCreated: context.now
213
+ };
214
+ const originalEntity = ObjectHelper.clone(vertexModel);
215
+ vertexModel.auditMode = vertex.auditMode;
216
+ vertexModel.annotationObject = vertex.annotationObject;
217
+ await this.updateAliasList(context, vertexModel, vertex.aliases);
218
+ await this.updateResourceList(context, vertexModel, vertex.resources);
219
+ await this.updateEdgeList(context, vertexModel, vertex.edges);
220
+ // Bypass vertices keep no changeset history, so there is no baseline version to record.
221
+ if (vertex.auditMode !== AuditableItemGraphAuditMode.Bypass) {
222
+ await this.addChangeset(context, originalEntity, vertexModel, true, 0);
223
+ vertexModel.version = 0;
224
+ }
225
+ await this._vertexStorage.set(vertexModel);
226
+ await this.syncVertexIndexes(vertexModel);
227
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesCreated);
228
+ const fullId = new Urn(AuditableItemGraphService.NAMESPACE, id).toString();
229
+ await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexCreated, { id: fullId });
230
+ return fullId;
231
+ }
232
+ catch (error) {
233
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "createFailed", undefined, error);
234
+ }
235
+ }
236
+ /**
237
+ * Update a graph vertex (PUT - full replacement of vertex state).
238
+ * Concurrent updates for the same vertex are serialized via `Mutex` on the vertex id.
239
+ * @param vertex The vertex to update.
240
+ * @returns A promise that resolves when the vertex has been updated.
241
+ * @throws GeneralError If a bypass vertex is being switched back to audited.
242
+ */
243
+ async update(vertex) {
244
+ Guards.object(AuditableItemGraphService.CLASS_NAME, "vertex", vertex);
245
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "vertex.id", vertex.id);
246
+ if (Is.notEmpty(vertex.auditMode)) {
247
+ Guards.arrayOneOf(AuditableItemGraphService.CLASS_NAME, "vertex.auditMode", vertex.auditMode, Object.values(AuditableItemGraphAuditMode));
248
+ }
249
+ const vertexId = this.parseVertexId(vertex.id);
250
+ await Mutex.lock(vertexId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
251
+ try {
252
+ try {
253
+ const schemaValidationFailures = [];
254
+ await DataTypeHelper.validate("vertex", `${AuditableItemGraphContexts.Namespace}${AuditableItemGraphTypes.Vertex}`, vertex, schemaValidationFailures);
255
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex", schemaValidationFailures);
256
+ const vertexEntity = await this._vertexStorage.get(vertexId);
257
+ if (Is.empty(vertexEntity)) {
258
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", vertex.id);
259
+ }
260
+ if (Is.object(vertex.annotationObject)) {
261
+ const validationFailures = [];
262
+ await JsonLdHelper.validate(vertex.annotationObject, validationFailures);
263
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "vertex.annotationObject", validationFailures);
264
+ }
265
+ const contextIds = await ContextIdStore.getContextIds();
266
+ const ownerOrganizationId = vertexEntity.organizationIdentity ??
267
+ contextIds?.[ContextIdKeys.UserOrganization] ??
268
+ contextIds?.[ContextIdKeys.Organization];
269
+ const context = {
270
+ now: new Date(Date.now()).toISOString(),
271
+ organizationIdentity: ownerOrganizationId,
272
+ userIdentity: contextIds?.[ContextIdKeys.User]
273
+ };
274
+ const auditModeTransition = this.resolveAuditModeTransition(vertexEntity, vertex.auditMode);
275
+ const originalEntity = ObjectHelper.clone(vertexEntity);
276
+ const newEntity = ObjectHelper.clone(vertexEntity);
277
+ newEntity.annotationObject = vertex.annotationObject;
278
+ await this.updateAliasList(context, newEntity, vertex.aliases);
279
+ await this.updateResourceList(context, newEntity, vertex.resources);
280
+ await this.updateEdgeList(context, newEntity, vertex.edges);
281
+ await this.persistVertexChanges(context, vertexId, vertex.id, originalEntity, newEntity, auditModeTransition);
282
+ }
283
+ catch (error) {
284
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "updatingFailed", undefined, error);
285
+ }
286
+ }
287
+ finally {
288
+ Mutex.unlock(vertexId);
289
+ }
290
+ }
291
+ /**
292
+ * Partially update a graph vertex (PATCH - explicit list patches; only defined properties applied).
293
+ * Serialized with `update` via `Mutex` on the same vertex id within this instance.
294
+ * @param partial The partial vertex update.
295
+ * @returns A promise that resolves when the partial update has been applied.
296
+ * @throws GeneralError If a bypass vertex is being switched back to audited.
297
+ */
298
+ async updatePartial(partial) {
299
+ Guards.object(AuditableItemGraphService.CLASS_NAME, "partial", partial);
300
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "partial.id", partial.id);
301
+ if (Is.notEmpty(partial.auditMode)) {
302
+ Guards.arrayOneOf(AuditableItemGraphService.CLASS_NAME, "partial.auditMode", partial.auditMode, Object.values(AuditableItemGraphAuditMode));
303
+ }
304
+ const vertexId = this.parseVertexId(partial.id);
305
+ await Mutex.lock(vertexId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
306
+ try {
307
+ try {
308
+ const vertexEntity = await this._vertexStorage.get(vertexId);
309
+ if (Is.empty(vertexEntity)) {
310
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", partial.id);
311
+ }
312
+ if (partial.annotationObject !== undefined && Is.object(partial.annotationObject)) {
313
+ const validationFailures = [];
314
+ await JsonLdHelper.validate(partial.annotationObject, validationFailures);
315
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "partial.annotationObject", validationFailures);
316
+ }
317
+ const contextIds = await ContextIdStore.getContextIds();
318
+ const ownerOrganizationId = vertexEntity.organizationIdentity ??
319
+ contextIds?.[ContextIdKeys.UserOrganization] ??
320
+ contextIds?.[ContextIdKeys.Organization];
321
+ const context = {
322
+ now: new Date(Date.now()).toISOString(),
323
+ organizationIdentity: ownerOrganizationId,
324
+ userIdentity: contextIds?.[ContextIdKeys.User]
325
+ };
326
+ const auditModeTransition = this.resolveAuditModeTransition(vertexEntity, partial.auditMode);
327
+ const originalEntity = ObjectHelper.clone(vertexEntity);
328
+ const newEntity = ObjectHelper.clone(vertexEntity);
329
+ if (partial.annotationObject !== undefined) {
330
+ newEntity.annotationObject = partial.annotationObject;
331
+ }
332
+ if (partial.aliasPatches !== undefined) {
333
+ const aliasPatch = this.validateListPatch("partial.aliasPatches", partial.aliasPatches);
334
+ await this.applyAliasPatch(context, newEntity, aliasPatch);
335
+ }
336
+ if (partial.resourcePatches !== undefined) {
337
+ const resourcePatch = this.validateListPatch("partial.resourcePatches", partial.resourcePatches);
338
+ await this.applyResourcePatch(context, newEntity, resourcePatch);
339
+ }
340
+ if (partial.edgePatches !== undefined) {
341
+ const edgePatch = this.validateListPatch("partial.edgePatches", partial.edgePatches);
342
+ await this.applyEdgePatch(context, newEntity, edgePatch);
343
+ }
344
+ await this.persistVertexChanges(context, vertexId, partial.id, originalEntity, newEntity, auditModeTransition);
345
+ }
346
+ catch (error) {
347
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "updatingFailed", undefined, error);
348
+ }
349
+ }
350
+ finally {
351
+ Mutex.unlock(vertexId);
352
+ }
353
+ }
354
+ /**
355
+ * Get a graph vertex.
356
+ * @param id The id of the vertex to get.
357
+ * @param options Additional options for the get operation.
358
+ * @param options.includeDeleted Whether to include deleted/updated aliases, resource, edges, defaults to false.
359
+ * @param options.verifySignatureDepth How many signatures to verify, defaults to "none".
360
+ * @returns The vertex if found.
361
+ * @throws NotFoundError if the vertex is not found.
362
+ */
363
+ async get(id, options) {
364
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
365
+ const urnParsed = Urn.fromValidString(id);
366
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
367
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
368
+ namespace: AuditableItemGraphService.NAMESPACE,
369
+ id
370
+ });
371
+ }
372
+ try {
373
+ const vertexId = urnParsed.namespaceSpecific(0);
374
+ const vertexEntity = await this._vertexStorage.get(vertexId);
375
+ if (Is.empty(vertexEntity)) {
376
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
377
+ }
378
+ const vertexModel = this.vertexEntityToJsonLd(vertexEntity);
379
+ // Bypass vertices have no changesets to verify, so verification is never reported for them.
380
+ const verifySignatureDepth = this.isBypass(vertexEntity)
381
+ ? VerifyDepth.None
382
+ : (options?.verifySignatureDepth ?? VerifyDepth.None);
383
+ let verified;
384
+ if (verifySignatureDepth === VerifyDepth.Current ||
385
+ verifySignatureDepth === VerifyDepth.All) {
386
+ const verifyResult = await this.verifyChangesets(vertexModel, verifySignatureDepth);
387
+ verified = verifyResult.verified;
388
+ vertexModel["@context"].push(ImmutableProofContexts.Context);
389
+ }
390
+ if (!(options?.includeDeleted ?? false)) {
391
+ if (Is.arrayValue(vertexModel.aliases)) {
392
+ vertexModel.aliases = vertexModel.aliases.filter(a => Is.undefined(a.dateDeleted));
393
+ if (vertexModel.aliases.length === 0) {
394
+ delete vertexModel.aliases;
395
+ }
396
+ }
397
+ if (Is.arrayValue(vertexModel.resources)) {
398
+ vertexModel.resources = vertexModel.resources.filter(r => Is.undefined(r.dateDeleted));
399
+ if (vertexModel.resources.length === 0) {
400
+ delete vertexModel.resources;
401
+ }
402
+ }
403
+ if (Is.arrayValue(vertexModel.edges)) {
404
+ vertexModel.edges = vertexModel.edges.filter(r => Is.undefined(r.dateDeleted));
405
+ if (vertexModel.edges.length === 0) {
406
+ delete vertexModel.edges;
407
+ }
408
+ }
409
+ }
410
+ if (verifySignatureDepth !== VerifyDepth.None) {
411
+ vertexModel.verified = verified;
412
+ }
413
+ const result = await JsonLdProcessor.compact(vertexModel, vertexModel["@context"], {
414
+ compactArrays: false
415
+ });
416
+ return result;
417
+ }
418
+ catch (error) {
419
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
420
+ }
421
+ }
422
+ /**
423
+ * Get a graph vertex changeset list.
424
+ * @param id The id of the vertex to get.
425
+ * @param cursor The optional cursor to get next chunk.
426
+ * @param limit Limit the number of entities to return.
427
+ * @param options Additional options for the get operation.
428
+ * @param options.verifySignatureDepth How many signatures to verify, defaults to "none".
429
+ * @returns The vertex if found.
430
+ * @throws NotFoundError if the vertex is not found.
431
+ */
432
+ async getChangesets(id, cursor, limit, options) {
433
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
434
+ const urnParsed = Urn.fromValidString(id);
435
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
436
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
437
+ namespace: AuditableItemGraphService.NAMESPACE,
438
+ id
439
+ });
440
+ }
441
+ try {
442
+ const vertexId = urnParsed.namespaceSpecific(0);
443
+ const vertexEntity = await this._vertexStorage.get(vertexId);
444
+ if (Is.empty(vertexEntity)) {
445
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
446
+ }
447
+ // Bypass vertices maintain no changeset history.
448
+ const chunk = this.isBypass(vertexEntity)
449
+ ? { changesets: [], verified: true, cursor: undefined }
450
+ : await this.verifyChangesetChunk(vertexId, options?.verifySignatureDepth ?? VerifyDepth.None, cursor, limit);
451
+ if (!this.isBypass(vertexEntity) &&
452
+ (options?.verifySignatureDepth ?? VerifyDepth.None) !== VerifyDepth.None) {
453
+ if (chunk.verified) {
454
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
455
+ }
456
+ else {
457
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
458
+ }
459
+ }
460
+ const changesetList = {
461
+ "@context": [
462
+ SchemaOrgContexts.Context,
463
+ AuditableItemGraphContexts.Context,
464
+ AuditableItemGraphContexts.ContextCommon
465
+ ],
466
+ type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.ChangesetList],
467
+ [SchemaOrgTypes.ItemListElement]: chunk.changesets
468
+ };
469
+ const result = await JsonLdProcessor.compact(changesetList, changesetList["@context"], {
470
+ compactArrays: false
471
+ });
472
+ return {
473
+ changesets: result,
474
+ cursor: chunk.cursor
475
+ };
476
+ }
477
+ catch (error) {
478
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
479
+ }
480
+ }
481
+ /**
482
+ * Get a graph vertex changeset.
483
+ * @param id The id of the vertex to get.
484
+ * @param options Additional options for the get operation.
485
+ * @param options.verifySignatureDepth How many signatures to verify, defaults to "none".
486
+ * @returns The vertex if found.
487
+ * @throws NotFoundError if the vertex is not found.
488
+ */
489
+ async getChangeset(id, options) {
490
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
491
+ const urnParsed = Urn.fromValidString(id);
492
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
493
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
494
+ namespace: AuditableItemGraphService.NAMESPACE,
495
+ id
496
+ });
497
+ }
498
+ try {
499
+ const namespaceSpecificParts = urnParsed.namespaceSpecificParts();
500
+ const vertexId = namespaceSpecificParts[0];
501
+ const changesetId = namespaceSpecificParts[2];
502
+ const vertexEntity = await this._vertexStorage.get(vertexId);
503
+ if (Is.empty(vertexEntity)) {
504
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
505
+ }
506
+ // Bypass vertices maintain no changeset history.
507
+ if (this.isBypass(vertexEntity)) {
508
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "changesetNotFound", id);
509
+ }
510
+ const changesetEntity = await this._changesetStorage.get(changesetId);
511
+ if (Is.empty(changesetEntity)) {
512
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "changesetNotFound", id);
513
+ }
514
+ const changesetModel = this.changesetEntityToJsonLd(vertexId, changesetEntity);
515
+ const verifySignatureDepth = options?.verifySignatureDepth ?? VerifyDepth.None;
516
+ if (verifySignatureDepth !== VerifyDepth.None) {
517
+ changesetModel["@context"]?.push(ImmutableProofContexts.Context);
518
+ changesetModel.verification = await this.verifyChangesetSignature(changesetModel);
519
+ if (changesetModel.verification?.verified) {
520
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
521
+ }
522
+ else {
523
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed, {
524
+ failureReason: changesetModel.verification?.failure
525
+ });
526
+ }
527
+ }
528
+ const result = await JsonLdProcessor.compact(changesetModel, changesetModel["@context"], {
529
+ compactArrays: false
530
+ });
531
+ return result;
532
+ }
533
+ catch (error) {
534
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
535
+ }
536
+ }
537
+ /**
538
+ * Get a graph vertex at a specific version.
539
+ * @param id The id of the vertex.
540
+ * @param version The version number to retrieve.
541
+ * @returns The vertex reconstructed at that version.
542
+ * @throws NotFoundError if the vertex or version is not found, a bypass vertex never has versions.
543
+ */
544
+ async getVersion(id, version) {
545
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
546
+ Guards.integer(AuditableItemGraphService.CLASS_NAME, "version", version);
547
+ const urnParsed = Urn.fromValidString(id);
548
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
549
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
550
+ namespace: AuditableItemGraphService.NAMESPACE,
551
+ id
552
+ });
553
+ }
554
+ try {
555
+ const vertexId = urnParsed.namespaceSpecific(0);
556
+ const vertexEntity = await this._vertexStorage.get(vertexId);
557
+ if (Is.empty(vertexEntity)) {
558
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
559
+ }
560
+ // Bypass vertices maintain no version chain.
561
+ if (this.isBypass(vertexEntity)) {
562
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "versionNotFound", version.toString());
563
+ }
564
+ const currentVersion = vertexEntity.version ?? 0;
565
+ if (version > currentVersion || version < 0) {
566
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "versionNotFound", version.toString());
567
+ }
568
+ // Short circuit if requesting the current version to avoid unnecessary changeset retrieval and patching
569
+ if (version === currentVersion) {
570
+ const vertexModel = this.vertexEntityToJsonLd(vertexEntity);
571
+ vertexModel.version = version;
572
+ return await JsonLdProcessor.compact(vertexModel, vertexModel["@context"], {
573
+ compactArrays: false
574
+ });
575
+ }
576
+ const changesets = await this.internalGetChangesets(vertexId, ["patches"], {
577
+ maxVersion: version
578
+ });
579
+ let entityState = {
580
+ id: vertexEntity.id,
581
+ dateCreated: vertexEntity.dateCreated,
582
+ organizationIdentity: vertexEntity.organizationIdentity
583
+ };
584
+ for (const changeset of changesets) {
585
+ entityState = JsonHelper.patch(entityState, changeset.patches);
586
+ }
587
+ const vertexModel = this.vertexEntityToJsonLd(entityState);
588
+ vertexModel.version = version;
589
+ const result = await JsonLdProcessor.compact(vertexModel, vertexModel["@context"], {
590
+ compactArrays: false
591
+ });
592
+ return result;
593
+ }
594
+ catch (error) {
595
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionFailed", undefined, error);
596
+ }
597
+ }
598
+ /**
599
+ * Get all versions of a graph vertex.
600
+ * @param id The id of the vertex.
601
+ * @param options Additional options for the operation.
602
+ * @param options.after Only return versions created after this ISO 8601 timestamp (exclusive).
603
+ * @param options.before Only return versions created before this ISO 8601 timestamp (exclusive).
604
+ * @returns The list of vertex versions, a bypass vertex returns a single baseline entry for its
605
+ * current state when that falls within the requested range.
606
+ * @throws NotFoundError if the vertex is not found.
607
+ */
608
+ async getVersions(id, options) {
609
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
610
+ const urnParsed = Urn.fromValidString(id);
611
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
612
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
613
+ namespace: AuditableItemGraphService.NAMESPACE,
614
+ id
615
+ });
616
+ }
617
+ try {
618
+ const vertexId = urnParsed.namespaceSpecific(0);
619
+ const vertexEntity = await this._vertexStorage.get(vertexId);
620
+ if (Is.empty(vertexEntity)) {
621
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
622
+ }
623
+ const beforeDate = Coerce.dateTime(options?.before);
624
+ const afterDate = Coerce.dateTime(options?.after);
625
+ const versions = this.isBypass(vertexEntity)
626
+ ? this.bypassVersions(vertexEntity, afterDate, beforeDate)
627
+ : await this.auditedVersions(vertexId, afterDate, beforeDate);
628
+ const versionList = {
629
+ "@context": [
630
+ SchemaOrgContexts.Context,
631
+ AuditableItemGraphContexts.Context,
632
+ AuditableItemGraphContexts.ContextCommon
633
+ ],
634
+ type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexVersionList],
635
+ [SchemaOrgTypes.ItemListElement]: versions
636
+ };
637
+ return await JsonLdProcessor.compact(versionList, versionList["@context"], {
638
+ compactArrays: false
639
+ });
640
+ }
641
+ catch (error) {
642
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionsFailed", undefined, error);
643
+ }
644
+ }
645
+ /**
646
+ * Remove the proof for an item.
647
+ * @param id The id of the vertex to remove the proof from.
648
+ * @returns A promise that resolves when the proof has been removed from all changesets.
649
+ * @throws NotFoundError if the vertex is not found.
650
+ */
651
+ async removeProof(id) {
652
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
653
+ const vertexId = this.parseVertexId(id);
654
+ await Mutex.lock(vertexId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
655
+ try {
656
+ const vertexEntity = await this._vertexStorage.get(vertexId);
657
+ if (Is.empty(vertexEntity)) {
658
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
659
+ }
660
+ let changesetsResult;
661
+ do {
662
+ changesetsResult = await this._changesetStorage.query({
663
+ property: "vertexId",
664
+ value: vertexId,
665
+ comparison: ComparisonOperator.Equals
666
+ }, [
667
+ {
668
+ property: "dateCreated",
669
+ sortDirection: SortDirection.Ascending
670
+ }
671
+ ], undefined, changesetsResult?.cursor);
672
+ for (const changeset of changesetsResult.entities) {
673
+ if (Is.stringValue(changeset.proofId)) {
674
+ await this._immutableProofComponent.removeNotarization(changeset.proofId);
675
+ delete changeset.proofId;
676
+ await this._changesetStorage.set(changeset);
677
+ }
678
+ }
679
+ } while (Is.stringValue(changesetsResult.cursor));
680
+ }
681
+ catch (error) {
682
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "removeProofFailed", undefined, error);
683
+ }
684
+ finally {
685
+ Mutex.unlock(vertexId);
686
+ }
687
+ }
688
+ /**
689
+ * Query the graph for vertices.
690
+ * @param options The query options.
691
+ * @param options.id The optional id to look for.
692
+ * @param options.idMode Look in id, alias or both, defaults to both.
693
+ * @param options.idExact Find only exact matches, default to false meaning partial matching.
694
+ * @param options.resourceTypes Include vertices with specific resource types.
695
+ * @param conditions Conditions to use in the query.
696
+ * @param orderBy The order for the results, defaults to created.
697
+ * @param orderByDirection The direction for the order, defaults to desc.
698
+ * @param properties The properties to return, if not provided defaults to id, created, aliases and object.
699
+ * @param cursor The cursor to request the next chunk of entities.
700
+ * @param limit The maximum number of entities to return, a page can contain fewer so follow
701
+ * the cursor until it is absent to read them all.
702
+ * @returns The entities, which can be partial if a limited keys list was provided.
703
+ */
704
+ async query(options, conditions, orderBy, orderByDirection, properties, cursor, limit) {
705
+ try {
706
+ const propertiesToReturn = properties ?? [
707
+ "id",
708
+ "dateCreated",
709
+ "dateModified",
710
+ "aliases",
711
+ "annotationObject"
712
+ ];
713
+ const orderProperty = orderBy ?? "dateCreated";
714
+ const orderDirection = orderByDirection ?? SortDirection.Descending;
715
+ const idExact = options?.idExact ?? false;
716
+ const idOrAlias = options?.id;
717
+ const idMode = options?.idMode ?? "both";
718
+ const resourceTypes = options?.resourceTypes;
719
+ const hasIdFilter = Is.stringValue(idOrAlias);
720
+ const hasResourceTypes = Is.arrayValue(resourceTypes);
721
+ if (!propertiesToReturn.includes("id")) {
722
+ propertiesToReturn.unshift("id");
723
+ }
724
+ // Every id, alias and resource type match is held in the index storage. Each filter is
725
+ // one dimension, satisfied by any one of the entries of a vertex, and a vertex has to
726
+ // satisfy every dimension it was given.
727
+ const indexDimensions = [];
728
+ if (hasIdFilter) {
729
+ const comparison = idExact ? ComparisonOperator.Equals : ComparisonOperator.Includes;
730
+ const term = idOrAlias.toLowerCase();
731
+ const idPredicates = [];
732
+ if (idMode === "id" || idMode === "both") {
733
+ idPredicates.push(this.indexTypeValueCondition(AuditableItemGraphService._INDEX_TYPE_VERTEX, comparison, term));
734
+ }
735
+ if (idMode === "alias" || idMode === "both") {
736
+ idPredicates.push(this.indexTypeValueCondition(AuditableItemGraphService._INDEX_TYPE_ALIAS, comparison, term));
737
+ }
738
+ indexDimensions.push(this.anyOfConditions(idPredicates));
739
+ }
740
+ if (hasResourceTypes) {
741
+ indexDimensions.push(this.indexTypeValueCondition(AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE, ComparisonOperator.In, resourceTypes.map(resourceType => resourceType.toLowerCase())));
742
+ }
743
+ if (indexDimensions.length > 0) {
744
+ const indexPage = await this.queryIndexVertices(indexDimensions, orderProperty, orderDirection, propertiesToReturn, conditions, cursor, limit);
745
+ return await this.buildVertexList(indexPage.entities, indexPage.cursor);
746
+ }
747
+ const finalConditions = Is.empty(conditions)
748
+ ? { logicalOperator: LogicalOperator.And, conditions: [] }
749
+ : conditions;
750
+ // With no id, alias or resource type filter there is nothing for the index to answer,
751
+ // so the vertex storage is queried directly and paged by its own cursor.
752
+ const results = await this._vertexStorage.query(finalConditions, [
753
+ {
754
+ property: orderProperty,
755
+ sortDirection: orderDirection
756
+ },
757
+ {
758
+ property: "id",
759
+ sortDirection: SortDirection.Ascending
760
+ }
761
+ ], propertiesToReturn, cursor, limit);
762
+ return await this.buildVertexList(results.entities, results.cursor);
763
+ }
764
+ catch (error) {
765
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "queryingFailed", undefined, error);
766
+ }
767
+ }
768
+ /**
769
+ * Parse and validate a vertex URN; return the compact storage id.
770
+ * @param id The vertex URN.
771
+ * @returns The compact vertex id.
772
+ * @throws GeneralError If the namespace does not match the expected namespace.
773
+ * @internal
774
+ */
775
+ parseVertexId(id) {
776
+ const urnParsed = Urn.fromValidString(id);
777
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
778
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
779
+ namespace: AuditableItemGraphService.NAMESPACE,
780
+ id
781
+ });
782
+ }
783
+ return urnParsed.namespaceSpecific(0);
784
+ }
785
+ /**
786
+ * Validate that a PATCH sub-list value is a list patch object, not a bare array.
787
+ * @param propertyName The property name for error reporting.
788
+ * @param patch The patch value.
789
+ * @returns The validated list patch.
790
+ * @throws GeneralError If the patch value is a bare array instead of a list patch object.
791
+ * @internal
792
+ */
793
+ validateListPatch(propertyName, patch) {
794
+ if (Is.array(patch)) {
795
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "listPatchInvalidFormat", {
796
+ property: propertyName
797
+ });
798
+ }
799
+ Guards.object(AuditableItemGraphService.CLASS_NAME, propertyName, patch);
800
+ return patch;
801
+ }
802
+ /**
803
+ * Resolve the audit mode for an update, bypass is terminal so it can not return to audited.
804
+ * @param vertexEntity The stored vertex entity.
805
+ * @param requestedMode The audit mode requested by the caller, when absent the stored mode is kept.
806
+ * @returns The audit mode to apply and whether the existing history must be compacted.
807
+ * @throws GeneralError If a bypass vertex is being switched back to audited.
808
+ * @internal
809
+ */
810
+ resolveAuditModeTransition(vertexEntity, requestedMode) {
811
+ const currentMode = vertexEntity.auditMode ?? AuditableItemGraphAuditMode.Audited;
812
+ if (Is.empty(requestedMode)) {
813
+ return { auditMode: currentMode, compactHistory: false };
814
+ }
815
+ if (currentMode === AuditableItemGraphAuditMode.Bypass &&
816
+ requestedMode === AuditableItemGraphAuditMode.Audited) {
817
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "auditModeTransitionNotAllowed", {
818
+ currentMode,
819
+ requestedMode
820
+ });
821
+ }
822
+ return {
823
+ auditMode: requestedMode,
824
+ compactHistory: currentMode === AuditableItemGraphAuditMode.Audited &&
825
+ requestedMode === AuditableItemGraphAuditMode.Bypass
826
+ };
827
+ }
828
+ /**
829
+ * Discard the changeset history for a vertex along with any proofs the changesets reference.
830
+ * @param vertexId The compact vertex id.
831
+ * @returns A promise that resolves when the history and its proofs have been removed.
832
+ * @internal
833
+ */
834
+ async compactVertexHistory(vertexId) {
835
+ const changesets = await this.internalGetChangesets(vertexId, ["proofId"]);
836
+ for (const changeset of changesets) {
837
+ await this._changesetStorage.remove(changeset.id);
838
+ if (Is.stringValue(changeset.proofId)) {
839
+ await this._immutableProofComponent.remove(changeset.proofId);
840
+ }
841
+ }
842
+ }
843
+ /**
844
+ * Persist vertex changes after update or partial update.
845
+ * @param context The context for the operation.
846
+ * @param vertexId The compact vertex id.
847
+ * @param vertexUrn The vertex URN for events.
848
+ * @param originalEntity The entity before changes.
849
+ * @param newEntity The entity after changes.
850
+ * @param auditModeTransition The audit mode to apply and whether the history must be compacted.
851
+ * @param auditModeTransition.auditMode The audit mode to apply.
852
+ * @param auditModeTransition.compactHistory Whether the existing history must be discarded.
853
+ * @returns A promise that resolves when the changes have been persisted and events published.
854
+ * @internal
855
+ */
856
+ async persistVertexChanges(context, vertexId, vertexUrn, originalEntity, newEntity, auditModeTransition) {
857
+ if (auditModeTransition.auditMode === AuditableItemGraphAuditMode.Bypass) {
858
+ await this.persistBypassChanges(context, vertexId, vertexUrn, originalEntity, newEntity, auditModeTransition.compactHistory);
859
+ return;
860
+ }
861
+ const nextVersion = Is.empty(originalEntity.version)
862
+ ? (await this.internalGetChangesets(vertexId, [])).length
863
+ : originalEntity.version + 1;
864
+ const patches = await this.addChangeset(context, originalEntity, newEntity, false, nextVersion);
865
+ if (patches.length > 0) {
866
+ newEntity.dateModified = context.now;
867
+ newEntity.version = nextVersion;
868
+ await this._vertexStorage.set(newEntity);
869
+ await this.syncVertexIndexes(newEntity);
870
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
871
+ patchCount: patches.length
872
+ });
873
+ await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertexUrn, patches });
874
+ }
875
+ }
876
+ /**
877
+ * Persist vertex changes for a bypass vertex, overwriting the stored state with no changeset.
878
+ * @param context The context for the operation.
879
+ * @param vertexId The compact vertex id.
880
+ * @param vertexUrn The vertex URN for events.
881
+ * @param originalEntity The entity before changes.
882
+ * @param newEntity The entity after changes.
883
+ * @param compactHistory Whether the vertex is switching from audited and must discard its history.
884
+ * @returns A promise that resolves when the changes have been persisted and events published.
885
+ * @internal
886
+ */
887
+ async persistBypassChanges(context, vertexId, vertexUrn, originalEntity, newEntity, compactHistory) {
888
+ newEntity.auditMode = AuditableItemGraphAuditMode.Bypass;
889
+ delete newEntity.version;
890
+ const patches = JsonHelper.diff(originalEntity, newEntity);
891
+ if (patches.length === 0) {
892
+ return;
893
+ }
894
+ if (compactHistory) {
895
+ await this.compactVertexHistory(vertexId);
896
+ }
897
+ newEntity.dateModified = context.now;
898
+ await this._vertexStorage.set(newEntity);
899
+ await this.syncVertexIndexes(newEntity);
900
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
901
+ patchCount: patches.length
902
+ });
903
+ await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertexUrn, patches });
904
+ }
905
+ /**
906
+ * Whether the vertex records its mutations in place with no audit trail.
907
+ * @param vertexEntity The stored vertex entity.
908
+ * @returns True if the vertex is in bypass mode.
909
+ * @internal
910
+ */
911
+ isBypass(vertexEntity) {
912
+ return vertexEntity.auditMode === AuditableItemGraphAuditMode.Bypass;
913
+ }
914
+ /**
915
+ * Map the vertex entity to JSON-LD.
916
+ * @param vertexEntity The vertex entity.
917
+ * @returns The model.
918
+ * @internal
919
+ */
920
+ vertexEntityToJsonLd(vertexEntity) {
921
+ const model = {
922
+ "@context": [
923
+ AuditableItemGraphContexts.Context,
924
+ AuditableItemGraphContexts.ContextCommon,
925
+ SchemaOrgContexts.Context
926
+ ],
927
+ type: AuditableItemGraphTypes.Vertex,
928
+ id: new Urn(AuditableItemGraphService.NAMESPACE, vertexEntity.id).toString(),
929
+ dateCreated: vertexEntity.dateCreated,
930
+ dateModified: vertexEntity.dateModified,
931
+ organizationIdentity: vertexEntity.organizationIdentity,
932
+ auditMode: vertexEntity.auditMode,
933
+ annotationObject: vertexEntity.annotationObject
934
+ };
935
+ if (Is.arrayValue(vertexEntity.aliases)) {
936
+ model.aliases ??= [];
937
+ for (const aliasEntity of vertexEntity.aliases) {
938
+ const aliasModel = {
939
+ "@context": [
940
+ AuditableItemGraphContexts.Context,
941
+ AuditableItemGraphContexts.ContextCommon,
942
+ SchemaOrgContexts.Context
943
+ ],
944
+ type: AuditableItemGraphTypes.Alias,
945
+ id: aliasEntity.id,
946
+ aliasFormat: aliasEntity.aliasFormat,
947
+ dateCreated: aliasEntity.dateCreated,
948
+ dateModified: aliasEntity.dateModified,
949
+ dateDeleted: aliasEntity.dateDeleted,
950
+ annotationObject: aliasEntity.annotationObject
951
+ };
952
+ model.aliases.push(aliasModel);
953
+ }
954
+ }
955
+ if (Is.arrayValue(vertexEntity.resources)) {
956
+ model.resources ??= [];
957
+ for (const resourceEntity of vertexEntity.resources) {
958
+ const resourceModel = {
959
+ "@context": [
960
+ AuditableItemGraphContexts.Context,
961
+ AuditableItemGraphContexts.ContextCommon,
962
+ SchemaOrgContexts.Context
963
+ ],
964
+ type: AuditableItemGraphTypes.Resource,
965
+ id: resourceEntity.id,
966
+ dateCreated: resourceEntity.dateCreated,
967
+ dateModified: resourceEntity.dateModified,
968
+ dateDeleted: resourceEntity.dateDeleted,
969
+ resourceObject: resourceEntity.resourceObject
970
+ };
971
+ model.resources.push(resourceModel);
972
+ }
973
+ }
974
+ if (Is.arrayValue(vertexEntity.edges)) {
975
+ model.edges ??= [];
976
+ for (const edgeEntity of vertexEntity.edges) {
977
+ const edgeModel = {
978
+ "@context": [
979
+ AuditableItemGraphContexts.Context,
980
+ AuditableItemGraphContexts.ContextCommon,
981
+ SchemaOrgContexts.Context
982
+ ],
983
+ type: AuditableItemGraphTypes.Edge,
984
+ id: this.fullEdgeId(vertexEntity.id, edgeEntity.id),
985
+ targetId: edgeEntity.targetId,
986
+ dateCreated: edgeEntity.dateCreated,
987
+ dateModified: edgeEntity.dateModified,
988
+ dateDeleted: edgeEntity.dateDeleted,
989
+ edgeRelationships: edgeEntity.edgeRelationships,
990
+ annotationObject: edgeEntity.annotationObject
991
+ };
992
+ model.edges.push(edgeModel);
993
+ }
994
+ }
995
+ return model;
996
+ }
997
+ /**
998
+ * Map the changeset entity to a JSON-LD.
999
+ * @param vertexId The id of the vertex the changeset belongs to.
1000
+ * @param changesetEntity The changeset entity.
1001
+ * @returns The model.
1002
+ * @internal
1003
+ */
1004
+ changesetEntityToJsonLd(vertexId, changesetEntity) {
1005
+ const model = {
1006
+ "@context": [
1007
+ AuditableItemGraphContexts.Context,
1008
+ AuditableItemGraphContexts.ContextCommon,
1009
+ SchemaOrgContexts.Context
1010
+ ],
1011
+ type: AuditableItemGraphTypes.Changeset,
1012
+ id: new Urn(AuditableItemGraphService.NAMESPACE, [
1013
+ vertexId,
1014
+ AuditableItemGraphService.NAMESPACE_CHANGESET,
1015
+ changesetEntity.id
1016
+ ]).toString(),
1017
+ dateCreated: changesetEntity.dateCreated,
1018
+ userIdentity: changesetEntity.userIdentity,
1019
+ patches: changesetEntity.patches.map(p => ({
1020
+ "@context": [
1021
+ AuditableItemGraphContexts.Context,
1022
+ AuditableItemGraphContexts.ContextCommon,
1023
+ SchemaOrgContexts.Context
1024
+ ],
1025
+ type: AuditableItemGraphTypes.PatchOperation,
1026
+ patchOperation: p.op,
1027
+ patchPath: p.path,
1028
+ patchFrom: p.from,
1029
+ patchValue: p.value
1030
+ })),
1031
+ proofId: changesetEntity.proofId,
1032
+ version: changesetEntity.version
1033
+ };
1034
+ return model;
1035
+ }
1036
+ /**
1037
+ * Fetch all changesets for a vertex in ascending date order.
1038
+ * @param vertexId The internal vertex id.
1039
+ * @param properties The additional changeset properties to retrieve, the id and dateCreated sort
1040
+ * keys are always included. Only the requested properties are populated on the returned
1041
+ * entities, so the large patches payload is only transferred when a caller reads it.
1042
+ * @param options Optional filtering options.
1043
+ * @param options.before Only fetch changesets created strictly before this ISO 8601 timestamp.
1044
+ * @param options.maxVersion Only fetch changesets with version <= this value.
1045
+ * @returns All changeset entities sorted ascending by dateCreated.
1046
+ * @internal
1047
+ */
1048
+ async internalGetChangesets(vertexId, properties, options) {
1049
+ const all = [];
1050
+ let cursor;
1051
+ const propertiesToReturn = ["id", "dateCreated"];
1052
+ for (const property of properties) {
1053
+ if (!propertiesToReturn.includes(property)) {
1054
+ propertiesToReturn.push(property);
1055
+ }
1056
+ }
1057
+ const conditions = [
1058
+ { property: "vertexId", value: vertexId, comparison: ComparisonOperator.Equals }
1059
+ ];
1060
+ if (Is.stringValue(options?.before)) {
1061
+ conditions.push({
1062
+ property: "dateCreated",
1063
+ value: options.before,
1064
+ comparison: ComparisonOperator.LessThan
1065
+ });
1066
+ }
1067
+ if (!Is.empty(options?.maxVersion)) {
1068
+ conditions.push({
1069
+ property: "version",
1070
+ value: options.maxVersion,
1071
+ comparison: ComparisonOperator.LessThanOrEqual
1072
+ });
1073
+ }
1074
+ do {
1075
+ const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }], propertiesToReturn, cursor);
1076
+ all.push(...result.entities);
1077
+ cursor = result.cursor;
1078
+ } while (Is.stringValue(cursor));
1079
+ return all;
1080
+ }
1081
+ /**
1082
+ * Build the version list for an audited vertex from its changesets.
1083
+ * @param vertexId The compact vertex id.
1084
+ * @param afterDate Only include versions created after this date, exclusive.
1085
+ * @param beforeDate Only include versions created before this date, exclusive.
1086
+ * @returns The matching versions in ascending order.
1087
+ * @internal
1088
+ */
1089
+ async auditedVersions(vertexId, afterDate, beforeDate) {
1090
+ const allChangesets = await this.internalGetChangesets(vertexId, ["version"], {
1091
+ before: beforeDate?.toISOString()
1092
+ });
1093
+ const versions = [];
1094
+ for (const changeset of allChangesets) {
1095
+ const changesetDate = Coerce.dateTime(changeset.dateCreated);
1096
+ const afterExcluded = !Is.empty(afterDate) && !Is.empty(changesetDate) && changesetDate <= afterDate;
1097
+ if (!afterExcluded) {
1098
+ versions.push({
1099
+ version: changeset.version ?? 0,
1100
+ dateCreated: changeset.dateCreated
1101
+ });
1102
+ }
1103
+ }
1104
+ return versions;
1105
+ }
1106
+ /**
1107
+ * Build the version list for a bypass vertex, which only exposes its current state as
1108
+ * a single baseline entry numbered zero.
1109
+ * @param vertexEntity The stored vertex entity.
1110
+ * @param afterDate Only include the state when it was modified after this date, exclusive.
1111
+ * @param beforeDate Only include the state when it was modified before this date, exclusive.
1112
+ * @returns The baseline entry when the state falls within the range, otherwise empty.
1113
+ * @internal
1114
+ */
1115
+ bypassVersions(vertexEntity, afterDate, beforeDate) {
1116
+ const stateDate = vertexEntity.dateModified ?? vertexEntity.dateCreated;
1117
+ const stateDateTime = Coerce.dateTime(stateDate);
1118
+ if (!Is.empty(stateDateTime)) {
1119
+ if (!Is.empty(beforeDate) && stateDateTime >= beforeDate) {
1120
+ return [];
1121
+ }
1122
+ if (!Is.empty(afterDate) && stateDateTime <= afterDate) {
1123
+ return [];
1124
+ }
1125
+ }
1126
+ return [{ version: 0, dateCreated: stateDate }];
1127
+ }
1128
+ /**
1129
+ * Replace the aliases of a vertex model (PUT).
1130
+ * @param context The context for the operation.
1131
+ * @param vertex The vertex.
1132
+ * @param aliases The new active alias set.
1133
+ * @returns A promise that resolves when the alias list has been replaced.
1134
+ * @internal
1135
+ */
1136
+ async updateAliasList(context, vertex, aliases) {
1137
+ const active = vertex.aliases?.filter(a => Is.empty(a.dateDeleted)) ?? [];
1138
+ if (Is.arrayValue(active)) {
1139
+ for (const alias of active) {
1140
+ if (!aliases?.find(a => a.id === alias.id)) {
1141
+ alias.dateDeleted = context.now;
1142
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
1143
+ }
1144
+ }
1145
+ }
1146
+ if (Is.arrayValue(aliases)) {
1147
+ for (const alias of aliases) {
1148
+ await this.updateAlias(context, vertex, alias);
1149
+ }
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Apply alias patch operations (PATCH).
1154
+ * @param context The context for the operation.
1155
+ * @param vertex The vertex.
1156
+ * @param patch The alias patch.
1157
+ * @returns A promise that resolves when the alias patch has been applied.
1158
+ * @internal
1159
+ */
1160
+ async applyAliasPatch(context, vertex, patch) {
1161
+ if (Is.arrayValue(patch.remove)) {
1162
+ for (const removeId of patch.remove) {
1163
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
1164
+ const alias = vertex.aliases?.find(a => a.id === removeId && Is.empty(a.dateDeleted));
1165
+ if (!Is.empty(alias)) {
1166
+ alias.dateDeleted = context.now;
1167
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
1168
+ }
1169
+ }
1170
+ }
1171
+ if (Is.arrayValue(patch.add)) {
1172
+ for (const alias of patch.add) {
1173
+ await this.updateAlias(context, vertex, alias);
1174
+ }
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Update an alias in the vertex.
1179
+ * @param context The context for the operation.
1180
+ * @param vertex The vertex.
1181
+ * @param alias The alias.
1182
+ * @returns A promise that resolves when the alias has been added or updated.
1183
+ * @internal
1184
+ */
1185
+ async updateAlias(context, vertex, alias) {
1186
+ Guards.object(AuditableItemGraphService.CLASS_NAME, "alias", alias);
1187
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "alias.id", alias.id);
1188
+ if (alias.unique ?? false) {
1189
+ const existingVertices = await this.findMatchingVertices(vertex.id, alias.id);
1190
+ if (existingVertices) {
1191
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "aliasNotUnique", {
1192
+ aliasId: alias.id
1193
+ });
1194
+ }
1195
+ }
1196
+ if (Is.object(alias.annotationObject)) {
1197
+ const validationFailures = [];
1198
+ await JsonLdHelper.validate(alias.annotationObject, validationFailures);
1199
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "alias.annotationObject", validationFailures);
1200
+ }
1201
+ // Try to find an existing alias with the same id.
1202
+ const existing = vertex.aliases?.find(a => a.id === alias.id);
1203
+ if (Is.empty(existing) || !Is.empty(existing?.dateDeleted)) {
1204
+ // Did not find a matching item, or found one which is deleted.
1205
+ vertex.aliases ??= [];
1206
+ const model = {
1207
+ id: alias.id,
1208
+ aliasFormat: alias.aliasFormat,
1209
+ dateCreated: context.now,
1210
+ annotationObject: alias.annotationObject,
1211
+ unique: alias.unique
1212
+ };
1213
+ vertex.aliases.push(model);
1214
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesAdded);
1215
+ }
1216
+ else if (existing.aliasFormat !== alias.aliasFormat ||
1217
+ !ObjectHelper.equal(existing.annotationObject, alias.annotationObject, false) ||
1218
+ existing.unique !== alias.unique) {
1219
+ // Existing alias found, update the annotationObject.
1220
+ existing.dateModified = context.now;
1221
+ existing.aliasFormat = alias.aliasFormat;
1222
+ existing.annotationObject = alias.annotationObject;
1223
+ existing.unique = alias.unique;
1224
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesModified);
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Replace the resources of a vertex (PUT).
1229
+ * @param context The context for the operation.
1230
+ * @param vertex The vertex.
1231
+ * @param resources The new active resource set.
1232
+ * @returns A promise that resolves when the resource list has been replaced.
1233
+ * @internal
1234
+ */
1235
+ async updateResourceList(context, vertex, resources) {
1236
+ if (Is.arrayValue(resources)) {
1237
+ for (let i = 0; i < resources.length; i++) {
1238
+ const id = this.getResourceId(resources[i]);
1239
+ if (Is.empty(id)) {
1240
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "resourceIdMissing", {
1241
+ index: i
1242
+ });
1243
+ }
1244
+ }
1245
+ }
1246
+ const active = vertex.resources?.filter(r => Is.empty(r.dateDeleted)) ?? [];
1247
+ if (Is.arrayValue(active)) {
1248
+ for (const resource of active) {
1249
+ if (!resources?.find(a => this.getResourceId(a) === this.getResourceId(resource))) {
1250
+ resource.dateDeleted = context.now;
1251
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
1252
+ }
1253
+ }
1254
+ }
1255
+ if (Is.arrayValue(resources)) {
1256
+ for (const resource of resources) {
1257
+ await this.updateResource(context, vertex, resource);
1258
+ }
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Apply resource patch operations (PATCH).
1263
+ * @param context The context for the operation.
1264
+ * @param vertex The vertex.
1265
+ * @param patch The resource patch.
1266
+ * @returns A promise that resolves when the resource patch has been applied.
1267
+ * @internal
1268
+ */
1269
+ async applyResourcePatch(context, vertex, patch) {
1270
+ if (Is.arrayValue(patch.remove)) {
1271
+ for (const removeId of patch.remove) {
1272
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
1273
+ const resource = vertex.resources?.find(r => this.getResourceId(r) === removeId && Is.empty(r.dateDeleted));
1274
+ if (!Is.empty(resource)) {
1275
+ resource.dateDeleted = context.now;
1276
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
1277
+ }
1278
+ }
1279
+ }
1280
+ if (Is.arrayValue(patch.add)) {
1281
+ for (const resource of patch.add) {
1282
+ await this.updateResource(context, vertex, resource);
1283
+ }
1284
+ }
1285
+ }
1286
+ /**
1287
+ * Add a resource to the vertex.
1288
+ * @param context The context for the operation.
1289
+ * @param vertex The vertex.
1290
+ * @param resource The resource.
1291
+ * @returns A promise that resolves when the resource has been added or updated.
1292
+ * @internal
1293
+ */
1294
+ async updateResource(context, vertex, resource) {
1295
+ Guards.object(AuditableItemGraphService.CLASS_NAME, "resource", resource);
1296
+ if (Is.object(resource.resourceObject)) {
1297
+ const validationFailures = [];
1298
+ await JsonLdHelper.validate(resource.resourceObject, validationFailures);
1299
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "resource.resourceObject", validationFailures);
1300
+ }
1301
+ // Try to find an existing resource with the same id.
1302
+ const existing = vertex.resources?.find(r => this.getResourceId(r) === this.getResourceId(resource));
1303
+ if (Is.empty(existing) || !Is.empty(existing?.dateDeleted)) {
1304
+ // Did not find a matching item, or found one which is deleted.
1305
+ vertex.resources ??= [];
1306
+ const model = {
1307
+ id: resource.id,
1308
+ dateCreated: context.now,
1309
+ resourceObject: resource.resourceObject
1310
+ };
1311
+ vertex.resources.push(model);
1312
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesAdded);
1313
+ }
1314
+ else if (!ObjectHelper.equal(existing.resourceObject, resource.resourceObject, false)) {
1315
+ // Existing resource found, update the resourceObject.
1316
+ existing.dateModified = context.now;
1317
+ existing.resourceObject = resource.resourceObject;
1318
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesModified);
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Replace the edges of a vertex (PUT).
1323
+ * @param context The context for the operation.
1324
+ * @param vertex The vertex.
1325
+ * @param edges The new active edge set.
1326
+ * @returns A promise that resolves when the edge list has been replaced.
1327
+ * @internal
1328
+ */
1329
+ async updateEdgeList(context, vertex, edges) {
1330
+ // `active` shares element refs with `vertex.edges` (the cloned newEntity); mutating `edge` below is intended.
1331
+ const active = vertex.edges?.filter(e => Is.empty(e.dateDeleted)) ?? [];
1332
+ if (Is.arrayValue(active)) {
1333
+ for (const edge of active) {
1334
+ if (!edges?.find(e => this.edgeMatchesStoredEdge(e, edge.id) ||
1335
+ this.edgeMatchesActiveEdgeByRelationship(e, edge))) {
1336
+ edge.dateDeleted = context.now;
1337
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
1338
+ }
1339
+ }
1340
+ }
1341
+ if (Is.arrayValue(edges)) {
1342
+ for (const edge of edges) {
1343
+ await this.updateEdge(context, vertex, edge);
1344
+ }
1345
+ }
1346
+ }
1347
+ /**
1348
+ * Apply edge patch operations (PATCH).
1349
+ * @param context The context for the operation.
1350
+ * @param vertex The vertex.
1351
+ * @param patch The edge patch.
1352
+ * @returns A promise that resolves when the edge patch has been applied.
1353
+ * @internal
1354
+ */
1355
+ async applyEdgePatch(context, vertex, patch) {
1356
+ if (Is.arrayValue(patch.remove)) {
1357
+ for (const removeId of patch.remove) {
1358
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "removeId", removeId);
1359
+ const edge = vertex.edges?.find(e => Is.empty(e.dateDeleted) && this.edgeRemoveIdMatches(e.id, removeId));
1360
+ if (!Is.empty(edge)) {
1361
+ edge.dateDeleted = context.now;
1362
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
1363
+ }
1364
+ }
1365
+ }
1366
+ if (Is.arrayValue(patch.add)) {
1367
+ for (const edge of patch.add) {
1368
+ await this.updateEdge(context, vertex, edge);
1369
+ }
1370
+ }
1371
+ }
1372
+ /**
1373
+ * Add an edge to the vertex.
1374
+ * @param context The context for the operation.
1375
+ * @param vertex The vertex.
1376
+ * @param edge The edge.
1377
+ * @returns A promise that resolves when the edge has been added or updated.
1378
+ * @internal
1379
+ */
1380
+ async updateEdge(context, vertex, edge) {
1381
+ Guards.object(AuditableItemGraphService.CLASS_NAME, "edge", edge);
1382
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "edge.targetId", edge.targetId);
1383
+ Guards.arrayValue(AuditableItemGraphService.CLASS_NAME, "edge.edgeRelationships", edge.edgeRelationships);
1384
+ const validationFailures = [];
1385
+ if (edge.targetId === vertex.id) {
1386
+ validationFailures.push({
1387
+ property: "id",
1388
+ reason: `validation.${StringHelper.camelCase(AuditableItemGraphService.CLASS_NAME)}.edgeIdSameAsVertexId`,
1389
+ properties: {
1390
+ targetId: edge.targetId
1391
+ }
1392
+ });
1393
+ }
1394
+ if (Is.object(edge.annotationObject)) {
1395
+ await JsonLdHelper.validate(edge.annotationObject, validationFailures);
1396
+ }
1397
+ Validation.asValidationError(AuditableItemGraphService.CLASS_NAME, "edge.annotationObject", validationFailures);
1398
+ let findId = Is.stringValue(edge.id) ? this.reduceEdgeId(edge.id) : undefined;
1399
+ if (Is.empty(findId)) {
1400
+ const existingActive = vertex.edges?.find(e => Is.empty(e.dateDeleted) &&
1401
+ e.targetId === edge.targetId &&
1402
+ ArrayHelper.matches(e.edgeRelationships, edge.edgeRelationships));
1403
+ findId = existingActive?.id ?? RandomHelper.generateUuidV7("compact");
1404
+ }
1405
+ // Try to find an existing edge with the same id.
1406
+ const existing = vertex.edges?.find(r => r.id === findId);
1407
+ if (Is.empty(existing) || !Is.empty(existing?.dateDeleted)) {
1408
+ // Did not find a matching item, or found one which is deleted.
1409
+ vertex.edges ??= [];
1410
+ const model = {
1411
+ id: findId,
1412
+ targetId: edge.targetId,
1413
+ dateCreated: context.now,
1414
+ annotationObject: edge.annotationObject,
1415
+ edgeRelationships: edge.edgeRelationships
1416
+ };
1417
+ vertex.edges.push(model);
1418
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesAdded);
1419
+ }
1420
+ else if (existing.targetId !== edge.targetId ||
1421
+ !ArrayHelper.matches(existing.edgeRelationships, edge.edgeRelationships) ||
1422
+ !ObjectHelper.equal(existing.annotationObject, edge.annotationObject, false)) {
1423
+ // Existing edge found, update the properties.
1424
+ existing.targetId = edge.targetId;
1425
+ existing.dateModified = context.now;
1426
+ existing.edgeRelationships = edge.edgeRelationships;
1427
+ existing.annotationObject = edge.annotationObject;
1428
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesModified);
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Add a changeset to the vertex and generate the associated verifications.
1433
+ * @param context The context for the operation.
1434
+ * @param original The original vertex.
1435
+ * @param updated The updated vertex.
1436
+ * @param isNew Whether this is a new item.
1437
+ * @param version The version number of the vertex after this changeset.
1438
+ * @returns True if there were changes.
1439
+ * @internal
1440
+ */
1441
+ async addChangeset(context, original, updated, isNew, version) {
1442
+ const patches = JsonHelper.diff(original, updated);
1443
+ // If there is a diff set or this is the first time the item is created.
1444
+ if (patches.length > 0 || isNew) {
1445
+ const changesetEntity = {
1446
+ id: RandomHelper.generateUuidV7("compact"),
1447
+ vertexId: updated.id,
1448
+ dateCreated: context.now,
1449
+ userIdentity: context.userIdentity,
1450
+ patches,
1451
+ version
1452
+ };
1453
+ // Create the JSON-LD object we want to use for the proof
1454
+ // this is a subset of fixed properties from the changeset object.
1455
+ const reducedChangesetJsonLd = this.changesetEntityToJsonLd(original.id, ObjectHelper.pick(changesetEntity, AuditableItemGraphService._PROOF_KEYS_CHANGESET));
1456
+ // Create the proof for the changeset object only when the vertex has
1457
+ // an owning organisation. Vertices created by inbound unauthenticated
1458
+ // activities (e.g. DSP push via skipAuth inbox) have no org context at
1459
+ // creation time and must not require a proof until a local org claims
1460
+ // ownership through an authenticated interaction.
1461
+ if (Is.stringValue(updated.organizationIdentity)) {
1462
+ changesetEntity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(reducedChangesetJsonLd));
1463
+ }
1464
+ // Link the storage id to the changeset
1465
+ await this._changesetStorage.set(changesetEntity);
1466
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ChangesetsCreated);
1467
+ return patches;
1468
+ }
1469
+ return [];
1470
+ }
1471
+ /**
1472
+ * Verify the changesets of a vertex.
1473
+ * @param vertex The vertex to verify.
1474
+ * @param verifySignatureDepth How many signatures to verify.
1475
+ * @returns The verified flag and list of changesets.
1476
+ * @internal
1477
+ */
1478
+ async verifyChangesets(vertex, verifySignatureDepth) {
1479
+ const changesets = [];
1480
+ let verified = true;
1481
+ const vertexIdUrn = Urn.fromValidString(vertex.id);
1482
+ const vertexId = vertexIdUrn.namespaceSpecific();
1483
+ let cursor;
1484
+ do {
1485
+ const chunk = await this.verifyChangesetChunk(vertexId, verifySignatureDepth, cursor);
1486
+ cursor = chunk.cursor;
1487
+ if (!chunk.verified) {
1488
+ verified = false;
1489
+ }
1490
+ changesets.push(...chunk.changesets);
1491
+ } while (Is.stringValue(cursor));
1492
+ if (verified) {
1493
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
1494
+ }
1495
+ else {
1496
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
1497
+ }
1498
+ return {
1499
+ verified,
1500
+ changesets
1501
+ };
1502
+ }
1503
+ /**
1504
+ * Verify a chunk of changesets for a vertex.
1505
+ * @param vertexId The id of the vertex to verify the changesets for.
1506
+ * @param verifySignatureDepth How many signatures to verify.
1507
+ * @param cursor The cursor to request the next chunk of changesets.
1508
+ * @param limit The maximum number of changesets to verify in this chunk.
1509
+ * @returns The changesets and whether they were verified.
1510
+ * @internal
1511
+ */
1512
+ async verifyChangesetChunk(vertexId, verifySignatureDepth, cursor, limit) {
1513
+ const changesets = [];
1514
+ let verified = true;
1515
+ const changesetsResult = await this._changesetStorage.query({
1516
+ property: "vertexId",
1517
+ value: vertexId,
1518
+ comparison: ComparisonOperator.Equals
1519
+ }, [
1520
+ {
1521
+ property: "dateCreated",
1522
+ sortDirection: SortDirection.Ascending
1523
+ }
1524
+ ], undefined, cursor, limit);
1525
+ const storedChangesets = changesetsResult.entities;
1526
+ if (Is.arrayValue(storedChangesets)) {
1527
+ for (let i = 0; i < storedChangesets.length; i++) {
1528
+ const storedChangeset = storedChangesets[i];
1529
+ const storedChangesetJsonLd = this.changesetEntityToJsonLd(vertexId, storedChangeset);
1530
+ changesets.push(storedChangesetJsonLd);
1531
+ // If we are verifying all signatures
1532
+ // or this is the last changeset (cursor is empty)
1533
+ // and the changeset has a proofId, then verify the proof.
1534
+ if (verifySignatureDepth === VerifyDepth.All ||
1535
+ (verifySignatureDepth === VerifyDepth.Current &&
1536
+ !Is.stringValue(changesetsResult.cursor) &&
1537
+ i === storedChangesets.length - 1)) {
1538
+ storedChangesetJsonLd.verification =
1539
+ await this.verifyChangesetSignature(storedChangesetJsonLd);
1540
+ if (storedChangesetJsonLd.verification?.verified !== true) {
1541
+ verified = false;
1542
+ }
1543
+ }
1544
+ }
1545
+ }
1546
+ return { changesets, verified, cursor: changesetsResult.cursor };
1547
+ }
1548
+ /**
1549
+ * Verify the signature of a changeset and add the verification result to the changeset JSON-LD.
1550
+ * @param storedChangeset The changeset to verify.
1551
+ * @returns Whether the changeset is verified.
1552
+ * @internal
1553
+ */
1554
+ async verifyChangesetSignature(storedChangeset) {
1555
+ let verification;
1556
+ if (!Is.stringValue(storedChangeset.proofId)) {
1557
+ verification = {
1558
+ "@context": ImmutableProofContexts.Context,
1559
+ type: ImmutableProofTypes.ImmutableProofVerification,
1560
+ verified: false,
1561
+ failure: ImmutableProofFailure.ProofMissing
1562
+ };
1563
+ }
1564
+ else {
1565
+ // Verify the proof for the changeset object
1566
+ verification = await this._immutableProofComponent.verify(storedChangeset.proofId);
1567
+ }
1568
+ return verification;
1569
+ }
1570
+ /**
1571
+ * Get the resource id from a resource object.
1572
+ * @param resource The resource.
1573
+ * @param resource.id The id of the resource.
1574
+ * @param resource.resourceObject The resource object.
1575
+ * @returns The resource id if it can find one.
1576
+ * @internal
1577
+ */
1578
+ getResourceId(resource) {
1579
+ return (resource.id ??
1580
+ ObjectHelper.extractProperty(resource.resourceObject, ["id", "@id"], false));
1581
+ }
1582
+ /**
1583
+ * Synchronise the index storage with the current state of a vertex.
1584
+ * @param vertex The vertex to synchronise the index entries for.
1585
+ * @returns A promise that resolves when the index entries match the vertex.
1586
+ * @internal
1587
+ */
1588
+ async syncVertexIndexes(vertex) {
1589
+ const required = new Map();
1590
+ // An entry always carries a modified date so a query ordered by it can be paged by the
1591
+ // same key set as the creation date, falling back to the creation date for a vertex which
1592
+ // has never been modified rather than leaving the column empty.
1593
+ const indexDateModified = vertex.dateModified ?? vertex.dateCreated;
1594
+ // Every vertex carries an entry for its own id, so a query matching on the vertex id can
1595
+ // be answered from the index alone. Without it a vertex holding no aliases and no
1596
+ // resources would have no entries at all and could never be matched here.
1597
+ const vertexIdValue = vertex.id.toLowerCase();
1598
+ required.set(`${AuditableItemGraphService._INDEX_TYPE_VERTEX}|${vertexIdValue}`, {
1599
+ type: AuditableItemGraphService._INDEX_TYPE_VERTEX,
1600
+ value: vertexIdValue
1601
+ });
1602
+ // Index values are case folded here, and every lookup folds its term to match, so the
1603
+ // comparisons stay case insensitive without relying on the column collation or on the
1604
+ // connector folding case itself. The vertex keeps the value exactly as supplied.
1605
+ for (const alias of vertex.aliases ?? []) {
1606
+ if (Is.empty(alias.dateDeleted) && Is.stringValue(alias.id)) {
1607
+ const value = alias.id.toLowerCase();
1608
+ required.set(`${AuditableItemGraphService._INDEX_TYPE_ALIAS}|${value}`, {
1609
+ type: AuditableItemGraphService._INDEX_TYPE_ALIAS,
1610
+ value
1611
+ });
1612
+ }
1613
+ }
1614
+ for (const resource of vertex.resources ?? []) {
1615
+ const resourceType = ObjectHelper.extractProperty(resource.resourceObject, ["@type", "type"], false);
1616
+ if (Is.stringValue(resourceType)) {
1617
+ const value = resourceType.toLowerCase();
1618
+ required.set(`${AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE}|${value}`, {
1619
+ type: AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE,
1620
+ value
1621
+ });
1622
+ }
1623
+ }
1624
+ const existing = await this.queryVertexIndexes({
1625
+ property: "vertexId",
1626
+ comparison: ComparisonOperator.Equals,
1627
+ value: vertex.id
1628
+ }, ["id", "type", "value", "valueHash", "dateCreated", "dateModified"]);
1629
+ // Collapse by row id first. A paged read can return the same row on more than one page,
1630
+ // and without this a retained row could be queued for removal by its own duplicate.
1631
+ const existingById = new Map();
1632
+ for (const entry of existing) {
1633
+ if (Is.stringValue(entry.id)) {
1634
+ existingById.set(entry.id, entry);
1635
+ }
1636
+ }
1637
+ const retainedKeys = new Set();
1638
+ const removeIds = [];
1639
+ const writeEntries = [];
1640
+ for (const [id, entry] of existingById) {
1641
+ // Entries which are no longer required, and any duplicates of a retained entry,
1642
+ // are removed so exactly one entry remains per index value.
1643
+ const key = `${entry.type}|${entry.value}`;
1644
+ const requiredEntry = required.get(key);
1645
+ if (!Is.empty(requiredEntry) && !retainedKeys.has(key)) {
1646
+ retainedKeys.add(key);
1647
+ const requiredIndexEntry = AuditableItemGraphVertexIndexHelper.createIndexEntry(vertex.id, requiredEntry.type, requiredEntry.value, vertex.dateCreated, indexDateModified);
1648
+ if (entry.dateCreated !== requiredIndexEntry.dateCreated ||
1649
+ entry.dateModified !== requiredIndexEntry.dateModified ||
1650
+ entry.valueHash !== requiredIndexEntry.valueHash) {
1651
+ // Keep the row id so the copied dates and hash are corrected in place rather than
1652
+ // duplicated.
1653
+ writeEntries.push({ ...requiredIndexEntry, id });
1654
+ }
1655
+ }
1656
+ else {
1657
+ removeIds.push(id);
1658
+ }
1659
+ }
1660
+ for (const [key, entry] of required) {
1661
+ if (!retainedKeys.has(key)) {
1662
+ writeEntries.push(AuditableItemGraphVertexIndexHelper.createIndexEntry(vertex.id, entry.type, entry.value, vertex.dateCreated, indexDateModified));
1663
+ }
1664
+ }
1665
+ if (removeIds.length > 0) {
1666
+ await this._vertexIndexStorage.removeBatch(removeIds);
1667
+ }
1668
+ if (writeEntries.length > 0) {
1669
+ await this._vertexIndexStorage.setBatch(writeEntries);
1670
+ }
1671
+ }
1672
+ /**
1673
+ * Build the JSON-LD vertex list returned by query, and record the query metric.
1674
+ * @param entities The vertex entities for the page.
1675
+ * @param cursor The already encoded cursor for the next page.
1676
+ * @returns The compacted vertex list and the cursor.
1677
+ * @internal
1678
+ */
1679
+ async buildVertexList(entities, cursor) {
1680
+ const models = entities.map(e => this.vertexEntityToJsonLd(e));
1681
+ const vertexList = {
1682
+ "@context": [
1683
+ SchemaOrgContexts.Context,
1684
+ AuditableItemGraphContexts.Context,
1685
+ AuditableItemGraphContexts.ContextCommon
1686
+ ],
1687
+ type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexList],
1688
+ [SchemaOrgTypes.ItemListElement]: models
1689
+ };
1690
+ const result = await JsonLdProcessor.compact(vertexList, vertexList["@context"], {
1691
+ compactArrays: false
1692
+ });
1693
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.QueriesExecuted, {
1694
+ resultCount: models.length,
1695
+ hasMore: Is.stringValue(cursor)
1696
+ });
1697
+ return {
1698
+ entries: result,
1699
+ cursor
1700
+ };
1701
+ }
1702
+ /**
1703
+ * Build the condition matching one index type and value. Exact matches are looked up by the
1704
+ * value hash, a partial match can only be answered by the value itself.
1705
+ * @param type The index type to match.
1706
+ * @param comparison The comparison to apply to the value.
1707
+ * @param value The index value or values to match, already lower cased.
1708
+ * @returns The condition.
1709
+ * @internal
1710
+ */
1711
+ indexTypeValueCondition(type, comparison, value) {
1712
+ const valueCondition = comparison === ComparisonOperator.Includes
1713
+ ? { property: "value", comparison, value }
1714
+ : {
1715
+ property: "valueHash",
1716
+ comparison,
1717
+ value: Is.array(value)
1718
+ ? value.map(v => AuditableItemGraphVertexIndexHelper.hashValue(v))
1719
+ : AuditableItemGraphVertexIndexHelper.hashValue(value)
1720
+ };
1721
+ return {
1722
+ logicalOperator: LogicalOperator.And,
1723
+ conditions: [
1724
+ { property: "type", comparison: ComparisonOperator.Equals, value: type },
1725
+ valueCondition
1726
+ ]
1727
+ };
1728
+ }
1729
+ /**
1730
+ * Combine conditions so that matching any one of them is enough.
1731
+ * @param conditions The conditions to combine.
1732
+ * @returns The single condition when only one was given, otherwise their union.
1733
+ * @internal
1734
+ */
1735
+ anyOfConditions(conditions) {
1736
+ return conditions.length === 1
1737
+ ? conditions[0]
1738
+ : { logicalOperator: LogicalOperator.Or, conditions };
1739
+ }
1740
+ /**
1741
+ * Read one page of vertices whose index entries satisfy every dimension. The entries of a
1742
+ * vertex are grouped so it appears once however many of them match, its vertex is joined to
1743
+ * the page so no second read is needed, and the connector pages the groups with its own cursor.
1744
+ * @param dimensions The index conditions, a vertex has to satisfy every one of them.
1745
+ * @param orderProperty The index date property to order by.
1746
+ * @param orderDirection The direction to order in.
1747
+ * @param propertiesToReturn The vertex properties to return.
1748
+ * @param conditions Additional conditions the vertices must match.
1749
+ * @param cursor The cursor returned by the previous page.
1750
+ * @param limit The maximum number of vertices to return.
1751
+ * @returns The vertices in order and the cursor for the next page.
1752
+ * @internal
1753
+ */
1754
+ async queryIndexVertices(dimensions, orderProperty, orderDirection, propertiesToReturn, conditions, cursor, limit) {
1755
+ // An entry only ever satisfies one dimension, so the entries of every dimension have to
1756
+ // survive into the group for the group conditions to find them there, which is why the
1757
+ // conditions are their union and the dimensions are applied across the group.
1758
+ const results = await this._vertexIndexStorage.queryJoin(this._vertexStorage, {
1759
+ property: "vertexId",
1760
+ joinProperty: "id",
1761
+ groupProperty: "vertexId",
1762
+ conditions: this.anyOfConditions(dimensions),
1763
+ groupConditions: dimensions.length > 1 ? dimensions : undefined,
1764
+ sortProperties: [
1765
+ { property: orderProperty, sortDirection: orderDirection },
1766
+ { property: "vertexId", sortDirection: SortDirection.Ascending }
1767
+ ],
1768
+ properties: ["vertexId"],
1769
+ joinConditions: conditions,
1770
+ joinRequired: true,
1771
+ joinProperties: propertiesToReturn,
1772
+ cursor,
1773
+ limit
1774
+ });
1775
+ // The join is required, so every group which survived carries its vertex.
1776
+ return {
1777
+ entities: results.entities.map(entity => entity.joined[0]),
1778
+ cursor: results.cursor
1779
+ };
1780
+ }
1781
+ /**
1782
+ * Query all the pages of the vertex index storage which match the conditions.
1783
+ * @param conditions The conditions to match.
1784
+ * @param properties The properties to return.
1785
+ * @returns The matching index entries.
1786
+ * @internal
1787
+ */
1788
+ async queryVertexIndexes(conditions, properties) {
1789
+ const entities = [];
1790
+ let cursor;
1791
+ do {
1792
+ // Paging needs a deterministic order or a row can be repeated or skipped between
1793
+ // pages, the entity declares no default sort so the primary key is used.
1794
+ const results = await this._vertexIndexStorage.query(conditions, [{ property: "id", sortDirection: SortDirection.Ascending }], properties, cursor);
1795
+ entities.push(...results.entities);
1796
+ cursor = results.cursor;
1797
+ } while (Is.stringValue(cursor));
1798
+ return entities;
1799
+ }
1800
+ /**
1801
+ * Find vertices with matching aliases.
1802
+ * @param vertexId The id of the vertex to exclude from the search.
1803
+ * @param aliasId The alias id to try and find.
1804
+ * @returns True if any other vertices have matching aliases.
1805
+ * @internal
1806
+ */
1807
+ async findMatchingVertices(vertexId, aliasId) {
1808
+ const entities = await this.queryVertexIndexes({
1809
+ logicalOperator: LogicalOperator.And,
1810
+ conditions: [
1811
+ {
1812
+ property: "type",
1813
+ comparison: ComparisonOperator.Equals,
1814
+ value: AuditableItemGraphService._INDEX_TYPE_ALIAS
1815
+ },
1816
+ {
1817
+ property: "valueHash",
1818
+ comparison: ComparisonOperator.Equals,
1819
+ value: AuditableItemGraphVertexIndexHelper.hashValue(aliasId)
1820
+ },
1821
+ {
1822
+ property: "vertexId",
1823
+ comparison: ComparisonOperator.NotEquals,
1824
+ value: vertexId
1825
+ }
1826
+ ]
1827
+ }, ["vertexId"]);
1828
+ return entities.length > 0;
1829
+ }
1830
+ /**
1831
+ * Whether an incoming edge matches a stored edge id.
1832
+ * @param incoming The incoming edge.
1833
+ * @param storedEdgeId The compact stored edge id.
1834
+ * @returns True if the incoming edge matches the stored edge id.
1835
+ * @internal
1836
+ */
1837
+ edgeMatchesStoredEdge(incoming, storedEdgeId) {
1838
+ return Is.stringValue(incoming.id) && this.reduceEdgeId(incoming.id) === storedEdgeId;
1839
+ }
1840
+ /**
1841
+ * Whether a PATCH remove id matches a stored compact edge id (full URN or compact).
1842
+ * @param storedEdgeId The compact stored edge id.
1843
+ * @param removeId The id from the patch remove list.
1844
+ * @returns True if the remove id identifies the stored edge.
1845
+ * @internal
1846
+ */
1847
+ edgeRemoveIdMatches(storedEdgeId, removeId) {
1848
+ if (storedEdgeId === removeId) {
1849
+ return true;
1850
+ }
1851
+ try {
1852
+ return this.reduceEdgeId(removeId) === storedEdgeId;
1853
+ }
1854
+ catch {
1855
+ return false;
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Whether an incoming id-less edge matches an active stored edge by target and relationships.
1860
+ * @param incoming The incoming edge.
1861
+ * @param stored The stored edge.
1862
+ * @returns True if they match.
1863
+ * @internal
1864
+ */
1865
+ edgeMatchesActiveEdgeByRelationship(incoming, stored) {
1866
+ return (Is.empty(incoming.id) &&
1867
+ Is.empty(stored.dateDeleted) &&
1868
+ incoming.targetId === stored.targetId &&
1869
+ ArrayHelper.matches(incoming.edgeRelationships, stored.edgeRelationships));
1870
+ }
1871
+ /**
1872
+ * Reduce the edge ID from a URN.
1873
+ * @param urn The URN to reduce.
1874
+ * @returns The edge ID.
1875
+ * @throws GeneralError if the URN is not valid or not an edge URN.
1876
+ * @internal
1877
+ */
1878
+ reduceEdgeId(urn) {
1879
+ const urnParsed = Urn.fromValidString(urn);
1880
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
1881
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
1882
+ namespace: AuditableItemGraphService.NAMESPACE,
1883
+ id: urn
1884
+ });
1885
+ }
1886
+ if (urnParsed.namespaceSpecificParts().length !== 3) {
1887
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "invalidEdgeId", {
1888
+ edgeId: urn
1889
+ });
1890
+ }
1891
+ if (urnParsed.namespaceSpecificParts()[1] !== AuditableItemGraphService.NAMESPACE_EDGE) {
1892
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "invalidEdgeId", {
1893
+ edgeId: urn
1894
+ });
1895
+ }
1896
+ return urnParsed.namespaceSpecificParts()[2];
1897
+ }
1898
+ /**
1899
+ * Create a full edge ID URN from a vertex ID and edge ID.
1900
+ * @param vertexId The vertex id the edge belongs to.
1901
+ * @param edgeId The edge id.
1902
+ * @returns The full edge ID URN.
1903
+ * @internal
1904
+ */
1905
+ fullEdgeId(vertexId, edgeId) {
1906
+ return new Urn(AuditableItemGraphService.NAMESPACE, [
1907
+ vertexId,
1908
+ AuditableItemGraphService.NAMESPACE_EDGE,
1909
+ edgeId
1910
+ ]).toString();
1911
+ }
1912
+ }
1913
+ //# sourceMappingURL=auditableItemGraphService.js.map