@twin.org/auditable-item-graph-service 0.0.3-next.13 → 0.0.3-next.15

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 (25) hide show
  1. package/dist/es/auditableItemGraphRoutes.js +162 -1
  2. package/dist/es/auditableItemGraphRoutes.js.map +1 -1
  3. package/dist/es/auditableItemGraphService.js +208 -11
  4. package/dist/es/auditableItemGraphService.js.map +1 -1
  5. package/dist/es/entities/auditableItemGraphChangeset.js +8 -0
  6. package/dist/es/entities/auditableItemGraphChangeset.js.map +1 -1
  7. package/dist/es/entities/auditableItemGraphVertex.js +8 -0
  8. package/dist/es/entities/auditableItemGraphVertex.js.map +1 -1
  9. package/dist/es/models/IAuditableItemGraphServiceConstructorOptions.js.map +1 -1
  10. package/dist/types/auditableItemGraphRoutes.d.ts +17 -1
  11. package/dist/types/auditableItemGraphService.d.ts +26 -1
  12. package/dist/types/entities/auditableItemGraphChangeset.d.ts +4 -0
  13. package/dist/types/entities/auditableItemGraphVertex.d.ts +4 -0
  14. package/dist/types/models/IAuditableItemGraphServiceConstructorOptions.d.ts +4 -0
  15. package/docs/changelog.md +29 -0
  16. package/docs/open-api/spec.json +284 -0
  17. package/docs/reference/classes/AuditableItemGraphChangeset.md +8 -0
  18. package/docs/reference/classes/AuditableItemGraphService.md +98 -0
  19. package/docs/reference/classes/AuditableItemGraphVertex.md +8 -0
  20. package/docs/reference/functions/auditableItemGraphVersionGet.md +31 -0
  21. package/docs/reference/functions/auditableItemGraphVersionList.md +31 -0
  22. package/docs/reference/index.md +2 -0
  23. package/docs/reference/interfaces/IAuditableItemGraphServiceConstructorOptions.md +8 -0
  24. package/locales/en.json +3 -0
  25. package/package.json +3 -2
