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

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.
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  import { AuditableItemGraphContexts, AuditableItemGraphDataTypes, 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";
@@ -124,7 +124,8 @@ export class AuditableItemGraphService {
124
124
  await this.updateEdgeList(context, vertexModel, vertex.edges);
125
125
  delete originalEntity.aliasIndex;
126
126
  delete originalEntity.resourceTypeIndex;
127
- await this.addChangeset(context, originalEntity, vertexModel, true);
127
+ await this.addChangeset(context, originalEntity, vertexModel, true, 0);
128
+ vertexModel.version = 0;
128
129
  await this._vertexStorage.set({
129
130
  ...vertexModel,
130
131
  ...this.buildIndexes(vertexModel)
@@ -183,9 +184,13 @@ export class AuditableItemGraphService {
183
184
  await this.updateAliasList(context, newEntity, vertex.aliases);
184
185
  await this.updateResourceList(context, newEntity, vertex.resources);
185
186
  await this.updateEdgeList(context, newEntity, vertex.edges);
186
- const patches = await this.addChangeset(context, originalEntity, newEntity, false);
187
+ const nextVersion = Is.empty(vertexEntity.version)
188
+ ? (await this.internalGetChangesets(vertexId)).length
189
+ : vertexEntity.version + 1;
190
+ const patches = await this.addChangeset(context, originalEntity, newEntity, false, nextVersion);
187
191
  if (patches.length > 0) {
188
192
  newEntity.dateModified = context.now;
193
+ newEntity.version = nextVersion;
189
194
  const indexes = this.buildIndexes(newEntity);
190
195
  await this._vertexStorage.set({
191
196
  ...newEntity,
@@ -348,6 +353,105 @@ export class AuditableItemGraphService {
348
353
  throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getFailed", undefined, error);
349
354
  }
350
355
  }
356
+ /**
357
+ * Get a graph vertex at a specific version.
358
+ * @param id The id of the vertex.
359
+ * @param versionId The id of the version (changeset id) to retrieve.
360
+ * @returns The vertex reconstructed at that version.
361
+ * @throws NotFoundError if the vertex or version is not found.
362
+ */
363
+ async getVersion(id, versionId) {
364
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
365
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "versionId", versionId);
366
+ const urnParsed = Urn.fromValidString(id);
367
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
368
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
369
+ namespace: AuditableItemGraphService.NAMESPACE,
370
+ id
371
+ });
372
+ }
373
+ try {
374
+ const vertexId = urnParsed.namespaceSpecific(0);
375
+ const vertexEntity = await this._vertexStorage.get(vertexId);
376
+ if (Is.empty(vertexEntity)) {
377
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
378
+ }
379
+ const targetChangeset = await this._changesetStorage.get(versionId);
380
+ if (Is.empty(targetChangeset) || targetChangeset.vertexId !== vertexId) {
381
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "versionNotFound", versionId);
382
+ }
383
+ const changesets = await this.internalGetChangesets(vertexId, {
384
+ maxVersion: targetChangeset.version
385
+ });
386
+ let entityState = {
387
+ id: vertexEntity.id,
388
+ dateCreated: vertexEntity.dateCreated,
389
+ organizationIdentity: vertexEntity.organizationIdentity
390
+ };
391
+ for (const changeset of changesets) {
392
+ entityState = JsonHelper.patch(entityState, changeset.patches);
393
+ }
394
+ const vertexModel = this.vertexEntityToJsonLd(entityState);
395
+ vertexModel.version = targetChangeset.version;
396
+ const result = await JsonLdProcessor.compact(vertexModel, vertexModel["@context"]);
397
+ return result;
398
+ }
399
+ catch (error) {
400
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionFailed", undefined, error);
401
+ }
402
+ }
403
+ /**
404
+ * Get all versions of a graph vertex.
405
+ * @param id The id of the vertex.
406
+ * @param options Additional options for the operation.
407
+ * @param options.after Only return versions created after this ISO 8601 timestamp (exclusive).
408
+ * @param options.before Only return versions created before this ISO 8601 timestamp (exclusive).
409
+ * @returns The list of vertex versions.
410
+ * @throws NotFoundError if the vertex is not found.
411
+ */
412
+ async getVersions(id, options) {
413
+ Guards.stringValue(AuditableItemGraphService.CLASS_NAME, "id", id);
414
+ const urnParsed = Urn.fromValidString(id);
415
+ if (urnParsed.namespaceIdentifier() !== AuditableItemGraphService.NAMESPACE) {
416
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "namespaceMismatch", {
417
+ namespace: AuditableItemGraphService.NAMESPACE,
418
+ id
419
+ });
420
+ }
421
+ try {
422
+ const vertexId = urnParsed.namespaceSpecific(0);
423
+ const vertexEntity = await this._vertexStorage.get(vertexId);
424
+ if (Is.empty(vertexEntity)) {
425
+ throw new NotFoundError(AuditableItemGraphService.CLASS_NAME, "vertexNotFound", id);
426
+ }
427
+ const beforeDate = Coerce.dateTime(options?.before);
428
+ const afterDate = Coerce.dateTime(options?.after);
429
+ const allChangesets = await this.internalGetChangesets(vertexId, {
430
+ before: beforeDate?.toISOString()
431
+ });
432
+ const versions = [];
433
+ for (const changeset of allChangesets) {
434
+ const changesetDate = Coerce.dateTime(changeset.dateCreated);
435
+ const afterExcluded = !Is.empty(afterDate) && !Is.empty(changesetDate) && changesetDate <= afterDate;
436
+ if (!afterExcluded) {
437
+ versions.push(changeset.version ?? 0);
438
+ }
439
+ }
440
+ const versionList = {
441
+ "@context": [
442
+ SchemaOrgContexts.Context,
443
+ AuditableItemGraphContexts.Context,
444
+ AuditableItemGraphContexts.ContextCommon
445
+ ],
446
+ type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexVersionList],
447
+ [SchemaOrgTypes.ItemListElement]: versions
448
+ };
449
+ return await JsonLdProcessor.compact(versionList, versionList["@context"]);
450
+ }
451
+ catch (error) {
452
+ throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "getVersionsFailed", undefined, error);
453
+ }
454
+ }
351
455
  /**
352
456
  * Remove the verifiable storage for an item.
353
457
  * @param id The id of the vertex to get.
@@ -599,10 +703,47 @@ export class AuditableItemGraphService {
599
703
  patchFrom: p.from,
600
704
  patchValue: p.value
601
705
  })),
602
- proofId: changesetEntity.proofId
706
+ proofId: changesetEntity.proofId,
707
+ version: changesetEntity.version
603
708
  };
604
709
  return model;
605
710
  }
711
+ /**
712
+ * Fetch all changesets for a vertex in ascending date order.
713
+ * @param vertexId The internal vertex id.
714
+ * @param options Optional filtering options.
715
+ * @param options.before Only fetch changesets created strictly before this ISO 8601 timestamp.
716
+ * @param options.maxVersion Only fetch changesets with version <= this value.
717
+ * @returns All changeset entities sorted ascending by dateCreated.
718
+ * @internal
719
+ */
720
+ async internalGetChangesets(vertexId, options) {
721
+ const all = [];
722
+ let cursor;
723
+ const conditions = [
724
+ { property: "vertexId", value: vertexId, comparison: ComparisonOperator.Equals }
725
+ ];
726
+ if (Is.stringValue(options?.before)) {
727
+ conditions.push({
728
+ property: "dateCreated",
729
+ value: options.before,
730
+ comparison: ComparisonOperator.LessThan
731
+ });
732
+ }
733
+ if (!Is.empty(options?.maxVersion)) {
734
+ conditions.push({
735
+ property: "version",
736
+ value: options.maxVersion,
737
+ comparison: ComparisonOperator.LessThanOrEqual
738
+ });
739
+ }
740
+ do {
741
+ const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }], undefined, cursor);
742
+ all.push(...result.entities);
743
+ cursor = result.cursor;
744
+ } while (Is.stringValue(cursor));
745
+ return all;
746
+ }
606
747
  /**
607
748
  * Update the aliases of a vertex model.
608
749
  * @param context The context for the operation.
@@ -820,10 +961,11 @@ export class AuditableItemGraphService {
820
961
  * @param original The original vertex.
821
962
  * @param updated The updated vertex.
822
963
  * @param isNew Whether this is a new item.
964
+ * @param version The version number of the vertex after this changeset.
823
965
  * @returns True if there were changes.
824
966
  * @internal
825
967
  */
