@3sixty/document-management-service 0.0.0-stage → 0.20.0-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.
Files changed (38) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +24 -2
  3. package/dist/es/documentManagementRoutes.js +609 -0
  4. package/dist/es/documentManagementRoutes.js.map +1 -0
  5. package/dist/es/documentManagementService.js +967 -0
  6. package/dist/es/documentManagementService.js.map +1 -0
  7. package/dist/es/index.js +8 -0
  8. package/dist/es/index.js.map +1 -0
  9. package/dist/es/models/IDocumentManagementServiceConfig.js +4 -0
  10. package/dist/es/models/IDocumentManagementServiceConfig.js.map +1 -0
  11. package/dist/es/models/IDocumentManagementStorageServiceConstructorOptions.js +2 -0
  12. package/dist/es/models/IDocumentManagementStorageServiceConstructorOptions.js.map +1 -0
  13. package/dist/es/restEntryPoints.js +13 -0
  14. package/dist/es/restEntryPoints.js.map +1 -0
  15. package/dist/types/documentManagementRoutes.d.ts +62 -0
  16. package/dist/types/documentManagementService.d.ts +147 -0
  17. package/dist/types/index.d.ts +5 -0
  18. package/dist/types/models/IDocumentManagementServiceConfig.d.ts +9 -0
  19. package/dist/types/models/IDocumentManagementStorageServiceConstructorOptions.d.ts +34 -0
  20. package/dist/types/restEntryPoints.d.ts +5 -0
  21. package/docs/changelog.md +17 -0
  22. package/docs/examples.md +121 -0
  23. package/docs/open-api/spec.json +1387 -0
  24. package/docs/reference/classes/DocumentManagementService.md +464 -0
  25. package/docs/reference/functions/documentManagementCreate.md +37 -0
  26. package/docs/reference/functions/documentManagementGet.md +31 -0
  27. package/docs/reference/functions/documentManagementGetRevision.md +31 -0
  28. package/docs/reference/functions/documentManagementQuery.md +31 -0
  29. package/docs/reference/functions/documentManagementRemove.md +31 -0
  30. package/docs/reference/functions/documentManagementUpdatePartial.md +31 -0
  31. package/docs/reference/functions/generateRestRoutesDocumentManagement.md +25 -0
  32. package/docs/reference/index.md +25 -0
  33. package/docs/reference/interfaces/IDocumentManagementServiceConfig.md +11 -0
  34. package/docs/reference/interfaces/IDocumentManagementServiceConstructorOptions.md +75 -0
  35. package/docs/reference/variables/restEntryPoints.md +5 -0
  36. package/docs/reference/variables/tagsDocumentManagement.md +5 -0
  37. package/locales/en.json +22 -0
  38. package/package.json +68 -5