@@ -1,14 +1,15 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
- import { AuditableItemGraphContexts, AuditableItemGraphDataTypes, 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, ComponentFactory, GeneralError, Guards, Is, JsonHelper, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@twin.org/core";
5
+ import { ArrayHelper, Coerce, ComponentFactory, GeneralError, Guards, Is, JsonHelper, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@twin.org/core";
6
6
  import { DataTypeHelper } from "@twin.org/data-core";
7
7
  import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
8
8
  import { ComparisonOperator, LogicalOperator, SortDirection } from "@twin.org/entity";
9
9
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
10
10
  import { ImmutableProofContexts, ImmutableProofFailure, ImmutableProofTypes } from "@twin.org/immutable-proof-models";
11
11
  import { SchemaOrgContexts, SchemaOrgDataTypes, SchemaOrgTypes } from "@twin.org/standards-schema-org";
12
+ import { MetricHelper } from "@twin.org/telemetry-models";
12
13
  /**
13
14
  * Class for performing auditable item graph operations.
14
15
  */
@@ -61,6 +62,11 @@ export class AuditableItemGraphService {
61
62
  * @internal
62
63
  */
63
64
  _eventBusComponent;
65
+ /**
66
+ * The telemetry component.
67
+ * @internal
68
+ */
69
+ _telemetryComponent;
64
70
  /**
65
71
  * Create a new instance of AuditableItemGraphService.
66
72
  * @param options The dependencies for the auditable item graph connector.
@@ -69,9 +75,8 @@ export class AuditableItemGraphService {
69
75
  this._immutableProofComponent = ComponentFactory.get(options?.immutableProofComponentType ?? "immutable-proof");
70
76
  this._vertexStorage = EntityStorageConnectorFactory.get(options?.vertexEntityStorageType ?? "auditable-item-graph-vertex");
71
77
  this._changesetStorage = EntityStorageConnectorFactory.get(options?.changesetEntityStorageType ?? "auditable-item-graph-changeset");
72
- if (Is.stringValue(options?.eventBusComponentType)) {
73
- this._eventBusComponent = ComponentFactory.get(options.eventBusComponentType);
74
- }
78
+ this._eventBusComponent = ComponentFactory.getIfExists(options?.eventBusComponentType);
79
+ this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
75
80
  SchemaOrgDataTypes.registerRedirects();
76
81
  AuditableItemGraphDataTypes.registerTypes();
77
82
  JsonLdDataTypes.registerTypes();
@@ -83,6 +88,15 @@ export class AuditableItemGraphService {
83
88
  className() {
84
89
  return AuditableItemGraphService.CLASS_NAME;
85
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
+ }
86
100
  /**
87
101
  * Create a new graph vertex.
88
102
  * @param vertex The vertex to create.
@@ -124,11 +138,13 @@ export class AuditableItemGraphService {
124
138
  await this.updateEdgeList(context, vertexModel, vertex.edges);
125
139
  delete originalEntity.aliasIndex;
126
140
  delete originalEntity.resourceTypeIndex;
127
- await this.addChangeset(context, originalEntity, vertexModel, true);
141
+ await this.addChangeset(context, originalEntity, vertexModel, true, 0);
142
+ vertexModel.version = 0;
128
143
  await this._vertexStorage.set({
129
144
  ...vertexModel,
130
145
  ...this.buildIndexes(vertexModel)
131
146
  });
147
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesCreated);
132
148
  const fullId = new Urn(AuditableItemGraphService.NAMESPACE, id).toString();
133
149
  await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexCreated, { id: fullId });
134
150
  return fullId;
@@ -183,14 +199,21 @@ export class AuditableItemGraphService {
183
199
  await this.updateAliasList(context, newEntity, vertex.aliases);
184
200
  await this.updateResourceList(context, newEntity, vertex.resources);
185
201
  await this.updateEdgeList(context, newEntity, vertex.edges);
186
- const patches = await this.addChangeset(context, originalEntity, newEntity, false);
202
+ const nextVersion = Is.empty(vertexEntity.version)
203
+ ? (await this.internalGetChangesets(vertexId)).length
204
+ : vertexEntity.version + 1;
205
+ const patches = await this.addChangeset(context, originalEntity, newEntity, false, nextVersion);
187
206
  if (patches.length > 0) {
188
207
  newEntity.dateModified = context.now;
208
+ newEntity.version = nextVersion;
189
209
  const indexes = this.buildIndexes(newEntity);
190
210
  await this._vertexStorage.set({
191
211
  ...newEntity,
192
212
  ...indexes
193
213
  });
214
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
215
+ patchCount: patches.length
216
+ });
194
217
  await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexUpdated, { id: vertex.id, patches });
195
218
  }
196
219
  }
@@ -287,6 +310,14 @@ export class AuditableItemGraphService {
287
310
  throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
288
311
  }
289
312
  const chunk = await this.verifyChangesetChunk(vertexId, options?.verifySignatureDepth ?? VerifyDepth.None, cursor, limit);
313
+ if ((options?.verifySignatureDepth ?? VerifyDepth.None) !== VerifyDepth.None) {
314
+ if (chunk.verified) {
315
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
316
+ }
317
+ else {
318
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
319
+ }
320
+ }
290
321
  const changesetList = {
291
322
  "@context": [
292
323
  SchemaOrgContexts.Context,
@@ -340,6 +371,14 @@ export class AuditableItemGraphService {
340
371
  if (verifySignatureDepth !== VerifyDepth.None) {
341
372
  changesetModel["@context"]?.push(ImmutableProofContexts.Context);
342
373
  changesetModel.verification = await this.verifyChangesetSignature(changesetModel);
374
+ if (changesetModel.verification?.verified) {
375
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
376
+ }
377
+ else {
378
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed, {
379
+ failureReason: changesetModel.verification?.failure
380
+ });
381
+ }
343
382
  }
344
383
  const result = await JsonLdProcessor.compact(changesetModel, changesetModel["@context"]);
345
384
  return result;
@@ -348,6 +387,105 @@ export class AuditableItemGraphService {
348
387
  throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
349
388
  }
350
389
  }
390
+ /**
391
+ * Get a graph vertex at a specific version.
392
+ * @param id The id of the vertex.
393
+ * @param versionId The id of the version (changeset id) to retrieve.
394
+ * @returns The vertex reconstructed at that version.
395
+ * @throws NotFoundError if the vertex or version is not found.
396
+ */
397
+ async getVersion(id, versionId) {
398
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
399
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "versionId", versionId);
400
+ const urnParsed = Urn.fromValidString(id);
401
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
402
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
403
+ namespace: AuditableItemGraphService.NAMESPACE,
404
+ id
405
+ });
406
+ }
407
+ try {
408
+ const vertexId = urnParsed.namespaceSpecific(0);
409
+ const vertexEntity = await this._vertexStorage.get(vertexId);
410
+ if (Is.empty(vertexEntity)) {
411
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
412
+ }
413
+ const targetChangeset = await this._changesetStorage.get(versionId);
414
+ if (Is.empty(targetChangeset) || targetChangeset.vertexId !== vertexId) {
415
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "versionNotFound", versionId);
416
+ }
417
+ const changesets = await this.internalGetChangesets(vertexId, {
418
+ maxVersion: targetChangeset.version
419
+ });
420
+ let entityState = {
421
+ id: vertexEntity.id,
422
+ dateCreated: vertexEntity.dateCreated,
423
+ organizationIdentity: vertexEntity.organizationIdentity
424
+ };
425
+ for (const changeset of changesets) {
426
+ entityState = JsonHelper.patch(entityState, changeset.patches);
427
+ }
428
+ const vertexModel = this.vertexEntityToJsonLd(entityState);
429
+ vertexModel.version = targetChangeset.version;
430
+ const result = await JsonLdProcessor.compact(vertexModel, vertexModel["@context"]);
431
+ return result;
432
+ }
433
+ catch (error) {
434
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionFailed", undefined, error);
435
+ }
436
+ }
437
+ /**
438
+ * Get all versions of a graph vertex.
439
+ * @param id The id of the vertex.
440
+ * @param options Additional options for the operation.
441
+ * @param options.after Only return versions created after this ISO 8601 timestamp (exclusive).
442
+ * @param options.before Only return versions created before this ISO 8601 timestamp (exclusive).
443
+ * @returns The list of vertex versions.
444
+ * @throws NotFoundError if the vertex is not found.
445
+ */
446
+ async getVersions(id, options) {
447
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
448
+ const urnParsed = Urn.fromValidString(id);
449
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
450
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
451
+ namespace: AuditableItemGraphService.NAMESPACE,
452
+ id
453
+ });
454
+ }
455
+ try {
456
+ const vertexId = urnParsed.namespaceSpecific(0);
457
+ const vertexEntity = await this._vertexStorage.get(vertexId);
458
+ if (Is.empty(vertexEntity)) {
459
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
460
+ }
461
+ const beforeDate = Coerce.dateTime(options?.before);
462
+ const afterDate = Coerce.dateTime(options?.after);
463
+ const allChangesets = await this.internalGetChangesets(vertexId, {
464
+ before: beforeDate?.toISOString()
465
+ });
466
+ const versions = [];
467
+ for (const changeset of allChangesets) {
468
+ const changesetDate = Coerce.dateTime(changeset.dateCreated);
469
+ const afterExcluded = !Is.empty(afterDate) && !Is.empty(changesetDate) && changesetDate <= afterDate;
470
+ if (!afterExcluded) {
471
+ versions.push(changeset.version ?? 0);
472
+ }
473
+ }
474
+ const versionList = {
475
+ "@context": [
476
+ SchemaOrgContexts.Context,
477
+ AuditableItemGraphContexts.Context,
478
+ AuditableItemGraphContexts.ContextCommon
479
+ ],
480
+ type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexVersionList],
481
+ [SchemaOrgTypes.ItemListElement]: versions
482
+ };
483
+ return await JsonLdProcessor.compact(versionList, versionList["@context"]);
484
+ }
485
+ catch (error) {
486
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionsFailed", undefined, error);
487
+ }
488
+ }
351
489
  /**
352
490
  * Remove the verifiable storage for an item.
353
491
  * @param id The id of the vertex to get.
@@ -474,6 +612,10 @@ export class AuditableItemGraphService {
474
612
  [SchemaOrgTypes.ItemListElement]: models
475
613
  };
476
614
  const result = await JsonLdProcessor.compact(vertexList, vertexList["@context"]);
615
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.QueriesExecuted, {
616
+ resultCount: models.length,
617
+ hasMore: Is.stringValue(results.cursor)
618
+ });
477
619
  return {
478
620
  entries: result,
479
621
  cursor: results.cursor
@@ -599,10 +741,47 @@ export class AuditableItemGraphService {
599
741
  patchFrom: p.from,
600
742
  patchValue: p.value
601
743
  })),
602
- proofId: changesetEntity.proofId
744
+ proofId: changesetEntity.proofId,
745
+ version: changesetEntity.version
603
746
  };
604
747
  return model;
605
748
  }
749
+ /**
750
+ * Fetch all changesets for a vertex in ascending date order.
751
+ * @param vertexId The internal vertex id.
752
+ * @param options Optional filtering options.
753
+ * @param options.before Only fetch changesets created strictly before this ISO 8601 timestamp.
754
+ * @param options.maxVersion Only fetch changesets with version <= this value.
755
+ * @returns All changeset entities sorted ascending by dateCreated.
756
+ * @internal
757
+ */
758
+ async internalGetChangesets(vertexId, options) {
759
+ const all = [];
760
+ let cursor;
761
+ const conditions = [
762
+ { property: "vertexId", value: vertexId, comparison: ComparisonOperator.Equals }
763
+ ];
764
+ if (Is.stringValue(options?.before)) {
765
+ conditions.push({
766
+ property: "dateCreated",
767
+ value: options.before,
768
+ comparison: ComparisonOperator.LessThan
769
+ });
770
+ }
771
+ if (!Is.empty(options?.maxVersion)) {
772
+ conditions.push({
773
+ property: "version",
774
+ value: options.maxVersion,
775
+ comparison: ComparisonOperator.LessThanOrEqual
776
+ });
777
+ }
778
+ do {
779
+ const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }], undefined, cursor);
780
+ all.push(...result.entities);
781
+ cursor = result.cursor;
782
+ } while (Is.stringValue(cursor));
783
+ return all;
784
+ }
606
785
  /**
607
786
  * Update the aliases of a vertex model.
608
787
  * @param context The context for the operation.
@@ -617,6 +796,7 @@ export class AuditableItemGraphService {
617
796
  for (const alias of active) {
618
797
  if (!aliases?.find(a => a.id === alias.id)) {
619
798
  alias.dateDeleted = context.now;
799
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesDeleted);
620
800
  }
621
801
  }
622
802
  }
@@ -662,6 +842,7 @@ export class AuditableItemGraphService {
662
842
  unique: alias.unique
663
843
  };
664
844
  vertex.aliases.push(model);
845
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesAdded);
665
846
  }
666
847
  else if (existing.aliasFormat !== alias.aliasFormat ||
667
848
  !ObjectHelper.equal(existing.annotationObject, alias.annotationObject, false) ||
@@ -671,6 +852,7 @@ export class AuditableItemGraphService {
671
852
  existing.aliasFormat = alias.aliasFormat;
672
853
  existing.annotationObject = alias.annotationObject;
673
854
  existing.unique = alias.unique;
855
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.AliasesModified);
674
856
  }
675
857
  }
676
858
  /**
@@ -697,6 +879,7 @@ export class AuditableItemGraphService {
697
879
  for (const resource of active) {
698
880
  if (!resources?.find(a => this.getResourceId(a) === this.getResourceId(resource))) {
699
881
  resource.dateDeleted = context.now;
882
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesDeleted);
700
883
  }
701
884
  }
702
885
  }
@@ -731,11 +914,13 @@ export class AuditableItemGraphService {
731
914
  resourceObject: resource.resourceObject
732
915
  };
733
916
  vertex.resources.push(model);
917
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesAdded);
734
918
  }
735
919
  else if (!ObjectHelper.equal(existing.resourceObject, resource.resourceObject, false)) {
736
920
  // Existing resource found, update the resourceObject.
737
921
  existing.dateModified = context.now;
738
922
  existing.resourceObject = resource.resourceObject;
923
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ResourcesModified);
739
924
  }
740
925
  }
741
926
  /**
@@ -752,6 +937,7 @@ export class AuditableItemGraphService {
752
937
  for (const edge of active) {
753
938
  if (!edges?.find(e => Is.stringValue(e.id) && this.reduceEdgeId(e.id) === edge.id)) {
754
939
  edge.dateDeleted = context.now;
940
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesDeleted);
755
941
  }
756
942
  }
757
943
  }
@@ -803,6 +989,7 @@ export class AuditableItemGraphService {
803
989
  edgeRelationships: edge.edgeRelationships
804
990
  };
805
991
  vertex.edges.push(model);
992
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesAdded);
806
993
  }
807
994
  else if (existing.targetId !== edge.targetId ||
808
995
  !ArrayHelper.matches(existing.edgeRelationships, edge.edgeRelationships) ||
@@ -812,6 +999,7 @@ export class AuditableItemGraphService {
812
999
  existing.dateModified = context.now;
813
1000
  existing.edgeRelationships = edge.edgeRelationships;
814
1001
  existing.annotationObject = edge.annotationObject;
1002
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.EdgesModified);
815
1003
  }
816
1004
  }
817
1005
  /**
@@ -820,10 +1008,11 @@ export class AuditableItemGraphService {
820
1008
  * @param original The original vertex.
821
1009
  * @param updated The updated vertex.
822
1010
  * @param isNew Whether this is a new item.
1011
+ * @param version The version number of the vertex after this changeset.
823
1012
  * @returns True if there were changes.
824
1013
  * @internal
825
1014
  */
