@twin.org/auditable-item-graph-service 0.10.0 → 0.10.1-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/auditableItemGraphService.js +481 -120
- package/dist/es/auditableItemGraphService.js.map +1 -1
- package/dist/es/entities/auditableItemGraphAlias.js +2 -2
- package/dist/es/entities/auditableItemGraphAlias.js.map +1 -1
- package/dist/es/entities/auditableItemGraphChangeset.js +15 -5
- package/dist/es/entities/auditableItemGraphChangeset.js.map +1 -1
- package/dist/es/entities/auditableItemGraphEdge.js +2 -2
- package/dist/es/entities/auditableItemGraphEdge.js.map +1 -1
- package/dist/es/entities/auditableItemGraphPatch.js +3 -3
- package/dist/es/entities/auditableItemGraphPatch.js.map +1 -1
- package/dist/es/entities/auditableItemGraphResource.js +1 -1
- package/dist/es/entities/auditableItemGraphResource.js.map +1 -1
- package/dist/es/entities/auditableItemGraphVertex.js +4 -20
- package/dist/es/entities/auditableItemGraphVertex.js.map +1 -1
- package/dist/es/entities/auditableItemGraphVertexIndex.js +83 -0
- package/dist/es/entities/auditableItemGraphVertexIndex.js.map +1 -0
- package/dist/es/entities/auditableItemGraphVertexV1.js +113 -0
- package/dist/es/entities/auditableItemGraphVertexV1.js.map +1 -0
- package/dist/es/index.js +2 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IAuditableItemGraphServiceConstructorOptions.js.map +1 -1
- package/dist/es/schema.js +4 -0
- package/dist/es/schema.js.map +1 -1
- package/dist/types/auditableItemGraphService.d.ts +3 -2
- package/dist/types/entities/auditableItemGraphVertex.d.ts +0 -8
- package/dist/types/entities/auditableItemGraphVertexIndex.d.ts +31 -0
- package/dist/types/entities/auditableItemGraphVertexV1.d.ts +58 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/models/IAuditableItemGraphServiceConstructorOptions.d.ts +5 -0
- package/docs/changelog.md +72 -0
- package/docs/open-api/spec.json +1 -1
- package/docs/reference/classes/AuditableItemGraphService.md +2 -1
- package/docs/reference/classes/AuditableItemGraphVertex.md +0 -16
- package/docs/reference/classes/AuditableItemGraphVertexIndex.md +63 -0
- package/docs/reference/classes/AuditableItemGraphVertexV1.md +109 -0
- package/docs/reference/index.md +2 -0
- package/docs/reference/interfaces/IAuditableItemGraphServiceConstructorOptions.md +14 -0
- package/locales/en.json +1 -0
- package/package.json +16 -16
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { HealthCategory, HealthStatus } from "@twin.org/api-models";
|
|
4
4
|
import { AuditableItemGraphAuditMode, AuditableItemGraphContexts, AuditableItemGraphDataTypes, AuditableItemGraphMetricIds, AuditableItemGraphMetrics, AuditableItemGraphTopics, AuditableItemGraphTypes, VerifyDepth } from "@twin.org/auditable-item-graph-models";
|
|
5
5
|
import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
6
|
-
import { ArrayHelper, BaseError, Coerce, ComponentFactory, GeneralError, Guards, Is, JsonHelper, Mutex, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@twin.org/core";
|
|
6
|
+
import { ArrayHelper, BaseError, Coerce, ComponentFactory, Converter, GeneralError, Guards, Is, JsonHelper, Mutex, NotFoundError, ObjectHelper, RandomHelper, StringHelper, Urn, Validation } from "@twin.org/core";
|
|
7
7
|
import { DataTypeHelper } from "@twin.org/data-core";
|
|
8
8
|
import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
|
|
9
9
|
import { ComparisonOperator, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
@@ -32,6 +32,26 @@ export class AuditableItemGraphService {
|
|
|
32
32
|
* The namespace for the service edge.
|
|
33
33
|
*/
|
|
34
34
|
static NAMESPACE_EDGE = "edge";
|
|
35
|
+
/**
|
|
36
|
+
* The number of vertices returned by a query when the caller gives no limit.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
static _DEFAULT_PAGE_SIZE = 40;
|
|
40
|
+
/**
|
|
41
|
+
* The index type for the id of the vertex itself.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
static _INDEX_TYPE_VERTEX = "vertex";
|
|
45
|
+
/**
|
|
46
|
+
* The index type for vertex aliases.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
static _INDEX_TYPE_ALIAS = "alias";
|
|
50
|
+
/**
|
|
51
|
+
* The index type for vertex resource types.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
static _INDEX_TYPE_RESOURCE_TYPE = "resourceType";
|
|
35
55
|
/**
|
|
36
56
|
* The keys to pick when creating the proof for the stream.
|
|
37
57
|
* @internal
|
|
@@ -53,6 +73,11 @@ export class AuditableItemGraphService {
|
|
|
53
73
|
* @internal
|
|
54
74
|
*/
|
|
55
75
|
_vertexStorage;
|
|
76
|
+
/**
|
|
77
|
+
* The entity storage for vertex indices.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
_vertexIndexStorage;
|
|
56
81
|
/**
|
|
57
82
|
* The entity storage for changesets.
|
|
58
83
|
* @internal
|
|
@@ -80,6 +105,7 @@ export class AuditableItemGraphService {
|
|
|
80
105
|
constructor(options) {
|
|
81
106
|
this._immutableProofComponent = ComponentFactory.get(options?.immutableProofComponentType ?? "immutable-proof");
|
|
82
107
|
this._vertexStorage = EntityStorageConnectorFactory.get(options?.vertexEntityStorageType ?? "auditable-item-graph-vertex");
|
|
108
|
+
this._vertexIndexStorage = EntityStorageConnectorFactory.get(options?.vertexIndexEntityStorageType ?? "auditable-item-graph-vertex-index");
|
|
83
109
|
this._changesetStorage = EntityStorageConnectorFactory.get(options?.changesetEntityStorageType ?? "auditable-item-graph-changeset");
|
|
84
110
|
this._eventBusComponent = ComponentFactory.getIfExists(options?.eventBusComponentType);
|
|
85
111
|
this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
|
|
@@ -197,15 +223,11 @@ export class AuditableItemGraphService {
|
|
|
197
223
|
await this.updateEdgeList(context, vertexModel, vertex.edges);
|
|
198
224
|
// Bypass vertices keep no changeset history, so there is no baseline version to record.
|
|
199
225
|
if (vertex.auditMode !== AuditableItemGraphAuditMode.Bypass) {
|
|
200
|
-
delete originalEntity.aliasIndex;
|
|
201
|
-
delete originalEntity.resourceTypeIndex;
|
|
202
226
|
await this.addChangeset(context, originalEntity, vertexModel, true, 0);
|
|
203
227
|
vertexModel.version = 0;
|
|
204
228
|
}
|
|
205
|
-
await this._vertexStorage.set(
|
|
206
|
-
|
|
207
|
-
...this.buildIndexes(vertexModel)
|
|
208
|
-
});
|
|
229
|
+
await this._vertexStorage.set(vertexModel);
|
|
230
|
+
await this.syncVertexIndexes(vertexModel);
|
|
209
231
|
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesCreated);
|
|
210
232
|
const fullId = new Urn(AuditableItemGraphService.NAMESPACE, id).toString();
|
|
211
233
|
await this._eventBusComponent?.publish(AuditableItemGraphTopics.VertexCreated, { id: fullId });
|
|
@@ -254,7 +276,6 @@ export class AuditableItemGraphService {
|
|
|
254
276
|
userIdentity: contextIds?.[ContextIdKeys.User]
|
|
255
277
|
};
|
|
256
278
|
const auditModeTransition = this.resolveAuditModeTransition(vertexEntity, vertex.auditMode);
|
|
257
|
-
delete vertexEntity.aliasIndex;
|
|
258
279
|
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
259
280
|
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
260
281
|
newEntity.annotationObject = vertex.annotationObject;
|
|
@@ -307,7 +328,6 @@ export class AuditableItemGraphService {
|
|
|
307
328
|
userIdentity: contextIds?.[ContextIdKeys.User]
|
|
308
329
|
};
|
|
309
330
|
const auditModeTransition = this.resolveAuditModeTransition(vertexEntity, partial.auditMode);
|
|
310
|
-
delete vertexEntity.aliasIndex;
|
|
311
331
|
const originalEntity = ObjectHelper.clone(vertexEntity);
|
|
312
332
|
const newEntity = ObjectHelper.clone(vertexEntity);
|
|
313
333
|
if (partial.annotationObject !== undefined) {
|
|
@@ -557,7 +577,7 @@ export class AuditableItemGraphService {
|
|
|
557
577
|
compactArrays: false
|
|
558
578
|
});
|
|
559
579
|
}
|
|
560
|
-
const changesets = await this.internalGetChangesets(vertexId, {
|
|
580
|
+
const changesets = await this.internalGetChangesets(vertexId, ["patches"], {
|
|
561
581
|
maxVersion: version
|
|
562
582
|
});
|
|
563
583
|
let entityState = {
|
|
@@ -681,7 +701,8 @@ export class AuditableItemGraphService {
|
|
|
681
701
|
* @param orderByDirection The direction for the order, defaults to desc.
|
|
682
702
|
* @param properties The properties to return, if not provided defaults to id, created, aliases and object.
|
|
683
703
|
* @param cursor The cursor to request the next chunk of entities.
|
|
684
|
-
* @param limit
|
|
704
|
+
* @param limit The maximum number of entities to return, a page can contain fewer so follow
|
|
705
|
+
* the cursor until it is absent to read them all.
|
|
685
706
|
* @returns The entities, which can be partial if a limited keys list was provided.
|
|
686
707
|
*/
|
|
687
708
|
async query(options, conditions, orderBy, orderByDirection, properties, cursor, limit) {
|
|
@@ -693,61 +714,58 @@ export class AuditableItemGraphService {
|
|
|
693
714
|
"aliases",
|
|
694
715
|
"annotationObject"
|
|
695
716
|
];
|
|
696
|
-
const andGroups = [];
|
|
697
717
|
const orderProperty = orderBy ?? "dateCreated";
|
|
698
718
|
const orderDirection = orderByDirection ?? SortDirection.Descending;
|
|
699
719
|
const idExact = options?.idExact ?? false;
|
|
700
|
-
if (!Is.empty(conditions)) {
|
|
701
|
-
andGroups.push(conditions);
|
|
702
|
-
}
|
|
703
720
|
const idOrAlias = options?.id;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
721
|
+
const idMode = options?.idMode ?? "both";
|
|
722
|
+
const resourceTypes = options?.resourceTypes;
|
|
723
|
+
const hasIdFilter = Is.stringValue(idOrAlias);
|
|
724
|
+
const hasResourceTypes = Is.arrayValue(resourceTypes);
|
|
725
|
+
if (!propertiesToReturn.includes("id")) {
|
|
726
|
+
propertiesToReturn.unshift("id");
|
|
727
|
+
}
|
|
728
|
+
// Every id, alias and resource type match is held in the index storage, which carries
|
|
729
|
+
// the ordering dates so it can page its own matches by key set. Nothing is drained into
|
|
730
|
+
// memory, so a term matching a very large number of entries costs one page.
|
|
731
|
+
const indexPredicates = [];
|
|
732
|
+
if (hasIdFilter) {
|
|
733
|
+
const comparison = idExact ? ComparisonOperator.Equals : ComparisonOperator.Includes;
|
|
734
|
+
const term = idOrAlias.toLowerCase();
|
|
707
735
|
if (idMode === "id" || idMode === "both") {
|
|
708
|
-
|
|
709
|
-
property: "id",
|
|
710
|
-
comparison: idExact ? ComparisonOperator.Equals : ComparisonOperator.Includes,
|
|
711
|
-
value: idOrAlias
|
|
712
|
-
});
|
|
736
|
+
indexPredicates.push(this.indexTypeValueCondition(AuditableItemGraphService._INDEX_TYPE_VERTEX, comparison, term));
|
|
713
737
|
}
|
|
714
738
|
if (idMode === "alias" || idMode === "both") {
|
|
715
|
-
|
|
716
|
-
property: "aliasIndex",
|
|
717
|
-
comparison: ComparisonOperator.Includes,
|
|
718
|
-
value: idExact ? `||${idOrAlias.toLowerCase()}||` : idOrAlias.toLowerCase()
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
if (idComparators.length === 1) {
|
|
722
|
-
andGroups.push(idComparators[0]);
|
|
723
|
-
}
|
|
724
|
-
else if (idComparators.length > 1) {
|
|
725
|
-
andGroups.push({ logicalOperator: LogicalOperator.Or, conditions: idComparators });
|
|
739
|
+
indexPredicates.push(this.indexTypeValueCondition(AuditableItemGraphService._INDEX_TYPE_ALIAS, comparison, term));
|
|
726
740
|
}
|
|
727
741
|
}
|
|
728
|
-
if (
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
comparison: ComparisonOperator.Includes,
|
|
732
|
-
value: `||${rt.toLowerCase()}||`
|
|
733
|
-
}));
|
|
734
|
-
if (resourceComparators.length === 1) {
|
|
735
|
-
andGroups.push(resourceComparators[0]);
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
andGroups.push({
|
|
739
|
-
logicalOperator: LogicalOperator.Or,
|
|
740
|
-
conditions: resourceComparators
|
|
741
|
-
});
|
|
742
|
+
else if (hasResourceTypes) {
|
|
743
|
+
for (const resourceType of resourceTypes) {
|
|
744
|
+
indexPredicates.push(this.indexTypeValueCondition(AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE, ComparisonOperator.Equals, resourceType.toLowerCase()));
|
|
742
745
|
}
|
|
743
746
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
747
|
+
if (indexPredicates.length > 0) {
|
|
748
|
+
const indexPage = await this.queryIndexVertexIdPage(indexPredicates, orderProperty, orderDirection, this.decodeCursor("i", cursor), limit);
|
|
749
|
+
let pageVertexIds = indexPage.vertexIds;
|
|
750
|
+
// An OR over the index gives the union, so when an id or alias filter is combined
|
|
751
|
+
// with resource types the page is narrowed to the intersection here.
|
|
752
|
+
if (hasIdFilter && hasResourceTypes) {
|
|
753
|
+
pageVertexIds = await this.filterVertexIdsByResourceTypes(pageVertexIds, resourceTypes.map(resourceType => resourceType.toLowerCase()));
|
|
754
|
+
}
|
|
755
|
+
const indexEntities = await this.fetchVerticesInOrder(pageVertexIds, propertiesToReturn, conditions);
|
|
756
|
+
return await this.buildVertexList(indexEntities, indexPage.hasMore
|
|
757
|
+
? this.encodeCursor({
|
|
758
|
+
s: "i",
|
|
759
|
+
d: indexPage.keySet?.d,
|
|
760
|
+
i: indexPage.keySet?.i
|
|
761
|
+
})
|
|
762
|
+
: undefined);
|
|
746
763
|
}
|
|
747
|
-
const finalConditions =
|
|
748
|
-
logicalOperator: LogicalOperator.And,
|
|
749
|
-
|
|
750
|
-
|
|
764
|
+
const finalConditions = Is.empty(conditions)
|
|
765
|
+
? { logicalOperator: LogicalOperator.And, conditions: [] }
|
|
766
|
+
: conditions;
|
|
767
|
+
// With no id, alias or resource type filter there is nothing for the index to answer,
|
|
768
|
+
// so the vertex storage is queried directly and paged by its own cursor.
|
|
751
769
|
const results = await this._vertexStorage.query(finalConditions, [
|
|
752
770
|
{
|
|
753
771
|
property: orderProperty,
|
|
@@ -757,28 +775,10 @@ export class AuditableItemGraphService {
|
|
|
757
775
|
property: "id",
|
|
758
776
|
sortDirection: SortDirection.Ascending
|
|
759
777
|
}
|
|
760
|
-
], propertiesToReturn, cursor, limit);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
SchemaOrgContexts.Context,
|
|
765
|
-
AuditableItemGraphContexts.Context,
|
|
766
|
-
AuditableItemGraphContexts.ContextCommon
|
|
767
|
-
],
|
|
768
|
-
type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexList],
|
|
769
|
-
[SchemaOrgTypes.ItemListElement]: models
|
|
770
|
-
};
|
|
771
|
-
const result = await JsonLdProcessor.compact(vertexList, vertexList["@context"], {
|
|
772
|
-
compactArrays: false
|
|
773
|
-
});
|
|
774
|
-
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.QueriesExecuted, {
|
|
775
|
-
resultCount: models.length,
|
|
776
|
-
hasMore: Is.stringValue(results.cursor)
|
|
777
|
-
});
|
|
778
|
-
return {
|
|
779
|
-
entries: result,
|
|
780
|
-
cursor: results.cursor
|
|
781
|
-
};
|
|
778
|
+
], propertiesToReturn, this.decodeCursor("v", cursor)?.c, limit);
|
|
779
|
+
return await this.buildVertexList(results.entities, Is.stringValue(results.cursor)
|
|
780
|
+
? this.encodeCursor({ s: "v", c: results.cursor })
|
|
781
|
+
: undefined);
|
|
782
782
|
}
|
|
783
783
|
catch (error) {
|
|
784
784
|
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "queryingFailed", undefined, error);
|
|
@@ -851,7 +851,7 @@ export class AuditableItemGraphService {
|
|
|
851
851
|
* @internal
|
|
852
852
|
*/
|
|
853
853
|
async compactVertexHistory(vertexId) {
|
|
854
|
-
const changesets = await this.internalGetChangesets(vertexId);
|
|
854
|
+
const changesets = await this.internalGetChangesets(vertexId, ["proofId"]);
|
|
855
855
|
for (const changeset of changesets) {
|
|
856
856
|
await this._changesetStorage.remove(changeset.id);
|
|
857
857
|
if (Is.stringValue(changeset.proofId)) {
|
|
@@ -878,17 +878,14 @@ export class AuditableItemGraphService {
|
|
|
878
878
|
return;
|
|
879
879
|
}
|
|
880
880
|
const nextVersion = Is.empty(originalEntity.version)
|
|
881
|
-
? (await this.internalGetChangesets(vertexId)).length
|
|
881
|
+
? (await this.internalGetChangesets(vertexId, [])).length
|
|
882
882
|
: originalEntity.version + 1;
|
|
883
883
|
const patches = await this.addChangeset(context, originalEntity, newEntity, false, nextVersion);
|
|
884
884
|
if (patches.length > 0) {
|
|
885
885
|
newEntity.dateModified = context.now;
|
|
886
886
|
newEntity.version = nextVersion;
|
|
887
|
-
|
|
888
|
-
await this.
|
|
889
|
-
...newEntity,
|
|
890
|
-
...indexes
|
|
891
|
-
});
|
|
887
|
+
await this._vertexStorage.set(newEntity);
|
|
888
|
+
await this.syncVertexIndexes(newEntity);
|
|
892
889
|
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
|
|
893
890
|
patchCount: patches.length
|
|
894
891
|
});
|
|
@@ -917,11 +914,8 @@ export class AuditableItemGraphService {
|
|
|
917
914
|
await this.compactVertexHistory(vertexId);
|
|
918
915
|
}
|
|
919
916
|
newEntity.dateModified = context.now;
|
|
920
|
-
|
|
921
|
-
await this.
|
|
922
|
-
...newEntity,
|
|
923
|
-
...indexes
|
|
924
|
-
});
|
|
917
|
+
await this._vertexStorage.set(newEntity);
|
|
918
|
+
await this.syncVertexIndexes(newEntity);
|
|
925
919
|
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.VerticesUpdated, {
|
|
926
920
|
patchCount: patches.length
|
|
927
921
|
});
|
|
@@ -1061,15 +1055,24 @@ export class AuditableItemGraphService {
|
|
|
1061
1055
|
/**
|
|
1062
1056
|
* Fetch all changesets for a vertex in ascending date order.
|
|
1063
1057
|
* @param vertexId The internal vertex id.
|
|
1058
|
+
* @param properties The additional changeset properties to retrieve, the id and dateCreated sort
|
|
1059
|
+
* keys are always included. Only the requested properties are populated on the returned
|
|
1060
|
+
* entities, so the large patches payload is only transferred when a caller reads it.
|
|
1064
1061
|
* @param options Optional filtering options.
|
|
1065
1062
|
* @param options.before Only fetch changesets created strictly before this ISO 8601 timestamp.
|
|
1066
1063
|
* @param options.maxVersion Only fetch changesets with version <= this value.
|
|
1067
1064
|
* @returns All changeset entities sorted ascending by dateCreated.
|
|
1068
1065
|
* @internal
|
|
1069
1066
|
*/
|
|
1070
|
-
async internalGetChangesets(vertexId, options) {
|
|
1067
|
+
async internalGetChangesets(vertexId, properties, options) {
|
|
1071
1068
|
const all = [];
|
|
1072
1069
|
let cursor;
|
|
1070
|
+
const propertiesToReturn = ["id", "dateCreated"];
|
|
1071
|
+
for (const property of properties) {
|
|
1072
|
+
if (!propertiesToReturn.includes(property)) {
|
|
1073
|
+
propertiesToReturn.push(property);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1073
1076
|
const conditions = [
|
|
1074
1077
|
{ property: "vertexId", value: vertexId, comparison: ComparisonOperator.Equals }
|
|
1075
1078
|
];
|
|
@@ -1088,7 +1091,7 @@ export class AuditableItemGraphService {
|
|
|
1088
1091
|
});
|
|
1089
1092
|
}
|
|
1090
1093
|
do {
|
|
1091
|
-
const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }],
|
|
1094
|
+
const result = await this._changesetStorage.query({ conditions, logicalOperator: LogicalOperator.And }, [{ property: "dateCreated", sortDirection: SortDirection.Ascending }], propertiesToReturn, cursor);
|
|
1092
1095
|
all.push(...result.entities);
|
|
1093
1096
|
cursor = result.cursor;
|
|
1094
1097
|
} while (Is.stringValue(cursor));
|
|
@@ -1103,7 +1106,7 @@ export class AuditableItemGraphService {
|
|
|
1103
1106
|
* @internal
|
|
1104
1107
|
*/
|
|
1105
1108
|
async auditedVersions(vertexId, afterDate, beforeDate) {
|
|
1106
|
-
const allChangesets = await this.internalGetChangesets(vertexId, {
|
|
1109
|
+
const allChangesets = await this.internalGetChangesets(vertexId, ["version"], {
|
|
1107
1110
|
before: beforeDate?.toISOString()
|
|
1108
1111
|
});
|
|
1109
1112
|
const versions = [];
|
|
@@ -1596,32 +1599,385 @@ export class AuditableItemGraphService {
|
|
|
1596
1599
|
ObjectHelper.extractProperty(resource.resourceObject, ["id", "@id"], false));
|
|
1597
1600
|
}
|
|
1598
1601
|
/**
|
|
1599
|
-
*
|
|
1600
|
-
* @param vertex The vertex to
|
|
1601
|
-
* @returns
|
|
1602
|
+
* Synchronise the index storage with the current state of a vertex.
|
|
1603
|
+
* @param vertex The vertex to synchronise the index entries for.
|
|
1604
|
+
* @returns A promise that resolves when the index entries match the vertex.
|
|
1605
|
+
* @internal
|
|
1606
|
+
*/
|
|
1607
|
+
async syncVertexIndexes(vertex) {
|
|
1608
|
+
const required = new Map();
|
|
1609
|
+
// An entry always carries a modified date so a query ordered by it can be paged by the
|
|
1610
|
+
// same key set as the creation date, falling back to the creation date for a vertex which
|
|
1611
|
+
// has never been modified rather than leaving the column empty.
|
|
1612
|
+
const indexDateModified = vertex.dateModified ?? vertex.dateCreated;
|
|
1613
|
+
// Every vertex carries an entry for its own id, so a query matching on the vertex id can
|
|
1614
|
+
// be answered from the index alone. Without it a vertex holding no aliases and no
|
|
1615
|
+
// resources would have no entries at all and could never be matched here.
|
|
1616
|
+
const vertexIdValue = vertex.id.toLowerCase();
|
|
1617
|
+
required.set(`${AuditableItemGraphService._INDEX_TYPE_VERTEX}|${vertexIdValue}`, {
|
|
1618
|
+
type: AuditableItemGraphService._INDEX_TYPE_VERTEX,
|
|
1619
|
+
value: vertexIdValue
|
|
1620
|
+
});
|
|
1621
|
+
// Index values are case folded here, and every lookup folds its term to match, so the
|
|
1622
|
+
// comparisons stay case insensitive without relying on the column collation or on the
|
|
1623
|
+
// connector folding case itself. The vertex keeps the value exactly as supplied.
|
|
1624
|
+
for (const alias of vertex.aliases ?? []) {
|
|
1625
|
+
if (Is.empty(alias.dateDeleted) && Is.stringValue(alias.id)) {
|
|
1626
|
+
const value = alias.id.toLowerCase();
|
|
1627
|
+
required.set(`${AuditableItemGraphService._INDEX_TYPE_ALIAS}|${value}`, {
|
|
1628
|
+
type: AuditableItemGraphService._INDEX_TYPE_ALIAS,
|
|
1629
|
+
value
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
for (const resource of vertex.resources ?? []) {
|
|
1634
|
+
const resourceType = ObjectHelper.extractProperty(resource.resourceObject, ["@type", "type"], false);
|
|
1635
|
+
if (Is.stringValue(resourceType)) {
|
|
1636
|
+
const value = resourceType.toLowerCase();
|
|
1637
|
+
required.set(`${AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE}|${value}`, {
|
|
1638
|
+
type: AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE,
|
|
1639
|
+
value
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const existing = await this.queryVertexIndexes({
|
|
1644
|
+
property: "vertexId",
|
|
1645
|
+
comparison: ComparisonOperator.Equals,
|
|
1646
|
+
value: vertex.id
|
|
1647
|
+
}, ["id", "type", "value", "dateCreated", "dateModified"]);
|
|
1648
|
+
// Collapse by row id first. A paged read can return the same row on more than one page,
|
|
1649
|
+
// and without this a retained row could be queued for removal by its own duplicate.
|
|
1650
|
+
const existingById = new Map();
|
|
1651
|
+
for (const entry of existing) {
|
|
1652
|
+
if (Is.stringValue(entry.id)) {
|
|
1653
|
+
existingById.set(entry.id, entry);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const retainedKeys = new Set();
|
|
1657
|
+
const removeIds = [];
|
|
1658
|
+
const writeEntries = [];
|
|
1659
|
+
for (const [id, entry] of existingById) {
|
|
1660
|
+
// Entries which are no longer required, and any duplicates of a retained entry,
|
|
1661
|
+
// are removed so exactly one entry remains per index value.
|
|
1662
|
+
const key = `${entry.type}|${entry.value}`;
|
|
1663
|
+
const requiredEntry = required.get(key);
|
|
1664
|
+
if (!Is.empty(requiredEntry) && !retainedKeys.has(key)) {
|
|
1665
|
+
retainedKeys.add(key);
|
|
1666
|
+
if (entry.dateCreated !== vertex.dateCreated || entry.dateModified !== indexDateModified) {
|
|
1667
|
+
// Keep the row id so the copied dates are corrected in place rather than duplicated.
|
|
1668
|
+
writeEntries.push({
|
|
1669
|
+
id,
|
|
1670
|
+
vertexId: vertex.id,
|
|
1671
|
+
type: requiredEntry.type,
|
|
1672
|
+
value: requiredEntry.value,
|
|
1673
|
+
dateCreated: vertex.dateCreated,
|
|
1674
|
+
dateModified: indexDateModified
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
removeIds.push(id);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (const [key, entry] of required) {
|
|
1683
|
+
if (!retainedKeys.has(key)) {
|
|
1684
|
+
writeEntries.push({
|
|
1685
|
+
id: Converter.bytesToHex(RandomHelper.generate(16)),
|
|
1686
|
+
vertexId: vertex.id,
|
|
1687
|
+
type: entry.type,
|
|
1688
|
+
value: entry.value,
|
|
1689
|
+
dateCreated: vertex.dateCreated,
|
|
1690
|
+
dateModified: indexDateModified
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (removeIds.length > 0) {
|
|
1695
|
+
await this._vertexIndexStorage.removeBatch(removeIds);
|
|
1696
|
+
}
|
|
1697
|
+
if (writeEntries.length > 0) {
|
|
1698
|
+
await this._vertexIndexStorage.setBatch(writeEntries);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Build the JSON-LD vertex list returned by query, and record the query metric.
|
|
1703
|
+
* @param entities The vertex entities for the page.
|
|
1704
|
+
* @param cursor The already encoded cursor for the next page.
|
|
1705
|
+
* @returns The compacted vertex list and the cursor.
|
|
1706
|
+
* @internal
|
|
1707
|
+
*/
|
|
1708
|
+
async buildVertexList(entities, cursor) {
|
|
1709
|
+
const models = entities.map(e => this.vertexEntityToJsonLd(e));
|
|
1710
|
+
const vertexList = {
|
|
1711
|
+
"@context": [
|
|
1712
|
+
SchemaOrgContexts.Context,
|
|
1713
|
+
AuditableItemGraphContexts.Context,
|
|
1714
|
+
AuditableItemGraphContexts.ContextCommon
|
|
1715
|
+
],
|
|
1716
|
+
type: [SchemaOrgTypes.ItemList, AuditableItemGraphTypes.VertexList],
|
|
1717
|
+
[SchemaOrgTypes.ItemListElement]: models
|
|
1718
|
+
};
|
|
1719
|
+
const result = await JsonLdProcessor.compact(vertexList, vertexList["@context"], {
|
|
1720
|
+
compactArrays: false
|
|
1721
|
+
});
|
|
1722
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemGraphMetricIds.QueriesExecuted, {
|
|
1723
|
+
resultCount: models.length,
|
|
1724
|
+
hasMore: Is.stringValue(cursor)
|
|
1725
|
+
});
|
|
1726
|
+
return {
|
|
1727
|
+
entries: result,
|
|
1728
|
+
cursor
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Encode a storage cursor into the opaque cursor handed back to callers.
|
|
1733
|
+
* Every cursor this service returns is in this format, so a cursor which does not decode
|
|
1734
|
+
* is invalid rather than a cursor from somewhere else.
|
|
1735
|
+
* @param payload The position to encode.
|
|
1736
|
+
* @param payload.s The strategy the position belongs to, i for index paged, v for vertex paged.
|
|
1737
|
+
* @param payload.d The ordering date of the last vertex returned, index strategy only.
|
|
1738
|
+
* @param payload.i The id of the last vertex returned, index strategy only.
|
|
1739
|
+
* @param payload.c The storage connector cursor, vertex strategy only.
|
|
1740
|
+
* @returns The encoded cursor, or undefined when there is no further page.
|
|
1741
|
+
* @internal
|
|
1742
|
+
*/
|
|
1743
|
+
encodeCursor(payload) {
|
|
1744
|
+
return Converter.bytesToBase64(ObjectHelper.toBytes(payload));
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Decode a cursor produced by encodeCursor.
|
|
1748
|
+
* @param strategy The strategy the current query is using.
|
|
1749
|
+
* @param cursor The cursor supplied by the caller.
|
|
1750
|
+
* @returns The decoded payload, or undefined when no cursor was supplied.
|
|
1751
|
+
* @throws GeneralError If the cursor is malformed or belongs to a different strategy.
|
|
1602
1752
|
* @internal
|
|
1603
1753
|
*/
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1754
|
+
decodeCursor(strategy, cursor) {
|
|
1755
|
+
if (!Is.stringValue(cursor)) {
|
|
1756
|
+
return undefined;
|
|
1757
|
+
}
|
|
1758
|
+
let decoded;
|
|
1759
|
+
if (Is.stringBase64(cursor)) {
|
|
1760
|
+
try {
|
|
1761
|
+
decoded = ObjectHelper.fromBytes(Converter.base64ToBytes(cursor));
|
|
1762
|
+
}
|
|
1763
|
+
catch (error) {
|
|
1764
|
+
// Base64 which is not an encoded cursor is still only an invalid cursor, so every
|
|
1765
|
+
// malformed cursor reports the same failure rather than leaking a parse error.
|
|
1766
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "invalidCursor", undefined, error);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
// A cursor from a different strategy cannot be applied to this query, and silently
|
|
1770
|
+
// restarting from the first page would loop a caller which keeps following the cursor.
|
|
1771
|
+
const populated = decoded?.s === strategy &&
|
|
1772
|
+
(strategy === "v"
|
|
1773
|
+
? Is.stringValue(decoded.c)
|
|
1774
|
+
: Is.stringValue(decoded.d) && Is.stringValue(decoded.i));
|
|
1775
|
+
if (!populated) {
|
|
1776
|
+
throw new GeneralError(AuditableItemGraphService.CLASS_NAME, "invalidCursor");
|
|
1777
|
+
}
|
|
1778
|
+
return decoded;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Build the condition matching one index type and value.
|
|
1782
|
+
* @param type The index type to match.
|
|
1783
|
+
* @param comparison The comparison to apply to the value.
|
|
1784
|
+
* @param value The index value to match, already lower cased.
|
|
1785
|
+
* @returns The condition.
|
|
1786
|
+
* @internal
|
|
1787
|
+
*/
|
|
1788
|
+
indexTypeValueCondition(type, comparison, value) {
|
|
1789
|
+
return {
|
|
1790
|
+
logicalOperator: LogicalOperator.And,
|
|
1791
|
+
conditions: [
|
|
1792
|
+
{ property: "type", comparison: ComparisonOperator.Equals, value: type },
|
|
1793
|
+
{ property: "value", comparison, value }
|
|
1794
|
+
]
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Build the condition which resumes an index scan after a position.
|
|
1799
|
+
* Every entry for a vertex carries the same date and vertex id, so a position on that pair
|
|
1800
|
+
* steps past all the remaining entries of the vertex it names.
|
|
1801
|
+
* @param orderProperty The index date property being ordered by.
|
|
1802
|
+
* @param orderDirection The direction being ordered in.
|
|
1803
|
+
* @param lastDate The date of the last vertex returned.
|
|
1804
|
+
* @param lastVertexId The id of the last vertex returned.
|
|
1805
|
+
* @returns The condition.
|
|
1806
|
+
* @internal
|
|
1807
|
+
*/
|
|
1808
|
+
indexKeySetCondition(orderProperty, orderDirection, lastDate, lastVertexId) {
|
|
1809
|
+
const dateComparison = orderDirection === SortDirection.Ascending
|
|
1810
|
+
? ComparisonOperator.GreaterThan
|
|
1811
|
+
: ComparisonOperator.LessThan;
|
|
1812
|
+
return {
|
|
1813
|
+
logicalOperator: LogicalOperator.Or,
|
|
1814
|
+
conditions: [
|
|
1815
|
+
{ property: orderProperty, comparison: dateComparison, value: lastDate },
|
|
1816
|
+
{
|
|
1817
|
+
logicalOperator: LogicalOperator.And,
|
|
1818
|
+
conditions: [
|
|
1819
|
+
{ property: orderProperty, comparison: ComparisonOperator.Equals, value: lastDate },
|
|
1820
|
+
{
|
|
1821
|
+
property: "vertexId",
|
|
1822
|
+
comparison: ComparisonOperator.GreaterThan,
|
|
1823
|
+
value: lastVertexId
|
|
1824
|
+
}
|
|
1825
|
+
]
|
|
1826
|
+
}
|
|
1827
|
+
]
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Read one page of distinct vertex ids from the index storage using a key set, so a broad
|
|
1832
|
+
* match is never drained into memory however many entries it has.
|
|
1833
|
+
* @param predicates The type and value conditions to match, combined with OR.
|
|
1834
|
+
* @param orderProperty The index date property to order by.
|
|
1835
|
+
* @param orderDirection The direction to order in.
|
|
1836
|
+
* @param keySet The position returned by the previous page.
|
|
1837
|
+
* @param keySet.d The ordering date of the last vertex returned.
|
|
1838
|
+
* @param keySet.i The id of the last vertex returned.
|
|
1839
|
+
* @param limit The maximum number of distinct vertices to return.
|
|
1840
|
+
* @returns The vertex ids in order, the position to resume from, and whether more remain.
|
|
1841
|
+
* @internal
|
|
1842
|
+
*/
|
|
1843
|
+
async queryIndexVertexIdPage(predicates, orderProperty, orderDirection, keySet, limit) {
|
|
1844
|
+
const pageSize = limit ?? AuditableItemGraphService._DEFAULT_PAGE_SIZE;
|
|
1845
|
+
const match = predicates.length === 1
|
|
1846
|
+
? predicates[0]
|
|
1847
|
+
: { logicalOperator: LogicalOperator.Or, conditions: predicates };
|
|
1848
|
+
const vertexIds = [];
|
|
1849
|
+
const positions = [];
|
|
1850
|
+
const seen = new Set();
|
|
1851
|
+
let scan = keySet;
|
|
1852
|
+
let exhausted = false;
|
|
1853
|
+
// One extra vertex is collected to tell whether a further page exists. Each pass either
|
|
1854
|
+
// adds a vertex or exhausts the match, so this runs at most pageSize + 1 times.
|
|
1855
|
+
while (vertexIds.length <= pageSize && !exhausted) {
|
|
1856
|
+
const conditions = [match];
|
|
1857
|
+
if (Is.stringValue(scan?.d) && Is.stringValue(scan?.i)) {
|
|
1858
|
+
conditions.push(this.indexKeySetCondition(orderProperty, orderDirection, scan.d, scan.i));
|
|
1859
|
+
}
|
|
1860
|
+
const results = await this._vertexIndexStorage.query({ logicalOperator: LogicalOperator.And, conditions }, [
|
|
1861
|
+
{ property: orderProperty, sortDirection: orderDirection },
|
|
1862
|
+
{ property: "vertexId", sortDirection: SortDirection.Ascending }
|
|
1863
|
+
], ["vertexId", orderProperty], undefined, pageSize + 1);
|
|
1864
|
+
if (results.entities.length === 0) {
|
|
1865
|
+
exhausted = true;
|
|
1866
|
+
}
|
|
1867
|
+
for (const entity of results.entities) {
|
|
1868
|
+
const entityDate = entity[orderProperty];
|
|
1869
|
+
if (Is.stringValue(entity.vertexId) && Is.stringValue(entityDate)) {
|
|
1870
|
+
const position = { d: entityDate, i: entity.vertexId };
|
|
1871
|
+
scan = position;
|
|
1872
|
+
if (!seen.has(entity.vertexId) && vertexIds.length <= pageSize) {
|
|
1873
|
+
seen.add(entity.vertexId);
|
|
1874
|
+
vertexIds.push(entity.vertexId);
|
|
1875
|
+
positions.push(position);
|
|
1876
|
+
}
|
|
1616
1877
|
}
|
|
1617
1878
|
}
|
|
1618
1879
|
}
|
|
1619
|
-
|
|
1880
|
+
let hasMore = false;
|
|
1881
|
+
if (vertexIds.length > pageSize) {
|
|
1882
|
+
vertexIds.length = pageSize;
|
|
1883
|
+
positions.length = pageSize;
|
|
1884
|
+
hasMore = true;
|
|
1885
|
+
}
|
|
1620
1886
|
return {
|
|
1621
|
-
|
|
1622
|
-
|
|
1887
|
+
vertexIds,
|
|
1888
|
+
keySet: positions.length > 0 ? positions[positions.length - 1] : undefined,
|
|
1889
|
+
hasMore
|
|
1623
1890
|
};
|
|
1624
1891
|
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Narrow a page of vertex ids to those which also carry one of the resource types.
|
|
1894
|
+
* @param vertexIds The candidate vertex ids.
|
|
1895
|
+
* @param resourceTypes The resource types to match, already lower cased.
|
|
1896
|
+
* @returns The matching vertex ids, keeping the incoming order.
|
|
1897
|
+
* @internal
|
|
1898
|
+
*/
|
|
1899
|
+
async filterVertexIdsByResourceTypes(vertexIds, resourceTypes) {
|
|
1900
|
+
if (vertexIds.length === 0) {
|
|
1901
|
+
return [];
|
|
1902
|
+
}
|
|
1903
|
+
const results = await this._vertexIndexStorage.query({
|
|
1904
|
+
logicalOperator: LogicalOperator.And,
|
|
1905
|
+
conditions: [
|
|
1906
|
+
{
|
|
1907
|
+
property: "type",
|
|
1908
|
+
comparison: ComparisonOperator.Equals,
|
|
1909
|
+
value: AuditableItemGraphService._INDEX_TYPE_RESOURCE_TYPE
|
|
1910
|
+
},
|
|
1911
|
+
{ property: "value", comparison: ComparisonOperator.In, value: resourceTypes },
|
|
1912
|
+
{ property: "vertexId", comparison: ComparisonOperator.In, value: vertexIds }
|
|
1913
|
+
]
|
|
1914
|
+
}, undefined, ["vertexId"], undefined, vertexIds.length * resourceTypes.length);
|
|
1915
|
+
const matched = new Set();
|
|
1916
|
+
for (const entity of results.entities) {
|
|
1917
|
+
if (Is.stringValue(entity.vertexId)) {
|
|
1918
|
+
matched.add(entity.vertexId);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return vertexIds.filter(vertexId => matched.has(vertexId));
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Read the vertices for a page of ids, keeping the order the ids were given in.
|
|
1925
|
+
* @param vertexIds The vertex ids to read.
|
|
1926
|
+
* @param propertiesToReturn The vertex properties to return.
|
|
1927
|
+
* @param conditions Additional conditions the vertices must match.
|
|
1928
|
+
* @returns The vertices which exist and match, in the order of the ids.
|
|
1929
|
+
* @internal
|
|
1930
|
+
*/
|
|
1931
|
+
async fetchVerticesInOrder(vertexIds, propertiesToReturn, conditions) {
|
|
1932
|
+
if (vertexIds.length === 0) {
|
|
1933
|
+
return [];
|
|
1934
|
+
}
|
|
1935
|
+
const idCondition = {
|
|
1936
|
+
property: "id",
|
|
1937
|
+
comparison: ComparisonOperator.In,
|
|
1938
|
+
value: vertexIds
|
|
1939
|
+
};
|
|
1940
|
+
const results = await this._vertexStorage.query(Is.empty(conditions)
|
|
1941
|
+
? idCondition
|
|
1942
|
+
: {
|
|
1943
|
+
logicalOperator: LogicalOperator.And,
|
|
1944
|
+
conditions: [idCondition, conditions]
|
|
1945
|
+
}, undefined, propertiesToReturn, undefined, vertexIds.length);
|
|
1946
|
+
// An In lookup has no guaranteed order, so the index ordering is reapplied here.
|
|
1947
|
+
const vertexById = new Map();
|
|
1948
|
+
for (const entity of results.entities) {
|
|
1949
|
+
if (Is.stringValue(entity.id)) {
|
|
1950
|
+
vertexById.set(entity.id, entity);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
const ordered = [];
|
|
1954
|
+
for (const vertexId of vertexIds) {
|
|
1955
|
+
const entity = vertexById.get(vertexId);
|
|
1956
|
+
if (!Is.empty(entity)) {
|
|
1957
|
+
ordered.push(entity);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
return ordered;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Query all the pages of the vertex index storage which match the conditions.
|
|
1964
|
+
* @param conditions The conditions to match.
|
|
1965
|
+
* @param properties The properties to return.
|
|
1966
|
+
* @returns The matching index entries.
|
|
1967
|
+
* @internal
|
|
1968
|
+
*/
|
|
1969
|
+
async queryVertexIndexes(conditions, properties) {
|
|
1970
|
+
const entities = [];
|
|
1971
|
+
let cursor;
|
|
1972
|
+
do {
|
|
1973
|
+
// Paging needs a deterministic order or a row can be repeated or skipped between
|
|
1974
|
+
// pages, the entity declares no default sort so the primary key is used.
|
|
1975
|
+
const results = await this._vertexIndexStorage.query(conditions, [{ property: "id", sortDirection: SortDirection.Ascending }], properties, cursor);
|
|
1976
|
+
entities.push(...results.entities);
|
|
1977
|
+
cursor = results.cursor;
|
|
1978
|
+
} while (Is.stringValue(cursor));
|
|
1979
|
+
return entities;
|
|
1980
|
+
}
|
|
1625
1981
|
/**
|
|
1626
1982
|
* Find vertices with matching aliases.
|
|
1627
1983
|
* @param vertexId The id of the vertex to exclude from the search.
|
|
@@ -1630,22 +1986,27 @@ export class AuditableItemGraphService {
|
|
|
1630
1986
|
* @internal
|
|
1631
1987
|
*/
|
|
1632
1988
|
async findMatchingVertices(vertexId, aliasId) {
|
|
1633
|
-
const
|
|
1989
|
+
const entities = await this.queryVertexIndexes({
|
|
1990
|
+
logicalOperator: LogicalOperator.And,
|
|
1634
1991
|
conditions: [
|
|
1635
1992
|
{
|
|
1636
|
-
property: "
|
|
1637
|
-
comparison: ComparisonOperator.
|
|
1638
|
-
value:
|
|
1993
|
+
property: "type",
|
|
1994
|
+
comparison: ComparisonOperator.Equals,
|
|
1995
|
+
value: AuditableItemGraphService._INDEX_TYPE_ALIAS
|
|
1639
1996
|
},
|
|
1640
1997
|
{
|
|
1641
|
-
property: "
|
|
1642
|
-
|
|
1643
|
-
|
|
1998
|
+
property: "value",
|
|
1999
|
+
comparison: ComparisonOperator.Equals,
|
|
2000
|
+
value: aliasId.toLowerCase()
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
property: "vertexId",
|
|
2004
|
+
comparison: ComparisonOperator.NotEquals,
|
|
2005
|
+
value: vertexId
|
|
1644
2006
|
}
|
|
1645
|
-
]
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
return results.entities.length > 0;
|
|
2007
|
+
]
|
|
2008
|
+
}, ["vertexId"]);
|
|
2009
|
+
return entities.length > 0;
|
|
1649
2010
|
}
|
|
1650
2011
|
/**
|
|
1651
2012
|
* Whether an incoming edge matches a stored edge id.
|