@@ -0,0 +1,967 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { HealthCategory, HealthStatus } from "@3sixty/api-models";
4
+ import { AuditableItemGraphContexts, AuditableItemGraphTypes } from "@3sixty/auditable-item-graph-models";
5
+ import { BlobStorageContexts } from "@3sixty/blob-storage-models";
6
+ import { ContextIdKeys, ContextIdStore } from "@3sixty/context";
7
+ import { BaseError, Coerce, ComponentFactory, Converter, GeneralError, Guards, Is, Mutex, NotFoundError, NumberHelper, ObjectHelper, RandomHelper, Urn } from "@3sixty/core";
8
+ import { IntegrityAlgorithm, IntegrityHelper, Sha256 } from "@3sixty/crypto";
9
+ import { JsonLdHelper, JsonLdProcessor } from "@3sixty/data-json-ld";
10
+ import { DocumentContexts, DocumentManagementMetricIds, DocumentManagementMetrics, DocumentTypes } from "@3sixty/document-management-models";
11
+ import { SchemaOrgContexts, SchemaOrgDataTypes, SchemaOrgTypes } from "@3sixty/standards-schema-org";
12
+ import { UneceDocumentCodeList } from "@3sixty/standards-unece";
13
+ import { MetricHelper } from "@3sixty/telemetry-models";
14
+ import { MimeTypes } from "@3sixty/web";
15
+ /**
16
+ * Service for performing document management operations.
17
+ */
18
+ export class DocumentManagementService {
19
+ /**
20
+ * Runtime name for the class.
21
+ */
22
+ static CLASS_NAME = "DocumentManagementService";
23
+ /**
24
+ * Prefix for the mutex key serialising creates of identical content.
25
+ * @internal
26
+ */
27
+ static _BLOB_INTEGRITY_LOCK_PREFIX = "blob-integrity:";
28
+ /**
29
+ * The component for the auditable item graph.
30
+ * @internal
31
+ */
32
+ _auditableItemGraphComponent;
33
+ /**
34
+ * The connector for the blob component.
35
+ * @internal
36
+ */
37
+ _blobStorageComponent;
38
+ /**
39
+ * The connector for the attestation.
40
+ * @internal
41
+ */
42
+ _attestationComponent;
43
+ /**
44
+ * The connector for the data processing.
45
+ * @internal
46
+ */
47
+ _dataProcessingComponent;
48
+ /**
49
+ * The optional telemetry component used for event metrics.
50
+ * @internal
51
+ */
52
+ _telemetryComponent;
53
+ /**
54
+ * The timeout in milliseconds for acquiring a mutex lock.
55
+ * @internal
56
+ */
57
+ _mutexTimeoutMs;
58
+ /**
59
+ * Create a new instance of DocumentManagementService.
60
+ * @param options The options for the service.
61
+ */
62
+ constructor(options) {
63
+ this._auditableItemGraphComponent = ComponentFactory.get(options?.auditableItemGraphComponentType ?? "auditable-item-graph");
64
+ this._blobStorageComponent = ComponentFactory.get(options?.blobStorageComponentType ?? "blob-storage");
65
+ this._attestationComponent = ComponentFactory.get(options?.attestationComponentType ?? "attestation");
66
+ this._dataProcessingComponent = ComponentFactory.get(options?.dataProcessingComponentType ?? "data-processing");
67
+ this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
68
+ this._mutexTimeoutMs = Coerce.integer(options?.config?.mutexTimeoutMs);
69
+ SchemaOrgDataTypes.registerRedirects();
70
+ }
71
+ /**
72
+ * Returns the class name of the component.
73
+ * @returns The class name of the component.
74
+ */
75
+ className() {
76
+ return DocumentManagementService.CLASS_NAME;
77
+ }
78
+ /**
79
+ * Runs a blob lifecycle (create, get, remove) against the blob storage component to verify
80
+ * the service is operational.
81
+ * @param callback The callback to invoke when a deferred health result is ready.
82
+ * @returns The health status of the service.
83
+ */
84
+ async healthApplication(callback) {
85
+ const contextIds = (await ContextIdStore.getContextIds()) ?? {};
86
+ const orgId = contextIds[ContextIdKeys.Organization];
87
+ if (!Is.stringValue(orgId)) {
88
+ return [];
89
+ }
90
+ try {
91
+ const blobId = await this._blobStorageComponent.create(Converter.bytesToBase64(RandomHelper.generate(32)), MimeTypes.OctetStream, undefined, {
92
+ "@context": "https://schema.org/",
93
+ "@type": "Thing",
94
+ description: "health"
95
+ }, { disableEncryption: true });
96
+ const entry = await this._blobStorageComponent.get(blobId);
97
+ await this._blobStorageComponent.remove(blobId);
98
+ return [
99
+ {
100
+ source: DocumentManagementService.CLASS_NAME,
101
+ category: HealthCategory.Application,
102
+ status: Is.object(entry) ? HealthStatus.Ok : HealthStatus.Error,
103
+ description: "healthDescription",
104
+ message: Is.object(entry) ? undefined : "getBlobFailed"
105
+ }
106
+ ];
107
+ }
108
+ catch (error) {
109
+ return [
110
+ {
111
+ source: DocumentManagementService.CLASS_NAME,
112
+ category: HealthCategory.Application,
113
+ status: HealthStatus.Error,
114
+ description: "healthDescription",
115
+ message: "getBlobFailed",
116
+ error: BaseError.fromError(error)
117
+ }
118
+ ];
119
+ }
120
+ }
121
+ /**
122
+ * Register all document management metrics with the telemetry component.
123
+ * @returns A promise that resolves when metrics have been registered.
124
+ */
125
+ async start() {
126
+ if (Is.undefined(this._telemetryComponent)) {
127
+ return;
128
+ }
129
+ await MetricHelper.createMetrics(this._telemetryComponent, DocumentManagementMetrics);
130
+ }
131
+ /**
132
+ * Store a document as an auditable item graph vertex and add its content to blob storage.
133
+ * If the document id already exists and the blob data is different a new revision will be created.
134
+ * For any other changes the current revision will be updated.
135
+ * @param document The document base properties.
136
+ * @param blob The data to create the document with as bytes, or an existing blob storage entry id.
137
+ * @param auditableItemGraphEdges The auditable item graph vertices to connect the document to.
138
+ * @param options Additional options for the set operation.
139
+ * @param options.includeAttestation Flag to include an attestation for the document, defaults to false.
140
+ * @param options.includeAlias Flag to add the document id as an alias to the aig vertex, defaults to true.
141
+ * @param options.aliasAnnotationObject Annotation object for the alias.
142
+ * @returns The auditable item graph vertex created for the document including its revision.
143
+ */
144
+ async create(document, blob, auditableItemGraphEdges, options) {
145
+ Guards.object(DocumentManagementService.CLASS_NAME, "document", document);
146
+ const { documentId, documentIdFormat, documentCode, annotationObject } = document;
147
+ Guards.stringValue(DocumentManagementService.CLASS_NAME, "documentId", documentId);
148
+ Guards.arrayOneOf(DocumentManagementService.CLASS_NAME, "documentCode", documentCode, Object.values(UneceDocumentCodeList));
149
+ if (!Is.uint8Array(blob) && !Is.stringValue(blob)) {
150
+ Guards.uint8Array(DocumentManagementService.CLASS_NAME, "blob", blob);
151
+ }
152
+ const contextIds = await ContextIdStore.getContextIds();
153
+ // Serialises creates of identical content, so the rollback below cannot remove a blob
154
+ // another request has deduplicated onto.
155
+ let blobIntegrityLockKey;
156
+ try {
157
+ const documentVertex = {
158
+ "@context": [AuditableItemGraphContexts.Context, AuditableItemGraphContexts.ContextCommon],
159
+ type: AuditableItemGraphTypes.Vertex
160
+ };
161
+ if (options?.includeAlias ?? true) {
162
+ documentVertex.aliases ??= [];
163
+ documentVertex.aliases.push({
164
+ "@context": AuditableItemGraphContexts.Context,
165
+ type: AuditableItemGraphTypes.Alias,
166
+ id: documentId,
167
+ aliasFormat: documentIdFormat,
168
+ annotationObject: options?.aliasAnnotationObject
169
+ });
170
+ }
171
+ // Resolve the blob storage id and integrity.
172
+ // If blob is a Uint8Array, upload the bytes; if it is a string, treat it as an
173
+ // existing blobStorageId and fetch the entry's stored integrity.
174
+ const blobIntegrity = await this.computeBlobIntegrity(blob);
175
+ if (Is.uint8Array(blob)) {
176
+ const lockKey = `${DocumentManagementService._BLOB_INTEGRITY_LOCK_PREFIX}${blobIntegrity}`;
177
+ await Mutex.lock(lockKey, {
178
+ throwOnTimeout: true,
179
+ timeoutMs: this._mutexTimeoutMs
180
+ });
181
+ // Recorded only once held, so a timeout never releases another caller's key.
182
+ blobIntegrityLockKey = lockKey;
183
+ }
184
+ let blobStorageId;
185
+ const blobCreateCorrelationId = Is.uint8Array(blob) ? crypto.randomUUID() : undefined;
186
+ if (Is.uint8Array(blob)) {
187
+ blobStorageId = await this._blobStorageComponent.create(Converter.bytesToBase64(blob), undefined, undefined, {
188
+ "@context": SchemaOrgContexts.Context,
189
+ identifier: blobCreateCorrelationId
190
+ });
191
+ }
192
+ else {
193
+ blobStorageId = blob;
194
+ }
195
+ const currentRevision = {
196
+ "@context": [
197
+ SchemaOrgContexts.Context,
198
+ DocumentContexts.Context,
199
+ DocumentContexts.ContextCommon
200
+ ],
201
+ type: DocumentTypes.Document,
202
+ id: this.createDocumentId(documentId, 0),
203
+ documentId,
204
+ documentIdFormat,
205
+ documentCode,
206
+ documentRevision: 0,
207
+ annotationObject,
208
+ integrity: blobIntegrity,
209
+ blobStorageId,
210
+ dateCreated: new Date(Date.now()).toISOString(),
211
+ organizationIdentity: contextIds?.[ContextIdKeys.Organization],
212
+ userIdentity: contextIds?.[ContextIdKeys.User]
213
+ };
214
+ if (options?.includeAttestation ?? false) {
215
+ currentRevision.attestationId = await this.createAttestation(currentRevision);
216
+ }
217
+ // Add the new revision in to the vertex
218
+ documentVertex.resources ??= [];
219
+ documentVertex.resources.push({
220
+ "@context": AuditableItemGraphContexts.Context,
221
+ type: AuditableItemGraphTypes.Resource,
222
+ resourceObject: currentRevision
223
+ });
224
+ // Add the outgoing edges from the document vertex to each connected item
225
+ if (Is.arrayValue(auditableItemGraphEdges)) {
226
+ documentVertex.edges ??= [];
227
+ for (const aigEdge of auditableItemGraphEdges) {
228
+ documentVertex.edges.push({
229
+ "@context": AuditableItemGraphContexts.Context,
230
+ type: AuditableItemGraphTypes.Edge,
231
+ targetId: aigEdge.targetId,
232
+ edgeRelationships: ["document"]
233
+ });
234
+ }
235
+ }
236
+ // And create the vertex
237
+ const vertexId = await this._auditableItemGraphComponent.create(ObjectHelper.removeEmptyProperties(documentVertex));
238
+ // Now add the edges to the connected vertices.
239
+ // isCreatePath = true enables fail-fast + rollback if a target vertex is missing.
240
+ const failingVertexId = await this.updateConnectedEdges(vertexId, auditableItemGraphEdges ?? [], [], documentId, documentIdFormat, true);
241
+ if (Is.stringValue(failingVertexId)) {
242
+ // At least one connected vertex was missing. Back-edges already written have been
243
+ // rolled back by updateConnectedEdges. Best-effort cleanup: remove the blob only
244
+ // if this request can prove ownership via a create correlation id in blob
245
+ // metadata, which the integrity lock keeps exclusive, then soft-delete the
246
+ // document resource so the vertex is left empty.
247
+ if (Is.stringValue(blobCreateCorrelationId)) {
248
+ await this.removeBlobIfCreatedByRequest(blobStorageId, blobCreateCorrelationId);
249
+ }
250
+ try {
251
+ await this._auditableItemGraphComponent.updatePartial({
252
+ "@context": [
253
+ AuditableItemGraphContexts.Context,
254
+ AuditableItemGraphContexts.ContextCommon
255
+ ],
256
+ id: vertexId,
257
+ resourcePatches: { remove: [currentRevision.id] }
258
+ });
259
+ }
260
+ catch { }
261
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "connectedVertexNotFound", failingVertexId);
262
+ }
263
+ await MetricHelper.metricIncrement(this._telemetryComponent, DocumentManagementMetricIds.DocumentsCreated, { hasAttestation: Is.stringValue(currentRevision.attestationId) });
264
+ return vertexId;
265
+ }
266
+ catch (error) {
267
+ if (BaseError.someErrorName(error, "NotFoundError")) {
268
+ throw error;
269
+ }
270
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "createFailed", undefined, error);
271
+ }
272
+ finally {
273
+ if (Is.stringValue(blobIntegrityLockKey)) {
274
+ Mutex.unlock(blobIntegrityLockKey);
275
+ }
276
+ }
277
+ }
278
+ /**
279
+ * Update a document as an auditable item graph vertex and add its content to blob storage.
280
+ * If the blob data is different a new revision will be created.
281
+ * For any other changes the current revision will be updated.
282
+ * @param auditableItemGraphDocumentId The auditable item graph vertex id which contains the document.
283
+ * @param document The document base properties to update. Only annotationObject is applied; other fields are ignored.
284
+ * @param blob The data to update the document with as bytes, or an existing blob storage entry id.
285
+ * @param auditableItemGraphEdges Explicit edge delta to apply. If undefined, existing connections
286
+ * are retained unchanged. Use `add` to create new connections and `remove` to disconnect existing
287
+ * ones by their target vertex id. To update alias metadata on an already-connected vertex, include
288
+ * it in `add` with the updated `aliasAnnotationObject` - AIG's alias patch is an upsert, so the
289
+ * alias is updated in place without creating a duplicate back-edge.
290
+ * @param auditableItemGraphEdges.add Connections to add; each creates a back-edge on the connected vertex.
291
+ * @param auditableItemGraphEdges.remove Target vertex IDs to disconnect; their back-edges are removed.
292
+ * @param options Additional options for the update operation.
293
+ * @param options.includeAttestation Set to true to include an attestation for the document, or false to remove the existing attestation. Omit (undefined) to leave attestation state unchanged.
294
+ * @param options.includeAlias Set to true to add the document id as an alias on the aig vertex, or false to remove it. Omit to leave alias state unchanged.
295
+ * @param options.aliasAnnotationObject Annotation object for the alias when adding.
296
+ * @returns A promise that resolves when the document has been updated.
297
+ */
298
+ async updatePartial(auditableItemGraphDocumentId, document, blob, auditableItemGraphEdges, options) {
299
+ Urn.guard(DocumentManagementService.CLASS_NAME, "auditableItemGraphDocumentId", auditableItemGraphDocumentId);
300
+ await Mutex.lock(auditableItemGraphDocumentId, {
301
+ throwOnTimeout: true,
302
+ timeoutMs: this._mutexTimeoutMs
303
+ });
304
+ try {
305
+ const documentVertex = await this._auditableItemGraphComponent.get(auditableItemGraphDocumentId, { includeDeleted: true });
306
+ if (Is.empty(documentVertex.resources)) {
307
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "documentRevisionNone");
308
+ }
309
+ const documents = await this.getDocumentsFromVertex(documentVertex, undefined, undefined, 1);
310
+ const latestRevision = documents.entries.itemListElement[0];
311
+ documentVertex.resources = documentVertex.resources.filter(r => Is.empty(r.dateDeleted));
312
+ if (Is.empty(latestRevision)) {
313
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "documentRevisionNone");
314
+ }
315
+ const { annotationObject, documentIdFormat, documentCode } = document ?? {};
316
+ // If auditableItemGraphEdges is undefined we are not updating the edges
317
+ // an empty array can be passed to remove all edges
318
+ const resourcePatchesAdd = [];
319
+ let blobRevisionCreated = false;
320
+ let newRevisionHasAttestation = false;
321
+ // If the blob is set and its hash has changed then we create a new revision.
322
+ // blob may be raw bytes (Uint8Array) to upload, or a string treated as an existing
323
+ // blobStorageId whose integrity is fetched from the blob storage entry.
324
+ if (Is.uint8Array(blob) || Is.stringValue(blob)) {
325
+ // Short-circuit: same blobStorageId reference on a live revision means the blob is
326
+ // unchanged - skip the network GET inside computeBlobIntegrity.
327
+ const blobUnchanged = Is.stringValue(blob) &&
328
+ blob === latestRevision.blobStorageId &&
329
+ Is.empty(latestRevision.dateDeleted);
330
+ const newIntegrity = blobUnchanged
331
+ ? latestRevision.integrity
332
+ : await this.computeBlobIntegrity(blob);
333
+ if (latestRevision.integrity !== newIntegrity) {
334
+ let blobStorageId;
335
+ if (Is.stringValue(blob)) {
336
+ blobStorageId = blob;
337
+ }
338
+ else {
339
+ blobStorageId = await this._blobStorageComponent.create(Converter.bytesToBase64(blob));
340
+ }
341
+ const newRevision = ObjectHelper.clone(latestRevision);
342
+ newRevision.documentRevision++;
343
+ newRevision.id = this.createDocumentId(newRevision.documentId, newRevision.documentRevision);
344
+ newRevision.integrity = newIntegrity;
345
+ newRevision.blobStorageId = blobStorageId;
346
+ this.applyDocumentFieldPatch(newRevision, document);
347
+ if (options?.includeAttestation === false) {
348
+ if (Is.stringValue(newRevision.attestationId)) {
349
+ await this._attestationComponent.destroy(newRevision.attestationId);
350
+ }
351
+ delete newRevision.attestationId;
352
+ }
353
+ else if (Is.stringValue(latestRevision.attestationId) || options?.includeAttestation) {
354
+ newRevision.attestationId = await this.createAttestation(newRevision);
355
+ }
356
+ resourcePatchesAdd.push({
357
+ "@context": AuditableItemGraphContexts.Context,
358
+ type: AuditableItemGraphTypes.Resource,
359
+ resourceObject: JsonLdHelper.toNodeObject(newRevision)
360
+ });
361
+ newRevisionHasAttestation = Is.stringValue(newRevision.attestationId);
362
+ blobRevisionCreated = true;
363
+ }
364
+ else if (Is.stringValue(latestRevision.dateDeleted)) {
365
+ // Same content as the most recent (soft-deleted) revision - restore it.
366
+ const restoredRevision = ObjectHelper.clone(latestRevision);
367
+ delete restoredRevision.dateDeleted;
368
+ this.applyDocumentFieldPatch(restoredRevision, document);
369
+ if (options?.includeAttestation === false) {
370
+ if (Is.stringValue(restoredRevision.attestationId)) {
371
+ await this._attestationComponent.destroy(restoredRevision.attestationId);
372
+ }
373
+ delete restoredRevision.attestationId;
374
+ }
375
+ else if (Is.stringValue(restoredRevision.attestationId) ||
376
+ options?.includeAttestation) {
377
+ restoredRevision.attestationId = await this.createAttestation(restoredRevision);
378
+ }
379
+ resourcePatchesAdd.push({
380
+ "@context": AuditableItemGraphContexts.Context,
381
+ type: AuditableItemGraphTypes.Resource,
382
+ resourceObject: JsonLdHelper.toNodeObject(restoredRevision)
383
+ });
384
+ newRevisionHasAttestation = Is.stringValue(restoredRevision.attestationId);
385
+ blobRevisionCreated = true;
386
+ }
387
+ }
388
+ // If no new revision was created, apply in-place updates to the current revision:
389
+ // annotation changes and/or adding first-time attestation.
390
+ // Undefined annotationObject means "no change" in patch semantics - it does not clear it.
391
+ if (!blobRevisionCreated) {
392
+ const annotationChanged = !Is.empty(annotationObject) &&
393
+ !ObjectHelper.equal(latestRevision.annotationObject, annotationObject);
394
+ const documentIdFormatChanged = !Is.empty(documentIdFormat) && latestRevision.documentIdFormat !== documentIdFormat;
395
+ const documentCodeChanged = !Is.empty(documentCode) && latestRevision.documentCode !== documentCode;
396
+ const addingAttestation = options?.includeAttestation === true && !Is.stringValue(latestRevision.attestationId);
397
+ const removingAttestation = options?.includeAttestation === false && Is.stringValue(latestRevision.attestationId);
398
+ if (annotationChanged ||
399
+ documentIdFormatChanged ||
400
+ documentCodeChanged ||
401
+ addingAttestation ||
402
+ removingAttestation) {
403
+ if (annotationChanged) {
404
+ latestRevision.annotationObject = annotationObject;
405
+ }
406
+ if (documentIdFormatChanged) {
407
+ latestRevision.documentIdFormat = documentIdFormat;
408
+ }
409
+ if (documentCodeChanged) {
410
+ latestRevision.documentCode = documentCode;
411
+ }
412
+ if (addingAttestation) {
413
+ latestRevision.attestationId = await this.createAttestation(latestRevision);
414
+ }
415
+ if (removingAttestation && Is.stringValue(latestRevision.attestationId)) {
416
+ await this._attestationComponent.destroy(latestRevision.attestationId);
417
+ delete latestRevision.attestationId;
418
+ }
419
+ latestRevision.dateModified = new Date(Date.now()).toISOString();
420
+ resourcePatchesAdd.push(ObjectHelper.removeEmptyProperties({
421
+ "@context": AuditableItemGraphContexts.Context,
422
+ type: AuditableItemGraphTypes.Resource,
423
+ resourceObject: JsonLdHelper.toNodeObject(latestRevision)
424
+ }));
425
+ }
426
+ }
427
+ // Build document-vertex edge patches directly from the explicit delta.
428
+ const edgesToAdd = auditableItemGraphEdges?.add ?? [];
429
+ const edgeTargetIdsToRemove = auditableItemGraphEdges?.remove ?? [];
430
+ const hasEdgeChanges = !Is.empty(auditableItemGraphEdges) &&
431
+ (edgesToAdd.length > 0 || edgeTargetIdsToRemove.length > 0);
432
+ const addingAlias = options?.includeAlias === true;
433
+ const removingAlias = options?.includeAlias === false;
434
+ const hasAliasChange = addingAlias || removingAlias;
435
+ const documentEdgePatchesAdd = edgesToAdd.map(aigEdge => ({
436
+ "@context": AuditableItemGraphContexts.Context,
437
+ type: AuditableItemGraphTypes.Edge,
438
+ targetId: aigEdge.targetId,
439
+ edgeRelationships: ["document"]
440
+ }));
441
+ // Resolve remove targetIds to stored edge IDs for the document vertex patch.
442
+ const documentEdgePatchesRemove = edgeTargetIdsToRemove
443
+ .map(targetId => documentVertex.edges?.find(e => e.targetId === targetId && Is.empty(e.dateDeleted))?.id)
444
+ .filter((id) => Is.stringValue(id));
445
+ if (resourcePatchesAdd.length > 0 || hasEdgeChanges || hasAliasChange) {
446
+ const partial = {
447
+ "@context": [
448
+ AuditableItemGraphContexts.Context,
449
+ AuditableItemGraphContexts.ContextCommon
450
+ ],
451
+ id: auditableItemGraphDocumentId
452
+ };
453
+ if (resourcePatchesAdd.length > 0) {
454
+ partial.resourcePatches = { add: resourcePatchesAdd };
455
+ }
456
+ if (hasEdgeChanges) {
457
+ partial.edgePatches = {
458
+ add: documentEdgePatchesAdd.length > 0 ? documentEdgePatchesAdd : undefined,
459
+ remove: documentEdgePatchesRemove.length > 0 ? documentEdgePatchesRemove : undefined
460
+ };
461
+ }
462
+ if (addingAlias) {
463
+ partial.aliasPatches = {
464
+ add: [
465
+ ObjectHelper.removeEmptyProperties({
466
+ "@context": AuditableItemGraphContexts.Context,
467
+ type: AuditableItemGraphTypes.Alias,
468
+ id: latestRevision.documentId,
469
+ aliasFormat: latestRevision.documentIdFormat,
470
+ annotationObject: options?.aliasAnnotationObject
471
+ })
472
+ ]
473
+ };
474
+ }
475
+ else if (removingAlias) {
476
+ partial.aliasPatches = { remove: [latestRevision.documentId] };
477
+ }
478
+ await this._auditableItemGraphComponent.updatePartial(partial);
479
+ }
480
+ if (hasEdgeChanges) {
481
+ await this.updateConnectedEdges(auditableItemGraphDocumentId, edgesToAdd, edgeTargetIdsToRemove, latestRevision.documentId, latestRevision.documentIdFormat);
482
+ }
483
+ const updatedVertex = resourcePatchesAdd.length > 0 || hasEdgeChanges || hasAliasChange;
484
+ if (blobRevisionCreated) {
485
+ await MetricHelper.metricIncrement(this._telemetryComponent, DocumentManagementMetricIds.RevisionsCreated, { hasAttestation: newRevisionHasAttestation });
486
+ }
487
+ if (updatedVertex) {
488
+ await MetricHelper.metricIncrement(this._telemetryComponent, DocumentManagementMetricIds.DocumentsUpdated, { hasNewRevision: blobRevisionCreated });
489
+ }
490
+ }
491
+ catch (error) {
492
+ if (BaseError.someErrorName(error, "NotFoundError")) {
493
+ throw error;
494
+ }
495
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "updateFailed", undefined, error);
496
+ }
497
+ finally {
498
+ Mutex.unlock(auditableItemGraphDocumentId);
499
+ }
500
+ }
501
+ /**
502
+ * Get a document using it's auditable item graph vertex id and optional revision.
503
+ * @param auditableItemGraphDocumentId The auditable item graph vertex id which contains the document.
504
+ * @param options Additional options for the get operation.
505
+ * @param options.includeBlobStorageMetadata Flag to include the blob storage metadata for the document, defaults to false.
506
+ * @param options.includeBlobStorageData Flag to include the blob storage data for the document, defaults to false.
507
+ * @param options.includeAttestation Flag to include the attestation information for the document, defaults to false.
508
+ * @param options.includeRemoved Flag to include deleted documents, defaults to false.
509
+ * @param options.includeDeletedEdges Flag to include soft-deleted edges in the response, defaults to false.
510
+ * @param options.extractRuleGroupId If provided will extract data from the document using the specified rule group id.
511
+ * @param options.extractMimeType By default extraction will auto detect the mime type of the document, this can be used to override the detection.
512
+ * @param cursor The cursor to get the next chunk of revisions.
513
+ * @param limit Limit the number of items to return, defaults to 1 so only most recent is returned.
514
+ * @returns The documents and revisions if requested, ordered by revision descending, cursor is set if there are more document revisions.
515
+ */
516
+ async get(auditableItemGraphDocumentId, options, cursor, limit) {
517
+ Urn.guard(DocumentManagementService.CLASS_NAME, "auditableItemGraphDocumentId", auditableItemGraphDocumentId);
518
+ try {
519
+ const documentVertex = await this._auditableItemGraphComponent.get(auditableItemGraphDocumentId, {
520
+ includeDeleted: (options?.includeRemoved ?? false) || (options?.includeDeletedEdges ?? false)
521
+ });
522
+ // If we fetched deleted items to expose edges but the caller did not ask for deleted
523
+ // documents, strip the deleted resources so they don't appear in the output.
524
+ if ((options?.includeDeletedEdges ?? false) && !(options?.includeRemoved ?? false)) {
525
+ documentVertex.resources = documentVertex.resources?.filter(r => Is.empty(r.dateDeleted));
526
+ }
527
+ // Populate the document and revisions with the options set
528
+ const documents = await this.getDocumentsFromVertex(documentVertex, options, cursor, limit);
529
+ const result = await JsonLdProcessor.compact(documents.entries, documents.entries["@context"], { compactArrays: false });
530
+ return {
531
+ entries: result,
532
+ cursor: documents.cursor
533
+ };
534
+ }
535
+ catch (error) {
536
+ if (BaseError.someErrorName(error, "NotFoundError")) {
537
+ throw error;
538
+ }
539
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "getFailed", undefined, error);
540
+ }
541
+ }
542
+ /**
543
+ * Get a document revision using it's auditable item graph vertex id.
544
+ * @param auditableItemGraphDocumentId The auditable item graph vertex id which contains the document.
545
+ * @param revision The revision id for the document.
546
+ * @param options Additional options for the get operation.
547
+ * @param options.includeBlobStorageMetadata Flag to include the blob storage metadata for the document, defaults to false.
548
+ * @param options.includeBlobStorageData Flag to include the blob storage data for the document, defaults to false.
549
+ * @param options.includeAttestation Flag to include the attestation information for the document, defaults to false.
550
+ * @param options.extractRuleGroupId If provided will extract data from the document using the specified rule group id.
551
+ * @param options.extractMimeType By default extraction will auto detect the mime type of the document, this can be used to override the detection.
552
+ * @returns The document for the specified revision.
553
+ */
554
+ async getRevision(auditableItemGraphDocumentId, revision, options) {
555
+ Urn.guard(DocumentManagementService.CLASS_NAME, "auditableItemGraphDocumentId", auditableItemGraphDocumentId);
556
+ Guards.integer(DocumentManagementService.CLASS_NAME, "revision", revision);
557
+ try {
558
+ const documentVertex = await this._auditableItemGraphComponent.get(auditableItemGraphDocumentId, { includeDeleted: true });
559
+ if (Is.empty(documentVertex.resources)) {
560
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "documentRevisionNone");
561
+ }
562
+ documentVertex.resources = documentVertex.resources.filter(d => d.resourceObject?.documentRevision === revision);
563
+ if (documentVertex.resources.length === 0) {
564
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "documentRevisionNotFound", revision.toString());
565
+ }
566
+ // Populate the document and revisions with the options set
567
+ const docList = await this.getDocumentsFromVertex(documentVertex, options);
568
+ const result = await JsonLdProcessor.compact(docList.entries.itemListElement[0], docList.entries.itemListElement[0]["@context"], { compactArrays: false });
569
+ return result;
570
+ }
571
+ catch (error) {
572
+ if (BaseError.someErrorName(error, "NotFoundError")) {
573
+ throw error;
574
+ }
575
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "getRevisionFailed", undefined, error);
576
+ }
577
+ }
578
+ /**
579
+ * Remove an auditable item graph vertex using it's id.
580
+ * The document dateDeleted will be set, but can still be queried with the includeRemoved flag.
581
+ * @param auditableItemGraphDocumentId The auditable item graph vertex id which contains the document.
582
+ * @param revision The revision of the document to remove.
583
+ * @returns A promise that resolves when the revision has been removed.
584
+ */
585
+ async removeRevision(auditableItemGraphDocumentId, revision) {
586
+ Urn.guard(DocumentManagementService.CLASS_NAME, "auditableItemGraphDocumentId", auditableItemGraphDocumentId);
587
+ Guards.integer(DocumentManagementService.CLASS_NAME, "revision", revision);
588
+ await Mutex.lock(auditableItemGraphDocumentId, {
589
+ throwOnTimeout: true,
590
+ timeoutMs: this._mutexTimeoutMs
591
+ });
592
+ try {
593
+ const documentVertex = await this._auditableItemGraphComponent.get(auditableItemGraphDocumentId);
594
+ if (Is.empty(documentVertex.resources)) {
595
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "documentRevisionNone");
596
+ }
597
+ const docRevisionIndex = documentVertex.resources.findIndex(d => d.resourceObject?.documentRevision === revision);
598
+ if (docRevisionIndex === -1) {
599
+ throw new NotFoundError(DocumentManagementService.CLASS_NAME, "documentRevisionNotFound", revision.toString());
600
+ }
601
+ const revisionResourceId = documentVertex.resources[docRevisionIndex].resourceObject?.id ??
602
+ documentVertex.resources[docRevisionIndex].resourceObject?.["@id"];
603
+ if (!Is.stringValue(revisionResourceId)) {
604
+ // The revision exists but its stored resource-id is unresolvable - integrity anomaly.
605
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "documentRevisionMissingId", {
606
+ revision
607
+ });
608
+ }
609
+ await this._auditableItemGraphComponent.updatePartial({
610
+ "@context": [AuditableItemGraphContexts.Context, AuditableItemGraphContexts.ContextCommon],
611
+ id: auditableItemGraphDocumentId,
612
+ resourcePatches: { remove: [revisionResourceId] }
613
+ });
614
+ await MetricHelper.metricIncrement(this._telemetryComponent, DocumentManagementMetricIds.RevisionsRemoved);
615
+ }
616
+ catch (error) {
617
+ if (BaseError.someErrorName(error, "NotFoundError")) {
618
+ throw error;
619
+ }
620
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "removeRevisionFailed", undefined, error);
621
+ }
622
+ finally {
623
+ Mutex.unlock(auditableItemGraphDocumentId);
624
+ }
625
+ }
626
+ /**
627
+ * Find all the document with a specific id.
628
+ * @param documentId The document id to find in the graph.
629
+ * @param cursor The cursor to get the next chunk of documents.
630
+ * @param limit The limit to get the next chunk of documents.
631
+ * @returns The graph vertices that contain documents referencing the specified document id.
632
+ */
633
+ async query(documentId, cursor, limit) {
634
+ Guards.stringValue(DocumentManagementService.CLASS_NAME, "documentId", documentId);
635
+ try {
636
+ const result = await this._auditableItemGraphComponent.query({
637
+ id: documentId,
638
+ idMode: "both",
639
+ resourceTypes: [DocumentTypes.Document]
640
+ }, undefined, undefined, undefined, ["id", "dateCreated", "dateModified", "aliases", "annotationObject", "resources", "edges"], cursor, limit);
641
+ return result;
642
+ }
643
+ catch (error) {
644
+ if (BaseError.someErrorName(error, "NotFoundError")) {
645
+ throw error;
646
+ }
647
+ throw new GeneralError(DocumentManagementService.CLASS_NAME, "queryFailed", undefined, error);
648
+ }
649
+ }
650
+ /**
651
+ * Update the edges on connected vertices using non-destructive patch operations.
652
+ * Uses updatePartial so AIG's per-vertex Mutex serialises concurrent callers.
653
+ *
654
+ * On the **create path** (`isCreatePath = true`) the method is fail-fast:
655
+ * if any back-edge write fails (target vertex does not exist), all back-edges
656
+ * already written in this call are removed (best-effort rollback) and the
657
+ * failing target vertex ID is returned so the caller can surface a meaningful error.
658
+ *
659
+ * On the **update path** each missing vertex is caught individually; the remaining
660
+ * updates continue and `undefined` is always returned.
661
+ * @param auditableItemGraphDocumentId The document id to use.
662
+ * @param edgesToAdd Connections to add - each connected vertex receives a new back-edge.
663
+ * @param edgeTargetIdsToRemove Target vertex IDs to disconnect - their back-edges are removed.
664
+ * @param documentId The document identifier.
665
+ * @param documentIdFormat The format of the document identifier.
666
+ * @param isCreatePath When true, enables fail-fast + rollback semantics.
667
+ * @returns The failing target vertex ID when `isCreatePath` is true and a write fails;
668
+ * `undefined` on success or when called from the update path.
669
+ * @internal
670
+ */
671
+ async updateConnectedEdges(auditableItemGraphDocumentId, edgesToAdd, edgeTargetIdsToRemove, documentId, documentIdFormat, isCreatePath = false) {
672
+ // Track which target IDs received a successful back-edge write so we can roll back
673
+ // if a later write fails (create path only).
674
+ const writtenTargetIds = [];
675
+ // Add back-edges to each newly connected vertex.
676
+ for (const aigEdge of edgesToAdd) {
677
+ const partial = {
678
+ "@context": [AuditableItemGraphContexts.Context, AuditableItemGraphContexts.ContextCommon],
679
+ id: aigEdge.targetId,
680
+ edgePatches: {
681
+ add: [
682
+ {
683
+ "@context": AuditableItemGraphContexts.Context,
684
+ type: AuditableItemGraphTypes.Edge,
685
+ targetId: auditableItemGraphDocumentId,
686
+ edgeRelationships: ["document"]
687
+ }
688
+ ]
689
+ }
690
+ };
691
+ if (aigEdge.includeAlias) {
692
+ partial.aliasPatches = {
693
+ add: [
694
+ {
695
+ "@context": AuditableItemGraphContexts.Context,
696
+ type: AuditableItemGraphTypes.Alias,
697
+ id: documentId,
698
+ aliasFormat: documentIdFormat,
699
+ annotationObject: aigEdge.aliasAnnotationObject
700
+ }
701
+ ]
702
+ };
703
+ }
704
+ if (isCreatePath) {
705
+ try {
706
+ await this._auditableItemGraphComponent.updatePartial(partial);
707
+ writtenTargetIds.push(aigEdge.targetId);
708
+ }
709
+ catch {
710
+ // Rollback all back-edges already written before this failure.
711
+ await this.rollbackConnectedEdges(auditableItemGraphDocumentId, writtenTargetIds, documentId);
712
+ return aigEdge.targetId;
713
+ }
714
+ }
715
+ else {
716
+ try {
717
+ await this._auditableItemGraphComponent.updatePartial(partial);
718
+ }
719
+ catch {
720
+ // Best-effort on the update path - swallow to avoid interrupting remaining back-edge writes.
721
+ }
722
+ }
723
+ }
724
+ // Remove back-edges from disconnected vertices.
725
+ for (const staleTargetId of edgeTargetIdsToRemove) {
726
+ await this.removeBackEdgeFromVertex(staleTargetId, auditableItemGraphDocumentId, documentId);
727
+ }
728
+ return undefined;
729
+ }
730
+ /**
731
+ * Best-effort removal of back-edges that were written during a failed create operation.
732
+ * Errors are silently swallowed to avoid masking the original failure.
733
+ * @param auditableItemGraphDocumentId The document vertex whose back-edges should be removed.
734
+ * @param targetIds The connected vertex IDs that received a back-edge.
735
+ * @param documentId The document identifier used for alias cleanup.
736
+ * @internal
737
+ */
738
+ async rollbackConnectedEdges(auditableItemGraphDocumentId, targetIds, documentId) {
739
+ for (const targetId of targetIds) {
740
+ try {
741
+ await this.removeBackEdgeFromVertex(targetId, auditableItemGraphDocumentId, documentId);
742
+ }
743
+ catch {
744
+ // Best-effort - do not let cleanup errors mask the original failure.
745
+ }
746
+ }
747
+ }
748
+ /**
749
+ * Remove the back-edge and alias that a connected vertex holds pointing at the document vertex.
750
+ * Fetches the connected vertex to resolve the stored edge ID, then issues a single updatePartial.
751
+ * No-ops silently if neither edge nor alias is found.
752
+ * @param targetId The connected vertex to patch.
753
+ * @param documentVertexId The document vertex whose back-edge should be removed.
754
+ * @param documentId The document id used for alias cleanup.
755
+ * @internal
756
+ */
757
+ async removeBackEdgeFromVertex(targetId, documentVertexId, documentId) {
758
+ const connected = await this._auditableItemGraphComponent.get(targetId);
759
+ const edgeId = connected.edges?.find(e => Is.empty(e.dateDeleted) && e.targetId === documentVertexId)?.id;
760
+ const hasAlias = Is.arrayValue(connected.aliases) &&
761
+ connected.aliases.some(a => Is.empty(a.dateDeleted) && a.id === documentId);
762
+ const partial = {
763
+ "@context": [AuditableItemGraphContexts.Context, AuditableItemGraphContexts.ContextCommon],
764
+ id: targetId
765
+ };
766
+ if (hasAlias) {
767
+ partial.aliasPatches = { remove: [documentId] };
768
+ }
769
+ if (Is.stringValue(edgeId)) {
770
+ partial.edgePatches = { remove: [edgeId] };
771
+ }
772
+ if (hasAlias || Is.stringValue(edgeId)) {
773
+ await this._auditableItemGraphComponent.updatePartial(partial);
774
+ }
775
+ }
776
+ /**
777
+ * Get the documents from the auditable item graph vertex.
778
+ * @param documentVertex The vertex containing the documents.
779
+ * @param options Additional options for the get operation.
780
+ * @param options.includeBlobStorageMetadata Flag to include the blob storage metadata for the document, defaults to false.
781
+ * @param options.includeBlobStorageData Flag to include the blob storage data for the document, defaults to false.
782
+ * @param options.includeAttestation Flag to include the attestation information for the document, defaults to false.
783
+ * @param options.includeDeletedEdges Flag to include soft-deleted edges in the response, defaults to false.
784
+ * @param options.extractRuleGroupId If provided will extract data from the document using the specified rule group id.
785
+ * @param options.extractMimeType By default extraction will auto detect the mime type of the document, this can be used to override the detection.
786
+ * @param cursor The cursor to get the next chunk of revisions.
787
+ * @param limit Limit the number of items to return, defaults to 1 so only most recent is returned.
788
+ * @returns The finalised list of documents.
789
+ * @internal
790
+ */
791
+ async getDocumentsFromVertex(documentVertex, options, cursor, limit) {
792
+ const docList = {
793
+ "@context": [
794
+ SchemaOrgContexts.Context,
795
+ DocumentContexts.Context,
796
+ DocumentContexts.ContextCommon
797
+ ],
798
+ type: SchemaOrgTypes.ItemList,
799
+ [SchemaOrgTypes.ItemListElement]: []
800
+ };
801
+ let nextCursor;
802
+ if (Is.arrayValue(documentVertex.resources)) {
803
+ // Sort by newest revision first
804
+ documentVertex.resources.sort((a, b) => (Coerce.number(b.resourceObject?.documentRevision) ?? 0) -
805
+ (Coerce.number(a.resourceObject?.documentRevision) ?? 0));
806
+ const startIndex = Coerce.integer(cursor) ?? 0;
807
+ const safeLimit = NumberHelper.clamp(limit ?? 1, 1);
808
+ const endIndex = Math.min(startIndex + safeLimit, documentVertex.resources.length);
809
+ const slicedResources = documentVertex.resources.slice(startIndex, endIndex);
810
+ nextCursor = documentVertex.resources.length > endIndex ? endIndex.toString() : undefined;
811
+ const includeBlobStorageMetadata = options?.includeBlobStorageMetadata ?? false;
812
+ const includeBlobStorageData = options?.includeBlobStorageData ?? false;
813
+ const includeAttestation = options?.includeAttestation ?? false;
814
+ const extractData = Is.stringValue(options?.extractRuleGroupId);
815
+ for (let i = 0; i < slicedResources.length; i++) {
816
+ const document = slicedResources[i].resourceObject;
817
+ if (Is.object(document) && document.type === DocumentTypes.Document) {
818
+ document.dateDeleted = slicedResources[i].dateDeleted;
819
+ docList[SchemaOrgTypes.ItemListElement].push(document);
820
+ const blobRequired = includeBlobStorageMetadata || includeBlobStorageData;
821
+ if (blobRequired || extractData) {
822
+ const blobEntry = await this._blobStorageComponent.get(document.blobStorageId, {
823
+ includeContent: includeBlobStorageData || extractData
824
+ });
825
+ if (blobRequired) {
826
+ document.blobStorageEntry = blobEntry;
827
+ if (Is.object(document.blobStorageEntry)) {
828
+ ObjectHelper.propertyDelete(document.blobStorageEntry, "@context");
829
+ }
830
+ if (!docList["@context"].includes(BlobStorageContexts.Context)) {
831
+ docList["@context"].push(BlobStorageContexts.Context);
832
+ }
833
+ }
834
+ if (Is.stringValue(options?.extractRuleGroupId) && Is.stringValue(blobEntry.blob)) {
835
+ const binaryBlob = Converter.base64ToBytes(blobEntry.blob);
836
+ document.extractedData = await this._dataProcessingComponent.extract(options.extractRuleGroupId, binaryBlob, undefined, options?.extractMimeType);
837
+ }
838
+ // If we have the blob data due to extraction but we weren't asked for it
839
+ // then we remove it from the document
840
+ if (!blobRequired) {
841
+ delete document.blobStorageEntry;
842
+ }
843
+ else if (!includeBlobStorageData) {
844
+ delete document.blobStorageEntry?.blob;
845
+ }
846
+ }
847
+ if (includeAttestation && Is.stringValue(document.attestationId)) {
848
+ const attestationInformation = await this._attestationComponent.get(document.attestationId);
849
+ document.attestationInformation = attestationInformation;
850
+ if (Is.object(document.attestationInformation)) {
851
+ ObjectHelper.propertyDelete(document.attestationInformation, "@context");
852
+ }
853
+ }
854
+ }
855
+ }
856
+ }
857
+ if (Is.arrayValue(documentVertex.edges)) {
858
+ docList.edges ??= [];
859
+ for (const edge of documentVertex.edges) {
860
+ if (Is.object(edge) &&
861
+ ((options?.includeDeletedEdges ?? false) || Is.empty(edge.dateDeleted))) {
862
+ docList.edges.push(edge.targetId);
863
+ }
864
+ }
865
+ }
866
+ return {
867
+ entries: docList,
868
+ cursor: nextCursor
869
+ };
870
+ }
871
+ /**
872
+ * Compute the integrity hash for a blob.
873
+ * For a Uint8Array the hash is computed locally; for a string blobStorageId the stored
874
+ * integrity is fetched from blob storage (one network call, no content download).
875
+ * @param blob The blob bytes or an existing blob storage entry id.
876
+ * @returns The SHA-256 integrity string.
877
+ * @internal
878
+ */
879
+ async computeBlobIntegrity(blob) {
880
+ if (Is.uint8Array(blob)) {
881
+ return IntegrityHelper.generate(IntegrityAlgorithm.Sha256, blob);
882
+ }
883
+ const blobEntry = await this._blobStorageComponent.get(blob, { includeContent: false });
884
+ return blobEntry.integrity;
885
+ }
886
+ /**
887
+ * Remove a blob on rollback only when its metadata proves this request created it.
888
+ * Uses the create-correlation identifier set during blob upload.
889
+ * @param blobStorageId The blob id to check and potentially remove.
890
+ * @param correlationId The request correlation id to match against blob metadata.
891
+ * @internal
892
+ */
893
+ async removeBlobIfCreatedByRequest(blobStorageId, correlationId) {
894
+ try {
895
+ const blobEntry = await this._blobStorageComponent.get(blobStorageId, {
896
+ includeContent: false
897
+ });
898
+ const metadataIdentifier = blobEntry.metadata
899
+ ?.identifier;
900
+ if (Is.stringValue(metadataIdentifier) && metadataIdentifier === correlationId) {
901
+ try {
902
+ await this._blobStorageComponent.remove(blobStorageId);
903
+ }
904
+ catch { }
905
+ }
906
+ }
907
+ catch {
908
+ // Best-effort cleanup only.
909
+ }
910
+ }
911
+ /**
912
+ * Apply a partial document field patch in-place to a revision object.
913
+ * Only non-empty values overwrite the existing fields; undefined means no change.
914
+ * @param revision The revision to mutate.
915
+ * @param patch The field values to apply.
916
+ * @internal
917
+ */
918
+ applyDocumentFieldPatch(revision, patch) {
919
+ const { annotationObject, documentIdFormat, documentCode } = patch ?? {};
920
+ if (!Is.empty(annotationObject)) {
921
+ revision.annotationObject = annotationObject;
922
+ }
923
+ if (!Is.empty(documentIdFormat)) {
924
+ revision.documentIdFormat = documentIdFormat;
925
+ }
926
+ if (!Is.empty(documentCode)) {
927
+ revision.documentCode = documentCode;
928
+ }
929
+ }
930
+ /**
931
+ * Create an attestation for the document.
932
+ * @param document The document to create the attestation for.
933
+ * @returns The attestation identifier.
934
+ * @internal
935
+ */
936
+ async createAttestation(document) {
937
+ const documentAttestation = {
938
+ "@context": [
939
+ SchemaOrgContexts.Context,
940
+ DocumentContexts.Context,
941
+ DocumentContexts.ContextCommon
942
+ ],
943
+ type: DocumentTypes.DocumentAttestation,
944
+ id: document.id,
945
+ documentId: document.documentId,
946
+ documentCode: document.documentCode,
947
+ documentRevision: document.documentRevision,
948
+ dateCreated: document.dateCreated,
949
+ integrity: document.integrity
950
+ };
951
+ const attestationId = await this._attestationComponent.create(documentAttestation);
952
+ await MetricHelper.metricIncrement(this._telemetryComponent, DocumentManagementMetricIds.AttestationsCreated);
953
+ return attestationId;
954
+ }
955
+ /**
956
+ * Create a document id from the document id and revision.
957
+ * @param documentId The document id to create.
958
+ * @param revision The revision of the document.
959
+ * @returns The document id.
960
+ * @internal
961
+ */
962
+ createDocumentId(documentId, revision) {
963
+ const documentIdHash = Converter.bytesToBase64Url(Sha256.sum256(Converter.utf8ToBytes(documentId)));
964
+ return `document:${documentIdHash}:${revision}`;
965
+ }
966
+ }
967
+ //# sourceMappingURL=documentManagementService.js.map