826
- async addChangeset(context, original, updated, isNew) {
1015
+ async addChangeset(context, original, updated, isNew, version) {
827
1016
  const patches = JsonHelper.diff(original, updated);
828
1017
  // If there is a diff set or this is the first time the item is created.
829
1018
  if (patches.length > 0 || isNew) {
@@ -832,15 +1021,17 @@ export class AuditableItemGraphService {
832
1021
  vertexId: updated.id,
833
1022
  dateCreated: context.now,
834
1023
  userIdentity: context.contextIds?.[ContextIdKeys.User],
835
- patches
1024
+ patches,
1025
+ version
836
1026
  };
837
1027
  // Create the JSON-LD object we want to use for the proof
838
1028
  // this is a subset of fixed properties from the changeset object.
839
1029
  const reducedChangesetJsonLd = this.changesetEntityToJsonLd(original.id, ObjectHelper.pick(changesetEntity, AuditableItemGraphService._PROOF_KEYS_CHANGESET));
840
1030
  // Create the proof for the changeset object
841
- changesetEntity.proofId = await this._immutableProofComponent.create(reducedChangesetJsonLd);
1031
+ changesetEntity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(reducedChangesetJsonLd));
842
1032
  // Link the verifiable storage id to the changeset
843
1033
  await this._changesetStorage.set(changesetEntity);
1034
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.ChangesetsCreated);
844
1035
  return patches;
845
1036
  }
846
1037
  return [];
@@ -865,6 +1056,12 @@ export class AuditableItemGraphService {
865
1056
  }
866
1057
  changesets.push(...chunk.changesets);
867
1058
  } while (Is.stringValue(cursor));
1059
+ if (verified) {
1060
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsSucceeded);
1061
+ }
1062
+ else {
1063
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerificationsFailed);
1064
+ }
868
1065
  return {
869
1066
  verified,
870
1067
  changesets