@twin.org/auditable-item-stream-service 0.0.3-next.2 → 0.0.3-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/es/auditableItemStreamRoutes.js +471 -130
- package/dist/es/auditableItemStreamRoutes.js.map +1 -1
- package/dist/es/auditableItemStreamService.js +330 -138
- package/dist/es/auditableItemStreamService.js.map +1 -1
- package/dist/es/entities/auditableItemStream.js +20 -6
- package/dist/es/entities/auditableItemStream.js.map +1 -1
- package/dist/es/models/IAuditableItemStreamServiceConstructorOptions.js.map +1 -1
- package/dist/es/models/IAuditableItemStreamServiceContext.js.map +1 -1
- package/dist/types/auditableItemStreamRoutes.d.ts +33 -1
- package/dist/types/auditableItemStreamService.d.ts +44 -37
- package/dist/types/entities/auditableItemStream.d.ts +12 -3
- package/dist/types/models/IAuditableItemStreamServiceConstructorOptions.d.ts +4 -0
- package/dist/types/models/IAuditableItemStreamServiceContext.d.ts +4 -0
- package/docs/changelog.md +355 -77
- package/docs/examples.md +211 -1
- package/docs/open-api/spec.json +839 -177
- package/docs/reference/classes/AuditableItemStream.md +32 -16
- package/docs/reference/classes/AuditableItemStreamEntry.md +13 -13
- package/docs/reference/classes/AuditableItemStreamService.md +107 -84
- package/docs/reference/functions/auditableItemStreamClose.md +31 -0
- package/docs/reference/functions/auditableItemStreamListEntriesNoStream.md +31 -0
- package/docs/reference/functions/auditableItemStreamListEntryObjectsNoStream.md +31 -0
- package/docs/reference/functions/auditableItemStreamRemoveProof.md +31 -0
- package/docs/reference/index.md +4 -0
- package/docs/reference/interfaces/IAuditableItemStreamServiceConfig.md +2 -2
- package/docs/reference/interfaces/IAuditableItemStreamServiceConstructorOptions.md +18 -10
- package/locales/en.json +6 -1
- package/package.json +6 -5
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Copyright 2024 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
-
import { AuditableItemStreamContexts, AuditableItemStreamTopics, AuditableItemStreamTypes } from "@twin.org/auditable-item-stream-models";
|
|
4
|
-
import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
5
|
-
import { Coerce, ComponentFactory,
|
|
6
|
-
import {
|
|
3
|
+
import { AuditableItemStreamContexts, AuditableItemStreamDataTypes, AuditableItemStreamMetricIds, AuditableItemStreamMetrics, AuditableItemStreamModes, AuditableItemStreamTopics, AuditableItemStreamTypes } from "@twin.org/auditable-item-stream-models";
|
|
4
|
+
import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
5
|
+
import { Coerce, ComponentFactory, GeneralError, Guards, Is, Mutex, NotFoundError, ObjectHelper, RandomHelper, Urn, Validation } from "@twin.org/core";
|
|
6
|
+
import { DataTypeHelper } from "@twin.org/data-core";
|
|
7
|
+
import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
|
|
7
8
|
import { ComparisonOperator, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
8
9
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
9
|
-
import { ImmutableProofContexts } from "@twin.org/immutable-proof-models";
|
|
10
|
+
import { ImmutableProofContexts, ImmutableProofDataTypes } from "@twin.org/immutable-proof-models";
|
|
10
11
|
import { SchemaOrgContexts, SchemaOrgDataTypes, SchemaOrgTypes } from "@twin.org/standards-schema-org";
|
|
12
|
+
import { MetricHelper } from "@twin.org/telemetry-models";
|
|
11
13
|
/**
|
|
12
14
|
* Class for performing auditable item stream operations.
|
|
13
15
|
*/
|
|
@@ -68,6 +70,11 @@ export class AuditableItemStreamService {
|
|
|
68
70
|
* @internal
|
|
69
71
|
*/
|
|
70
72
|
_eventBusComponent;
|
|
73
|
+
/**
|
|
74
|
+
* The telemetry component.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
_telemetryComponent;
|
|
71
78
|
/**
|
|
72
79
|
* The default interval for the integrity checks.
|
|
73
80
|
* @internal
|
|
@@ -84,9 +91,13 @@ export class AuditableItemStreamService {
|
|
|
84
91
|
if (Is.stringValue(options?.eventBusComponentType)) {
|
|
85
92
|
this._eventBusComponent = ComponentFactory.get(options.eventBusComponentType);
|
|
86
93
|
}
|
|
94
|
+
this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
|
|
87
95
|
this._config = options?.config ?? {};
|
|
88
96
|
this._defaultImmutableInterval = this._config.defaultImmutableInterval ?? 10;
|
|
89
97
|
SchemaOrgDataTypes.registerRedirects();
|
|
98
|
+
AuditableItemStreamDataTypes.registerTypes();
|
|
99
|
+
JsonLdDataTypes.registerTypes();
|
|
100
|
+
ImmutableProofDataTypes.registerTypes();
|
|
90
101
|
}
|
|
91
102
|
/**
|
|
92
103
|
* Returns the class name of the component.
|
|
@@ -95,75 +106,83 @@ export class AuditableItemStreamService {
|
|
|
95
106
|
className() {
|
|
96
107
|
return AuditableItemStreamService.CLASS_NAME;
|
|
97
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Register all AIS metrics with the telemetry component.
|
|
111
|
+
*/
|
|
112
|
+
async start() {
|
|
113
|
+
if (Is.undefined(this._telemetryComponent)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await MetricHelper.createMetrics(this._telemetryComponent, AuditableItemStreamMetrics);
|
|
117
|
+
}
|
|
98
118
|
/**
|
|
99
119
|
* Create a new stream.
|
|
100
120
|
* @param stream The stream to create.
|
|
101
|
-
* @param stream.annotationObject The object for the stream as JSON-LD.
|
|
102
|
-
* @param stream.entries Entries to store in the stream.
|
|
103
|
-
* @param options Options for creating the stream.
|
|
104
|
-
* @param options.immutableInterval After how many entries do we add immutable checks, defaults to service configured value.
|
|
105
|
-
* A value of 0 will disable integrity checks, 1 will be every item, or any other integer for an interval.
|
|
106
121
|
* @returns The id of the new stream item.
|
|
107
122
|
*/
|
|
108
|
-
async create(stream
|
|
123
|
+
async create(stream) {
|
|
109
124
|
Guards.object(AuditableItemStreamService.CLASS_NAME, "stream", stream);
|
|
110
125
|
const contextIds = await ContextIdStore.getContextIds();
|
|
126
|
+
ContextIdHelper.guard(contextIds, ContextIdKeys.Organization);
|
|
111
127
|
try {
|
|
128
|
+
const ownerOrganizationId = contextIds[ContextIdKeys.UserOrganization] ?? contextIds[ContextIdKeys.Organization];
|
|
129
|
+
const id = RandomHelper.generateUuidV7("compact");
|
|
130
|
+
const schemaValidationFailures = [];
|
|
131
|
+
await DataTypeHelper.validate("stream", `${AuditableItemStreamContexts.Namespace}${AuditableItemStreamTypes.Stream}Base`, stream, schemaValidationFailures);
|
|
132
|
+
Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream", schemaValidationFailures);
|
|
133
|
+
if (stream.closed && !Is.arrayValue(stream.entries?.[SchemaOrgTypes.ItemListElement])) {
|
|
134
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "closedRequiresEntries");
|
|
135
|
+
}
|
|
112
136
|
if (Is.object(stream.annotationObject)) {
|
|
113
137
|
const validationFailures = [];
|
|
114
138
|
await JsonLdHelper.validate(stream.annotationObject, validationFailures);
|
|
115
139
|
Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream.annotationObject", validationFailures);
|
|
116
140
|
}
|
|
117
|
-
const id = Converter.bytesToHex(RandomHelper.generate(32), false);
|
|
118
141
|
const context = {
|
|
119
142
|
now: new Date(Date.now()).toISOString(),
|
|
120
143
|
contextIds,
|
|
121
144
|
indexCounter: 0,
|
|
122
|
-
immutableInterval:
|
|
145
|
+
immutableInterval: stream?.immutableInterval ?? this._defaultImmutableInterval,
|
|
146
|
+
organizationIdentity: ownerOrganizationId
|
|
123
147
|
};
|
|
124
148
|
const streamEntity = {
|
|
125
149
|
id,
|
|
126
|
-
organizationIdentity:
|
|
150
|
+
organizationIdentity: ownerOrganizationId,
|
|
127
151
|
userIdentity: contextIds?.[ContextIdKeys.User],
|
|
128
152
|
dateCreated: context.now,
|
|
129
153
|
immutableInterval: context.immutableInterval,
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
closed: stream.closed,
|
|
155
|
+
mode: stream.mode,
|
|
156
|
+
numberOfItems: 0
|
|
132
157
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Create the proof for the stream object
|
|
137
|
-
streamEntity.proofId = await this._immutableProofComponent.create(streamModel);
|
|
138
|
-
if (Is.arrayValue(stream.entries)) {
|
|
139
|
-
for (const entry of stream.entries) {
|
|
158
|
+
const streamUrn = await this.createStreamProof(streamEntity, context.immutableInterval);
|
|
159
|
+
if (Is.arrayValue(stream.entries?.[SchemaOrgTypes.ItemListElement])) {
|
|
160
|
+
for (const entry of stream.entries[SchemaOrgTypes.ItemListElement]) {
|
|
140
161
|
await this.setEntry(context, id, entry);
|
|
141
162
|
}
|
|
142
163
|
}
|
|
143
164
|
// Add these dynamic properties to the stream object after the proof has been created.
|
|
144
165
|
streamEntity.dateModified = context.now;
|
|
145
166
|
streamEntity.annotationObject = stream.annotationObject;
|
|
146
|
-
streamEntity.
|
|
167
|
+
streamEntity.numberOfItems = context.indexCounter;
|
|
147
168
|
await this._streamStorage.set(streamEntity);
|
|
148
|
-
await
|
|
149
|
-
|
|
169
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsCreated, {
|
|
170
|
+
mode: streamEntity.mode ?? AuditableItemStreamModes.Default,
|
|
171
|
+
immutableInterval: context.immutableInterval
|
|
172
|
+
});
|
|
173
|
+
await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamCreated, { id: streamUrn });
|
|
174
|
+
return streamUrn;
|
|
150
175
|
}
|
|
151
176
|
catch (error) {
|
|
152
177
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "createFailed", undefined, error);
|
|
153
178
|
}
|
|
154
179
|
}
|
|
155
180
|
/**
|
|
156
|
-
*
|
|
157
|
-
* @param id The id of the stream to
|
|
158
|
-
* @
|
|
159
|
-
* @param options.includeEntries Whether to include the entries, defaults to false.
|
|
160
|
-
* @param options.includeDeleted Whether to include deleted entries, defaults to false.
|
|
161
|
-
* @param options.verifyStream Should the stream be verified, defaults to false.
|
|
162
|
-
* @param options.verifyEntries Should the entries be verified, defaults to false.
|
|
163
|
-
* @returns The stream and entries if found.
|
|
164
|
-
* @throws NotFoundError if the stream is not found
|
|
181
|
+
* Close a stream.
|
|
182
|
+
* @param id The id of the stream to close.
|
|
183
|
+
* @returns Nothing.
|
|
165
184
|
*/
|
|
166
|
-
async
|
|
185
|
+
async close(id) {
|
|
167
186
|
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "id", id);
|
|
168
187
|
const urnParsed = Urn.fromValidString(id);
|
|
169
188
|
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
@@ -172,38 +191,31 @@ export class AuditableItemStreamService {
|
|
|
172
191
|
id
|
|
173
192
|
});
|
|
174
193
|
}
|
|
194
|
+
const streamId = urnParsed.namespaceSpecific(0);
|
|
195
|
+
await Mutex.lock(streamId, { throwOnTimeout: true });
|
|
175
196
|
try {
|
|
176
|
-
const streamId = urnParsed.namespaceSpecific(0);
|
|
177
197
|
const streamEntity = await this._streamStorage.get(streamId);
|
|
178
198
|
if (Is.empty(streamEntity)) {
|
|
179
199
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", id);
|
|
180
200
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
streamModel.cursor = result.cursor;
|
|
188
|
-
}
|
|
189
|
-
if (verifyStream && Is.stringValue(streamEntity.proofId)) {
|
|
190
|
-
streamModel.verification = await this._immutableProofComponent.verify(streamEntity.proofId);
|
|
191
|
-
}
|
|
192
|
-
if (verifyStream || verifyEntries) {
|
|
193
|
-
streamModel["@context"].push(ImmutableProofContexts.ContextRoot);
|
|
201
|
+
if (!streamEntity.closed) {
|
|
202
|
+
streamEntity.closed = true;
|
|
203
|
+
streamEntity.dateModified = new Date(Date.now()).toISOString();
|
|
204
|
+
await this._streamStorage.set(streamEntity);
|
|
205
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsClosed);
|
|
206
|
+
await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamUpdated, { id });
|
|
194
207
|
}
|
|
195
|
-
const result = await JsonLdProcessor.compact(streamModel, streamModel["@context"]);
|
|
196
|
-
return result;
|
|
197
208
|
}
|
|
198
209
|
catch (error) {
|
|
199
|
-
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "
|
|
210
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "closeFailed", undefined, error);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
Mutex.unlock(streamId);
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
/**
|
|
203
217
|
* Update a stream.
|
|
204
|
-
* @param stream The stream to update.
|
|
205
|
-
* @param stream.id The id of the stream to update.
|
|
206
|
-
* @param stream.annotationObject The object for the stream as JSON-LD.
|
|
218
|
+
* @param stream The stream to update, does not update entries.
|
|
207
219
|
* @returns Nothing.
|
|
208
220
|
*/
|
|
209
221
|
async update(stream) {
|
|
@@ -216,8 +228,12 @@ export class AuditableItemStreamService {
|
|
|
216
228
|
id: stream.id
|
|
217
229
|
});
|
|
218
230
|
}
|
|
231
|
+
const streamId = urnParsed.namespaceSpecific(0);
|
|
232
|
+
await Mutex.lock(streamId, { throwOnTimeout: true });
|
|
219
233
|
try {
|
|
220
|
-
const
|
|
234
|
+
const schemaValidationFailures = [];
|
|
235
|
+
await DataTypeHelper.validate("stream", `${AuditableItemStreamContexts.Namespace}${AuditableItemStreamTypes.Stream}`, stream, schemaValidationFailures);
|
|
236
|
+
Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream", schemaValidationFailures);
|
|
221
237
|
const streamEntity = await this._streamStorage.get(streamId);
|
|
222
238
|
if (Is.empty(streamEntity)) {
|
|
223
239
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", stream.id);
|
|
@@ -227,16 +243,94 @@ export class AuditableItemStreamService {
|
|
|
227
243
|
await JsonLdHelper.validate(stream.annotationObject, validationFailures);
|
|
228
244
|
Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream.annotationObject", validationFailures);
|
|
229
245
|
}
|
|
246
|
+
let changed = false;
|
|
230
247
|
if (!ObjectHelper.equal(streamEntity.annotationObject, stream.annotationObject, false)) {
|
|
231
248
|
streamEntity.annotationObject = stream.annotationObject;
|
|
249
|
+
changed = true;
|
|
250
|
+
}
|
|
251
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
252
|
+
const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
|
|
253
|
+
if (!Is.stringValue(streamEntity.organizationIdentity) &&
|
|
254
|
+
Is.stringValue(ownerOrganizationId)) {
|
|
255
|
+
streamEntity.organizationIdentity = ownerOrganizationId;
|
|
256
|
+
changed = true;
|
|
257
|
+
}
|
|
258
|
+
if (!Is.stringValue(streamEntity.proofId) &&
|
|
259
|
+
Is.stringValue(streamEntity.organizationIdentity)) {
|
|
260
|
+
await this.createStreamProof(streamEntity, streamEntity.immutableInterval ?? this._defaultImmutableInterval);
|
|
261
|
+
if (Is.stringValue(streamEntity.proofId)) {
|
|
262
|
+
changed = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (changed) {
|
|
232
266
|
streamEntity.dateModified = new Date(Date.now()).toISOString();
|
|
233
267
|
await this._streamStorage.set(streamEntity);
|
|
268
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsUpdated);
|
|
234
269
|
await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamUpdated, { id: stream.id });
|
|
235
270
|
}
|
|
236
271
|
}
|
|
237
272
|
catch (error) {
|
|
238
273
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "updatingFailed", undefined, error);
|
|
239
274
|
}
|
|
275
|
+
finally {
|
|
276
|
+
Mutex.unlock(streamId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get a stream header without the entries.
|
|
281
|
+
* @param id The id of the stream to get.
|
|
282
|
+
* @param cursor Cursor to use for next chunk of entries.
|
|
283
|
+
* @param limit Limit the number of entries to return, only applicable if includeEntries is true.
|
|
284
|
+
* @param options Additional options for the get operation.
|
|
285
|
+
* @param options.includeEntries Whether to include the entries, defaults to false.
|
|
286
|
+
* @param options.includeDeleted Whether to include deleted entries, defaults to false.
|
|
287
|
+
* @param options.verifyStream Should the stream be verified, defaults to false.
|
|
288
|
+
* @param options.verifyEntries Should the entries be verified, defaults to false.
|
|
289
|
+
* @returns The stream and entries if found.
|
|
290
|
+
* @throws NotFoundError if the stream is not found
|
|
291
|
+
*/
|
|
292
|
+
async get(id, cursor, limit, options) {
|
|
293
|
+
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "id", id);
|
|
294
|
+
const urnParsed = Urn.fromValidString(id);
|
|
295
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
296
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
297
|
+
namespace: AuditableItemStreamService._NAMESPACE,
|
|
298
|
+
id
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const streamId = urnParsed.namespaceSpecific(0);
|
|
303
|
+
const streamEntity = await this._streamStorage.get(streamId);
|
|
304
|
+
if (Is.empty(streamEntity)) {
|
|
305
|
+
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", id);
|
|
306
|
+
}
|
|
307
|
+
const verifyStream = options?.verifyStream ?? false;
|
|
308
|
+
const verifyEntries = options?.verifyEntries ?? false;
|
|
309
|
+
const streamModel = this.streamEntityToJsonLd(streamEntity);
|
|
310
|
+
let returnCursor;
|
|
311
|
+
if (options?.includeEntries) {
|
|
312
|
+
const result = await this.findEntries(streamId, options?.includeDeleted, verifyEntries, undefined, undefined, undefined, limit, cursor);
|
|
313
|
+
streamModel.entries = {
|
|
314
|
+
type: SchemaOrgTypes.ItemList,
|
|
315
|
+
[SchemaOrgTypes.ItemListElement]: result.entries
|
|
316
|
+
};
|
|
317
|
+
returnCursor = result.cursor;
|
|
318
|
+
}
|
|
319
|
+
if (verifyStream && Is.stringValue(streamEntity.proofId)) {
|
|
320
|
+
streamModel.verification = await this._immutableProofComponent.verify(streamEntity.proofId);
|
|
321
|
+
}
|
|
322
|
+
if (verifyStream || verifyEntries) {
|
|
323
|
+
streamModel["@context"].push(ImmutableProofContexts.Context);
|
|
324
|
+
}
|
|
325
|
+
const result = await JsonLdProcessor.compact(streamModel, streamModel["@context"]);
|
|
326
|
+
return {
|
|
327
|
+
stream: result,
|
|
328
|
+
cursor: returnCursor
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "getFailed", undefined, error);
|
|
333
|
+
}
|
|
240
334
|
}
|
|
241
335
|
/**
|
|
242
336
|
* Delete the stream.
|
|
@@ -252,19 +346,24 @@ export class AuditableItemStreamService {
|
|
|
252
346
|
id
|
|
253
347
|
});
|
|
254
348
|
}
|
|
349
|
+
const streamId = urnParsed.namespaceSpecific(0);
|
|
350
|
+
await Mutex.lock(streamId, { throwOnTimeout: true });
|
|
255
351
|
try {
|
|
256
|
-
const streamId = urnParsed.namespaceSpecific(0);
|
|
257
352
|
const streamEntity = await this._streamStorage.get(streamId);
|
|
258
353
|
if (Is.empty(streamEntity)) {
|
|
259
354
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", id);
|
|
260
355
|
}
|
|
261
356
|
await this.internalRemoveEntries(streamEntity, false);
|
|
262
357
|
await this._streamStorage.remove(streamEntity.id);
|
|
358
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsDeleted);
|
|
263
359
|
await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamDeleted, { id });
|
|
264
360
|
}
|
|
265
361
|
catch (error) {
|
|
266
362
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removingFailed", undefined, error);
|
|
267
363
|
}
|
|
364
|
+
finally {
|
|
365
|
+
Mutex.unlock(streamId);
|
|
366
|
+
}
|
|
268
367
|
}
|
|
269
368
|
/**
|
|
270
369
|
* Query all the streams, will not return entries.
|
|
@@ -305,16 +404,18 @@ export class AuditableItemStreamService {
|
|
|
305
404
|
], propertiesToReturn, cursor, limit);
|
|
306
405
|
const list = {
|
|
307
406
|
"@context": [
|
|
308
|
-
SchemaOrgContexts.
|
|
309
|
-
AuditableItemStreamContexts.
|
|
310
|
-
AuditableItemStreamContexts.
|
|
407
|
+
SchemaOrgContexts.Context,
|
|
408
|
+
AuditableItemStreamContexts.Context,
|
|
409
|
+
AuditableItemStreamContexts.ContextCommon
|
|
311
410
|
],
|
|
312
411
|
type: [SchemaOrgTypes.ItemList, AuditableItemStreamTypes.StreamList],
|
|
313
|
-
[SchemaOrgTypes.ItemListElement]: results.entities.map(e => this.streamEntityToJsonLd(e))
|
|
314
|
-
[SchemaOrgTypes.NextItem]: results.cursor
|
|
412
|
+
[SchemaOrgTypes.ItemListElement]: results.entities.map(e => this.streamEntityToJsonLd(e))
|
|
315
413
|
};
|
|
316
414
|
const result = await JsonLdProcessor.compact(list, list["@context"]);
|
|
317
|
-
return
|
|
415
|
+
return {
|
|
416
|
+
entries: result,
|
|
417
|
+
cursor: results.cursor
|
|
418
|
+
};
|
|
318
419
|
}
|
|
319
420
|
catch (error) {
|
|
320
421
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "queryingFailed", undefined, error);
|
|
@@ -336,24 +437,36 @@ export class AuditableItemStreamService {
|
|
|
336
437
|
id: streamId
|
|
337
438
|
});
|
|
338
439
|
}
|
|
440
|
+
const streamIdParts = urnParsed.namespaceSpecific(0);
|
|
441
|
+
await Mutex.lock(streamIdParts, { throwOnTimeout: true });
|
|
339
442
|
try {
|
|
340
|
-
const streamIdParts = urnParsed.namespaceSpecific(0);
|
|
341
443
|
const streamEntity = await this._streamStorage.get(streamIdParts);
|
|
342
444
|
if (Is.empty(streamEntity)) {
|
|
343
445
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamIdParts);
|
|
344
446
|
}
|
|
447
|
+
if (streamEntity.closed) {
|
|
448
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ClosedStreamRejections, { operation: "createEntry" });
|
|
449
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "streamClosed", {
|
|
450
|
+
id: streamId
|
|
451
|
+
});
|
|
452
|
+
}
|
|
345
453
|
const context = {
|
|
346
454
|
now: new Date(Date.now()).toISOString(),
|
|
347
455
|
contextIds,
|
|
348
|
-
indexCounter: streamEntity.
|
|
349
|
-
immutableInterval: streamEntity.immutableInterval
|
|
456
|
+
indexCounter: streamEntity.numberOfItems,
|
|
457
|
+
immutableInterval: streamEntity.immutableInterval,
|
|
458
|
+
organizationIdentity: streamEntity.organizationIdentity ?? contextIds?.[ContextIdKeys.Organization]
|
|
350
459
|
};
|
|
351
460
|
const createdId = await this.setEntry(context, streamEntity.id, {
|
|
352
461
|
entryObject
|
|
353
462
|
});
|
|
354
463
|
streamEntity.dateModified = context.now;
|
|
355
|
-
streamEntity.
|
|
464
|
+
streamEntity.numberOfItems = context.indexCounter;
|
|
356
465
|
await this._streamStorage.set(streamEntity);
|
|
466
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.EntriesCreated, {
|
|
467
|
+
hasProof: context.immutableInterval > 0 &&
|
|
468
|
+
(context.indexCounter - 1) % context.immutableInterval === 0
|
|
469
|
+
});
|
|
357
470
|
const fullId = new Urn(AuditableItemStreamService._NAMESPACE, [
|
|
358
471
|
streamEntity.id,
|
|
359
472
|
createdId
|
|
@@ -364,6 +477,9 @@ export class AuditableItemStreamService {
|
|
|
364
477
|
catch (error) {
|
|
365
478
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "creatingEntryFailed", undefined, error);
|
|
366
479
|
}
|
|
480
|
+
finally {
|
|
481
|
+
Mutex.unlock(streamIdParts);
|
|
482
|
+
}
|
|
367
483
|
}
|
|
368
484
|
/**
|
|
369
485
|
* Get the entry from the stream.
|
|
@@ -405,7 +521,7 @@ export class AuditableItemStreamService {
|
|
|
405
521
|
}
|
|
406
522
|
const entry = this.streamEntryEntityToJsonLd(result.entity);
|
|
407
523
|
if (verifyEntry) {
|
|
408
|
-
entry["@context"].
|
|
524
|
+
entry["@context"] = JsonLdProcessor.combineContexts(entry["@context"], ImmutableProofContexts.Context);
|
|
409
525
|
entry.verification = result.verification;
|
|
410
526
|
}
|
|
411
527
|
const result2 = await JsonLdProcessor.compact(entry, entry["@context"]);
|
|
@@ -466,7 +582,6 @@ export class AuditableItemStreamService {
|
|
|
466
582
|
async updateEntry(streamId, entryId, entryObject) {
|
|
467
583
|
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
|
|
468
584
|
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "entryId", entryId);
|
|
469
|
-
const contextIds = await ContextIdStore.getContextIds();
|
|
470
585
|
const urnParsed = Urn.fromValidString(streamId);
|
|
471
586
|
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
472
587
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
@@ -481,8 +596,9 @@ export class AuditableItemStreamService {
|
|
|
481
596
|
id: entryId
|
|
482
597
|
});
|
|
483
598
|
}
|
|
599
|
+
const streamNamespaceId = urnParsed.namespaceSpecific(0);
|
|
600
|
+
await Mutex.lock(streamNamespaceId, { throwOnTimeout: true });
|
|
484
601
|
try {
|
|
485
|
-
const streamNamespaceId = urnParsed.namespaceMethod();
|
|
486
602
|
const streamEntryNamespaceId = urnParsedEntry.namespaceMethod();
|
|
487
603
|
if (streamNamespaceId !== streamEntryNamespaceId) {
|
|
488
604
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
@@ -494,29 +610,48 @@ export class AuditableItemStreamService {
|
|
|
494
610
|
if (Is.empty(streamEntity)) {
|
|
495
611
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
|
|
496
612
|
}
|
|
613
|
+
if (streamEntity.closed) {
|
|
614
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ClosedStreamRejections, { operation: "updateEntry" });
|
|
615
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "streamClosed", {
|
|
616
|
+
id: streamId
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
if (streamEntity.mode === AuditableItemStreamModes.AppendOnly) {
|
|
620
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.AppendOnlyRejections, { operation: "updateEntry" });
|
|
621
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "appendOnlyNoEntryUpdates", {
|
|
622
|
+
id: streamId
|
|
623
|
+
});
|
|
624
|
+
}
|
|
497
625
|
const entryNamespaceId = urnParsedEntry.namespaceSpecific(1);
|
|
498
626
|
const existing = await this.findEntry(streamEntity.id, entryNamespaceId);
|
|
499
627
|
if (Is.empty(existing)) {
|
|
500
628
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamEntryNotFound", entryId);
|
|
501
629
|
}
|
|
630
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
631
|
+
const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
|
|
502
632
|
const context = {
|
|
503
633
|
now: new Date(Date.now()).toISOString(),
|
|
504
634
|
contextIds,
|
|
505
|
-
indexCounter: streamEntity.
|
|
506
|
-
immutableInterval: streamEntity.immutableInterval
|
|
635
|
+
indexCounter: streamEntity.numberOfItems,
|
|
636
|
+
immutableInterval: streamEntity.immutableInterval,
|
|
637
|
+
organizationIdentity: streamEntity.organizationIdentity ?? ownerOrganizationId
|
|
507
638
|
};
|
|
508
639
|
await this.setEntry(context, streamEntity.id, {
|
|
509
640
|
...existing.entity,
|
|
510
641
|
entryObject
|
|
511
642
|
});
|
|
512
643
|
streamEntity.dateModified = context.now;
|
|
513
|
-
streamEntity.
|
|
644
|
+
streamEntity.numberOfItems = context.indexCounter;
|
|
514
645
|
await this._streamStorage.set(streamEntity);
|
|
646
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.EntriesUpdated);
|
|
515
647
|
await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamEntryUpdated, { id: streamId, entryId });
|
|
516
648
|
}
|
|
517
649
|
catch (error) {
|
|
518
650
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "updatingEntryFailed", undefined, error);
|
|
519
651
|
}
|
|
652
|
+
finally {
|
|
653
|
+
Mutex.unlock(streamNamespaceId);
|
|
654
|
+
}
|
|
520
655
|
}
|
|
521
656
|
/**
|
|
522
657
|
* Delete from the stream.
|
|
@@ -527,7 +662,6 @@ export class AuditableItemStreamService {
|
|
|
527
662
|
async removeEntry(streamId, entryId) {
|
|
528
663
|
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
|
|
529
664
|
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "entryId", entryId);
|
|
530
|
-
const contextIds = await ContextIdStore.getContextIds();
|
|
531
665
|
const urnParsed = Urn.fromValidString(streamId);
|
|
532
666
|
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
533
667
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
@@ -542,8 +676,9 @@ export class AuditableItemStreamService {
|
|
|
542
676
|
id: entryId
|
|
543
677
|
});
|
|
544
678
|
}
|
|
679
|
+
const streamNamespaceId = urnParsed.namespaceSpecific(0);
|
|
680
|
+
await Mutex.lock(streamNamespaceId, { throwOnTimeout: true });
|
|
545
681
|
try {
|
|
546
|
-
const streamNamespaceId = urnParsed.namespaceMethod();
|
|
547
682
|
const streamEntryNamespaceId = urnParsedEntry.namespaceMethod();
|
|
548
683
|
if (streamNamespaceId !== streamEntryNamespaceId) {
|
|
549
684
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
@@ -555,35 +690,48 @@ export class AuditableItemStreamService {
|
|
|
555
690
|
if (Is.empty(streamEntity)) {
|
|
556
691
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
|
|
557
692
|
}
|
|
693
|
+
if (streamEntity.mode === AuditableItemStreamModes.AppendOnly) {
|
|
694
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.AppendOnlyRejections, { operation: "removeEntry" });
|
|
695
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "appendOnlyNoEntryRemovals", {
|
|
696
|
+
id: streamId
|
|
697
|
+
});
|
|
698
|
+
}
|
|
558
699
|
const entryNamespaceId = urnParsedEntry.namespaceSpecific(1);
|
|
559
700
|
const result = await this.findEntry(streamNamespaceId, entryNamespaceId);
|
|
560
701
|
if (Is.empty(result)) {
|
|
561
702
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamEntryNotFound", entryId);
|
|
562
703
|
}
|
|
563
704
|
if (Is.empty(result.entity.dateDeleted)) {
|
|
705
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
706
|
+
const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
|
|
564
707
|
const context = {
|
|
565
708
|
now: new Date(Date.now()).toISOString(),
|
|
566
709
|
contextIds,
|
|
567
|
-
indexCounter: streamEntity.
|
|
568
|
-
immutableInterval: streamEntity.immutableInterval
|
|
710
|
+
indexCounter: streamEntity.numberOfItems,
|
|
711
|
+
immutableInterval: streamEntity.immutableInterval,
|
|
712
|
+
organizationIdentity: streamEntity.organizationIdentity ?? ownerOrganizationId
|
|
569
713
|
};
|
|
570
714
|
await this.setEntry(context, streamEntity.id, {
|
|
571
715
|
...result.entity,
|
|
572
716
|
dateDeleted: context.now
|
|
573
717
|
});
|
|
574
718
|
streamEntity.dateModified = context.now;
|
|
575
|
-
streamEntity.
|
|
719
|
+
streamEntity.numberOfItems = context.indexCounter;
|
|
576
720
|
await this._streamStorage.set(streamEntity);
|
|
721
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.EntriesDeleted);
|
|
577
722
|
await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamEntryDeleted, { id: streamId, entryId });
|
|
578
723
|
}
|
|
579
724
|
}
|
|
580
725
|
catch (error) {
|
|
581
726
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removingEntryFailed", undefined, error);
|
|
582
727
|
}
|
|
728
|
+
finally {
|
|
729
|
+
Mutex.unlock(streamNamespaceId);
|
|
730
|
+
}
|
|
583
731
|
}
|
|
584
732
|
/**
|
|
585
733
|
* Get the entries for the stream.
|
|
586
|
-
* @param streamId The id of the stream to get.
|
|
734
|
+
* @param streamId The id of the stream to get, if undefined returns all matching entries.
|
|
587
735
|
* @param options Additional options for the get operation.
|
|
588
736
|
* @param options.conditions The conditions to filter the stream.
|
|
589
737
|
* @param options.includeDeleted Whether to include deleted entries, defaults to false.
|
|
@@ -595,37 +743,44 @@ export class AuditableItemStreamService {
|
|
|
595
743
|
* @throws NotFoundError if the stream is not found.
|
|
596
744
|
*/
|
|
597
745
|
async getEntries(streamId, options) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
746
|
+
let streamNamespaceId;
|
|
747
|
+
if (!Is.empty(streamId)) {
|
|
748
|
+
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
|
|
749
|
+
const urnParsed = Urn.fromValidString(streamId);
|
|
750
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
751
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
752
|
+
namespace: AuditableItemStreamService._NAMESPACE,
|
|
753
|
+
id: streamId
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
streamNamespaceId = urnParsed.namespaceSpecific(0);
|
|
605
757
|
}
|
|
606
758
|
try {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
759
|
+
if (Is.stringValue(streamNamespaceId)) {
|
|
760
|
+
const streamEntity = await this._streamStorage.get(streamNamespaceId);
|
|
761
|
+
if (Is.empty(streamEntity)) {
|
|
762
|
+
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
|
|
763
|
+
}
|
|
611
764
|
}
|
|
612
765
|
const verifyEntries = options?.verifyEntries ?? false;
|
|
613
766
|
const result = await this.findEntries(streamNamespaceId, options?.includeDeleted, verifyEntries, options?.conditions, options?.order, undefined, options?.limit, options?.cursor);
|
|
614
767
|
const list = {
|
|
615
768
|
"@context": [
|
|
616
|
-
SchemaOrgContexts.
|
|
617
|
-
AuditableItemStreamContexts.
|
|
618
|
-
AuditableItemStreamContexts.
|
|
769
|
+
SchemaOrgContexts.Context,
|
|
770
|
+
AuditableItemStreamContexts.Context,
|
|
771
|
+
AuditableItemStreamContexts.ContextCommon
|
|
619
772
|
],
|
|
620
773
|
type: [SchemaOrgTypes.ItemList, AuditableItemStreamTypes.StreamEntryList],
|
|
621
|
-
[SchemaOrgTypes.ItemListElement]: result.entries
|
|
622
|
-
[SchemaOrgTypes.NextItem]: result.cursor
|
|
774
|
+
[SchemaOrgTypes.ItemListElement]: result.entries
|
|
623
775
|
};
|
|
624
776
|
if (verifyEntries) {
|
|
625
|
-
list["@context"].push(ImmutableProofContexts.
|
|
777
|
+
list["@context"].push(ImmutableProofContexts.Context);
|
|
626
778
|
}
|
|
627
779
|
const result2 = await JsonLdProcessor.compact(list, list["@context"]);
|
|
628
|
-
return
|
|
780
|
+
return {
|
|
781
|
+
entries: result2,
|
|
782
|
+
cursor: result.cursor
|
|
783
|
+
};
|
|
629
784
|
}
|
|
630
785
|
catch (error) {
|
|
631
786
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "gettingEntriesFailed", undefined, error);
|
|
@@ -633,7 +788,7 @@ export class AuditableItemStreamService {
|
|
|
633
788
|
}
|
|
634
789
|
/**
|
|
635
790
|
* Get the entry objects for the stream.
|
|
636
|
-
* @param streamId The id of the stream to get.
|
|
791
|
+
* @param streamId The id of the stream to get, if undefined returns all matching entries.
|
|
637
792
|
* @param options Additional options for the get operation.
|
|
638
793
|
* @param options.conditions The conditions to filter the stream.
|
|
639
794
|
* @param options.includeDeleted Whether to include deleted entries, defaults to false.
|
|
@@ -644,45 +799,52 @@ export class AuditableItemStreamService {
|
|
|
644
799
|
* @throws NotFoundError if the stream is not found.
|
|
645
800
|
*/
|
|
646
801
|
async getEntryObjects(streamId, options) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
802
|
+
let streamNamespaceId;
|
|
803
|
+
if (!Is.empty(streamId)) {
|
|
804
|
+
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
|
|
805
|
+
const urnParsed = Urn.fromValidString(streamId);
|
|
806
|
+
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
807
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
|
|
808
|
+
namespace: AuditableItemStreamService._NAMESPACE,
|
|
809
|
+
id: streamId
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
streamNamespaceId = urnParsed.namespaceSpecific(0);
|
|
654
813
|
}
|
|
655
814
|
try {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
815
|
+
if (Is.stringValue(streamNamespaceId)) {
|
|
816
|
+
const streamEntity = await this._streamStorage.get(streamNamespaceId);
|
|
817
|
+
if (Is.empty(streamEntity)) {
|
|
818
|
+
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
|
|
819
|
+
}
|
|
660
820
|
}
|
|
661
821
|
const result = await this.findEntries(streamNamespaceId, options?.includeDeleted, false, options?.conditions, options?.order, undefined, options?.limit, options?.cursor);
|
|
662
822
|
const list = {
|
|
663
823
|
"@context": [
|
|
664
|
-
SchemaOrgContexts.
|
|
665
|
-
AuditableItemStreamContexts.
|
|
666
|
-
AuditableItemStreamContexts.
|
|
824
|
+
SchemaOrgContexts.Context,
|
|
825
|
+
AuditableItemStreamContexts.Context,
|
|
826
|
+
AuditableItemStreamContexts.ContextCommon
|
|
667
827
|
],
|
|
668
828
|
type: [SchemaOrgTypes.ItemList, AuditableItemStreamTypes.StreamEntryObjectList],
|
|
669
|
-
[SchemaOrgTypes.ItemListElement]: result.entries.map(m => m.entryObject)
|
|
670
|
-
[SchemaOrgTypes.NextItem]: result.cursor
|
|
829
|
+
[SchemaOrgTypes.ItemListElement]: result.entries.map(m => m.entryObject)
|
|
671
830
|
};
|
|
672
831
|
const result2 = await JsonLdProcessor.compact(list, list["@context"]);
|
|
673
|
-
return
|
|
832
|
+
return {
|
|
833
|
+
entries: result2,
|
|
834
|
+
cursor: result.cursor
|
|
835
|
+
};
|
|
674
836
|
}
|
|
675
837
|
catch (error) {
|
|
676
838
|
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "gettingEntryObjectsFailed", undefined, error);
|
|
677
839
|
}
|
|
678
840
|
}
|
|
679
841
|
/**
|
|
680
|
-
* Remove the
|
|
681
|
-
* @param streamId The id of the stream to remove the
|
|
842
|
+
* Remove the proof for the stream and entries.
|
|
843
|
+
* @param streamId The id of the stream to remove the proof from.
|
|
682
844
|
* @returns Nothing.
|
|
683
845
|
* @throws NotFoundError if the vertex is not found.
|
|
684
846
|
*/
|
|
685
|
-
async
|
|
847
|
+
async removeProof(streamId) {
|
|
686
848
|
Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
|
|
687
849
|
const urnParsed = Urn.fromValidString(streamId);
|
|
688
850
|
if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
|
|
@@ -691,8 +853,9 @@ export class AuditableItemStreamService {
|
|
|
691
853
|
id: streamId
|
|
692
854
|
});
|
|
693
855
|
}
|
|
856
|
+
const streamIdParts = urnParsed.namespaceSpecific(0);
|
|
857
|
+
await Mutex.lock(streamIdParts, { throwOnTimeout: true });
|
|
694
858
|
try {
|
|
695
|
-
const streamIdParts = urnParsed.namespaceSpecific(0);
|
|
696
859
|
const streamEntity = await this._streamStorage.get(streamIdParts);
|
|
697
860
|
if (Is.empty(streamEntity)) {
|
|
698
861
|
throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamIdParts);
|
|
@@ -700,9 +863,27 @@ export class AuditableItemStreamService {
|
|
|
700
863
|
await this.internalRemoveEntries(streamEntity, true);
|
|
701
864
|
}
|
|
702
865
|
catch (error) {
|
|
703
|
-
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "
|
|
866
|
+
throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removeProofFailed", undefined, error);
|
|
867
|
+
}
|
|
868
|
+
finally {
|
|
869
|
+
Mutex.unlock(streamIdParts);
|
|
704
870
|
}
|
|
705
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Create an immutable proof for the stream entity if the conditions are met.
|
|
874
|
+
* @param streamEntity The stream entity to create the proof for.
|
|
875
|
+
* @param immutableInterval The immutable interval for the stream.
|
|
876
|
+
* @returns The proof id.
|
|
877
|
+
* @internal
|
|
878
|
+
*/
|
|
879
|
+
async createStreamProof(streamEntity, immutableInterval) {
|
|
880
|
+
const streamModel = this.streamEntityToJsonLd(ObjectHelper.pick(streamEntity, AuditableItemStreamService._PROOF_KEYS_STREAM));
|
|
881
|
+
if (immutableInterval > 0 && Is.stringValue(streamModel.organizationIdentity)) {
|
|
882
|
+
streamEntity.proofId = await this._immutableProofComponent.create(streamModel);
|
|
883
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsCreatedStream);
|
|
884
|
+
}
|
|
885
|
+
return streamModel.id;
|
|
886
|
+
}
|
|
706
887
|
/**
|
|
707
888
|
* Map the stream entity to a JSON-LD model.
|
|
708
889
|
* @param streamEntity The stream entity.
|
|
@@ -712,9 +893,9 @@ export class AuditableItemStreamService {
|
|
|
712
893
|
streamEntityToJsonLd(streamEntity) {
|
|
713
894
|
const model = {
|
|
714
895
|
"@context": [
|
|
715
|
-
|
|
716
|
-
AuditableItemStreamContexts.
|
|
717
|
-
|
|
896
|
+
SchemaOrgContexts.Context,
|
|
897
|
+
AuditableItemStreamContexts.Context,
|
|
898
|
+
AuditableItemStreamContexts.ContextCommon
|
|
718
899
|
],
|
|
719
900
|
type: AuditableItemStreamTypes.Stream,
|
|
720
901
|
id: `${AuditableItemStreamService._NAMESPACE}:${streamEntity.id}`,
|
|
@@ -724,7 +905,10 @@ export class AuditableItemStreamService {
|
|
|
724
905
|
userIdentity: streamEntity.userIdentity,
|
|
725
906
|
annotationObject: streamEntity.annotationObject,
|
|
726
907
|
immutableInterval: streamEntity.immutableInterval,
|
|
727
|
-
proofId: streamEntity.proofId
|
|
908
|
+
proofId: streamEntity.proofId,
|
|
909
|
+
numberOfItems: streamEntity.numberOfItems,
|
|
910
|
+
closed: streamEntity.closed,
|
|
911
|
+
mode: streamEntity.mode
|
|
728
912
|
};
|
|
729
913
|
return model;
|
|
730
914
|
}
|
|
@@ -737,9 +921,9 @@ export class AuditableItemStreamService {
|
|
|
737
921
|
streamEntryEntityToJsonLd(streamEntryEntity) {
|
|
738
922
|
const streamEntryModel = {
|
|
739
923
|
"@context": [
|
|
740
|
-
AuditableItemStreamContexts.
|
|
741
|
-
AuditableItemStreamContexts.
|
|
742
|
-
SchemaOrgContexts.
|
|
924
|
+
AuditableItemStreamContexts.Context,
|
|
925
|
+
AuditableItemStreamContexts.ContextCommon,
|
|
926
|
+
SchemaOrgContexts.Context
|
|
743
927
|
],
|
|
744
928
|
type: AuditableItemStreamTypes.StreamEntry,
|
|
745
929
|
id: `${AuditableItemStreamService._NAMESPACE}:${streamEntryEntity.streamId}:${streamEntryEntity.id}`,
|
|
@@ -770,7 +954,7 @@ export class AuditableItemStreamService {
|
|
|
770
954
|
Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "entry.entryObject", validationFailures);
|
|
771
955
|
}
|
|
772
956
|
const entity = {
|
|
773
|
-
id: entry.id ??
|
|
957
|
+
id: entry.id ?? RandomHelper.generateUuidV7("compact"),
|
|
774
958
|
streamId,
|
|
775
959
|
dateCreated: entry.dateCreated ?? context.now,
|
|
776
960
|
dateDeleted: entry.dateDeleted,
|
|
@@ -786,8 +970,12 @@ export class AuditableItemStreamService {
|
|
|
786
970
|
// Create the JSON-LD object we want to use for the proof
|
|
787
971
|
// this is a subset of fixed properties from the stream entry object.
|
|
788
972
|
const streamEntryModel = this.streamEntryEntityToJsonLd(ObjectHelper.pick(entity, AuditableItemStreamService._PROOF_KEYS_STREAM_ENTRY));
|
|
789
|
-
// Create the proof for the stream object
|
|
790
|
-
|
|
973
|
+
// Create the proof for the stream object but only if we have an organization identity,
|
|
974
|
+
// either from the stream or the context, as this is needed for the proof creation and immutability.
|
|
975
|
+
if (Is.stringValue(context.organizationIdentity)) {
|
|
976
|
+
entity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(streamEntryModel));
|
|
977
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsCreatedEntry, { index: entity.index });
|
|
978
|
+
}
|
|
791
979
|
}
|
|
792
980
|
await this._streamEntryStorage.set(entity);
|
|
793
981
|
return entity.id;
|
|
@@ -797,6 +985,7 @@ export class AuditableItemStreamService {
|
|
|
797
985
|
* @param streamId The stream id.
|
|
798
986
|
* @param entryId The entry id.
|
|
799
987
|
* @param verifyEntry Should the entry be verified.
|
|
988
|
+
* @returns The entry entity and optional verification result, or undefined if not found.
|
|
800
989
|
* @internal
|
|
801
990
|
*/
|
|
802
991
|
async findEntry(streamId, entryId, verifyEntry) {
|
|
@@ -843,18 +1032,19 @@ export class AuditableItemStreamService {
|
|
|
843
1032
|
* @param propertiesToReturn The properties to return.
|
|
844
1033
|
* @param limit Limit the number of entities when finding.
|
|
845
1034
|
* @param cursor The cursor.
|
|
846
|
-
* @
|
|
1035
|
+
* @returns The stream entries and optional next cursor.
|
|
847
1036
|
* @internal
|
|
848
1037
|
*/
|
|
849
1038
|
async findEntries(streamId, includeDeleted, verifyEntries, conditions, sortDirection, propertiesToReturn, limit, cursor) {
|
|
850
1039
|
const needToVerify = verifyEntries ?? false;
|
|
851
|
-
const combinedConditions = [
|
|
852
|
-
|
|
1040
|
+
const combinedConditions = [];
|
|
1041
|
+
if (Is.stringValue(streamId)) {
|
|
1042
|
+
combinedConditions.push({
|
|
853
1043
|
property: "streamId",
|
|
854
1044
|
comparison: ComparisonOperator.Equals,
|
|
855
1045
|
value: streamId
|
|
856
|
-
}
|
|
857
|
-
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
858
1048
|
if (Is.stringValue(cursor)) {
|
|
859
1049
|
const parts = cursor.split("|");
|
|
860
1050
|
if (parts.length > 1) {
|
|
@@ -920,9 +1110,10 @@ export class AuditableItemStreamService {
|
|
|
920
1110
|
*/
|
|
921
1111
|
async internalRemoveEntries(streamEntity, removeOnlyProof) {
|
|
922
1112
|
if (Is.stringValue(streamEntity.proofId)) {
|
|
923
|
-
await this._immutableProofComponent.
|
|
1113
|
+
await this._immutableProofComponent.removeNotarization(streamEntity.proofId);
|
|
924
1114
|
delete streamEntity.proofId;
|
|
925
1115
|
await this._streamStorage.set(streamEntity);
|
|
1116
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsRemovedStream);
|
|
926
1117
|
}
|
|
927
1118
|
const entryIds = [];
|
|
928
1119
|
let entriesResult;
|
|
@@ -940,8 +1131,9 @@ export class AuditableItemStreamService {
|
|
|
940
1131
|
for (const streamEntry of entriesResult.entities) {
|
|
941
1132
|
entryIds.push(streamEntry.id);
|
|
942
1133
|
if (Is.stringValue(streamEntry.proofId)) {
|
|
943
|
-
await this._immutableProofComponent.
|
|
1134
|
+
await this._immutableProofComponent.removeNotarization(streamEntry.proofId);
|
|
944
1135
|
delete streamEntry.proofId;
|
|
1136
|
+
await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsRemovedEntry);
|
|
945
1137
|
// If we are only removing the proof, we need to set the entry
|
|
946
1138
|
// otherwise the entry is going to be removed later anyway.
|
|
947
1139
|
if (removeOnlyProof) {
|