826
- async addChangeset(context, original, updated, isNew) {
968
+ async addChangeset(context, original, updated, isNew, version) {
827
969
  const patches = JsonHelper.diff(original, updated);
828
970
  // If there is a diff set or this is the first time the item is created.
829
971
  if (patches.length > 0 || isNew) {
@@ -832,13 +974,14 @@ export class AuditableItemGraphService {
832
974
  vertexId: updated.id,
833
975
  dateCreated: context.now,
834
976
  userIdentity: context.contextIds?.[ContextIdKeys.User],
835
- patches
977
+ patches,
978
+ version
836
979
  };
837
980
  // Create the JSON-LD object we want to use for the proof
838
981
  // this is a subset of fixed properties from the changeset object.
839
982
  const reducedChangesetJsonLd = this.changesetEntityToJsonLd(original.id, ObjectHelper.pick(changesetEntity, AuditableItemGraphService._PROOF_KEYS_CHANGESET));
840
983
  // Create the proof for the changeset object
841
- changesetEntity.proofId = await this._immutableProofComponent.create(reducedChangesetJsonLd);
984
+ changesetEntity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(reducedChangesetJsonLd));
842
985
  // Link the verifiable storage id to the changeset
843
986
  await this._changesetStorage.set(changesetEntity);
844
987
  return patches;