Skip to content

Commit 8d609fc

Browse files
committed
RDBC-1011 refactor: update remote attachment handling
- Replaced `RemoteAttachmentFlags` enum with string literals for enhanced type safety. - Introduced new remote attachment parameters with extended metadata handling. - Updated GetAttachmentOperation to integrate remote attachment processing using HTTP headers. - Improved metadata transformation logic for attachments and refactored utility methods.
1 parent 965e4c7 commit 8d609fc

8 files changed

Lines changed: 109 additions & 36 deletions

File tree

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
11
/**
22
* Flags that indicate the location and characteristics of an attachment.
33
*/
4-
export enum RemoteAttachmentFlags {
5-
/**
6-
* No flags are set. The attachment is stored locally.
7-
*/
8-
None = 0,
9-
10-
/**
11-
* The attachment is stored remotely in cloud storage rather than in the local database.
12-
*/
13-
Remote = 0x1
14-
}
4+
export type RemoteAttachmentFlags = "None" | "Remote"

‎src/Documents/Attachments/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Readable } from "node:stream";
22
import { HttpResponse } from "../../Primitives/Http.js";
33
import { closeHttpResponse } from "../../Utility/HttpUtil.js";
44
import { CapitalizeType } from "../../Types/index.js";
5-
import { RemoteAttachmentFlags } from "./RemoteAttachmentFlags.js";
5+
import { RemoteAttachmentParameters } from "../Operations/Attachments/RemoteAttachmentParameters.js";
66

77
export type AttachmentType = "Document" | "Revision";
88

@@ -11,7 +11,7 @@ export interface AttachmentName {
1111
hash: string;
1212
contentType: string;
1313
size: number;
14-
remoteParameters?: RemoteAttachmentFlags;
14+
remoteParameters?: RemoteAttachmentParameters;
1515
}
1616

1717
export interface AttachmentNameWithCount extends AttachmentName {

‎src/Documents/Commands/Batches/PutAttachmentCommandData.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RemoteAttachmentParameters } from "../../Operations/Attachments/RemoteA
44
import { StringUtil } from "../../../Utility/StringUtil.js";
55
import { throwError } from "../../../Exceptions/index.js";
66
import { DocumentConventions } from "../../Conventions/DocumentConventions.js";
7+
import { RemoteAttachmentFlags } from "../../Attachments/RemoteAttachmentFlags.js";
78

89
interface DocumentAttachmentDto {
910
Id: string;
@@ -16,8 +17,8 @@ interface DocumentAttachmentDto {
1617

1718
interface RemoteParametersDto {
1819
Identifier: string;
19-
Flags: number;
20-
At?: string;
20+
Flags: RemoteAttachmentFlags;
21+
At?: Date;
2122
}
2223

2324
export class PutAttachmentCommandData implements ICommandData {
@@ -66,7 +67,7 @@ export class PutAttachmentCommandData implements ICommandData {
6667
result.RemoteParameters = {
6768
Identifier: this.remoteParameters.identifier,
6869
Flags: this.remoteParameters.flags,
69-
At: this.remoteParameters.at?.toISOString()
70+
At: this.remoteParameters.at
7071
};
7172
}
7273

‎src/Documents/Operations/Attachments/GetAttachmentOperation.ts‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { IOperation, OperationResultType } from "../OperationAbstractions.js";
2-
import { AttachmentDetails } from "../../Attachments/index.js";
2+
import { AttachmentDetails, AttachmentResult, AttachmentType } from "../../Attachments/index.js";
33
import { getEtagHeader } from "../../../Utility/HttpUtil.js";
44
import { HttpRequestParameters, HttpResponse } from "../../../Primitives/Http.js";
5-
import { AttachmentResult, AttachmentType } from "../../Attachments/index.js";
65
import { RavenCommand, ResponseDisposeHandling } from "../../../Http/RavenCommand.js";
76
import { HttpCache } from "../../../Http/HttpCache.js";
87
import { IDocumentStore } from "../../IDocumentStore.js";
@@ -11,6 +10,10 @@ import { throwError } from "../../../Exceptions/index.js";
1110
import { StringUtil } from "../../../Utility/StringUtil.js";
1211
import { ServerNode } from "../../../Http/ServerNode.js";
1312
import { Readable } from "node:stream";
13+
import { HEADERS } from "../../../Constants.js";
14+
import { RemoteAttachmentFlags } from "../../Attachments/RemoteAttachmentFlags.js";
15+
import { RemoteAttachmentParameters } from "./RemoteAttachmentParameters.js";
16+
import { DateUtil } from "../../../Utility/DateUtil.js";
1417

1518
export class GetAttachmentOperation implements IOperation<AttachmentResult> {
1619
private readonly _documentId: string;
@@ -37,6 +40,12 @@ export class GetAttachmentOperation implements IOperation<AttachmentResult> {
3740

3841
}
3942

43+
interface InternalRemoteAttachmentParameters {
44+
identifier: string;
45+
flags: RemoteAttachmentFlags;
46+
at: Date;
47+
}
48+
4049
export class GetAttachmentCommand extends RavenCommand<AttachmentResult> {
4150
private readonly _documentId: string;
4251
private readonly _name: string;
@@ -91,6 +100,10 @@ export class GetAttachmentCommand extends RavenCommand<AttachmentResult> {
91100
const hash = response.headers.get("attachment-hash") as string;
92101
let size = 0;
93102
const sizeHeader = response.headers.get("attachment-size") as string;
103+
const remoteParametersIdentifier = response.headers.get(HEADERS.ATTACHMENT_REMOTE_PARAMETERS_IDENTIFIER) as string;
104+
const remoteParametersAt = response.headers.get(HEADERS.ATTACHMENT_REMOTE_PARAMETERS_AT) as string; // iso date
105+
const remoteParametersFlags = response.headers.get(HEADERS.ATTACHMENT_REMOTE_PARAMETERS_FLAGS) as RemoteAttachmentFlags;
106+
94107
if (sizeHeader) {
95108
size = Number.parseInt(sizeHeader, 10);
96109
}
@@ -104,11 +117,22 @@ export class GetAttachmentCommand extends RavenCommand<AttachmentResult> {
104117
size
105118
};
106119

120+
if (remoteParametersIdentifier && remoteParametersAt && remoteParametersFlags) {
121+
const remoteParameters: InternalRemoteAttachmentParameters = {
122+
at: DateUtil.utc.parse(remoteParametersAt),
123+
identifier: remoteParametersIdentifier,
124+
flags: remoteParametersFlags,
125+
}
126+
127+
details.remoteParameters = remoteParameters as RemoteAttachmentParameters
128+
}
129+
130+
107131
this.result = new AttachmentResult(bodyStream, details, response);
108132
return "Manually";
109133
}
110134

111135
public get isReadRequest() {
112136
return true;
113137
}
114-
}
138+
}

‎src/Documents/Operations/Attachments/RemoteAttachmentParameters.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class RemoteAttachmentParameters {
2626

2727
constructor(identifier: string, at?: Date) {
2828
this.identifier = identifier;
29-
this._flags = RemoteAttachmentFlags.None; // Always set to None for user-created instances
29+
this._flags = "None"; // Always set to None for user-created instances
3030
this.at = at;
3131
}
3232

@@ -38,3 +38,4 @@ export class RemoteAttachmentParameters {
3838
return this._flags;
3939
}
4040
}
41+

‎src/Utility/ObjectUtil.ts‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ export class ObjectUtil {
116116
return transformed;
117117
}
118118

119+
120+
119121
public static transformMetadataKeys(metadata: MetadataObject, conventions: DocumentConventions) {
120122
if (!metadata) {
121123
return metadata;
@@ -128,15 +130,19 @@ export class ObjectUtil {
128130

129131
for (const [key, value] of Object.entries(metadata)) {
130132
if (key === CONSTANTS.Documents.Metadata.ATTACHMENTS) {
131-
result[CONSTANTS.Documents.Metadata.ATTACHMENTS] = value ? value.map(x => ObjectUtil.mapAttachmentDetailsToLocalObject(x)) : null
132-
} else if (key[0] === "@" || key === "Raven-Node-Type") {
133+
result[CONSTANTS.Documents.Metadata.ATTACHMENTS] = ObjectUtil.transformAttachments(value, conventions);
134+
continue;
135+
}
136+
137+
if (ObjectUtil.isSystemMetadataKey(key)) {
133138
result[key] = value;
139+
continue;
140+
}
141+
142+
if (needsCaseTransformation) {
143+
userMetadataFieldsToTransform[key] = value;
134144
} else {
135-
if (needsCaseTransformation) {
136-
userMetadataFieldsToTransform[key] = value;
137-
} else {
138-
result[key] = value;
139-
}
145+
result[key] = value;
140146
}
141147
}
142148

@@ -151,14 +157,25 @@ export class ObjectUtil {
151157
return result;
152158
}
153159

160+
private static transformAttachments(attachments: any[], conventions: DocumentConventions): any[] {
161+
return attachments?.map(x => ObjectUtil.transformObjectKeys(x, {
162+
defaultTransform: conventions.serverToLocalFieldNameConverter ?? ObjectUtil.camelCase
163+
})) ?? null;
164+
}
165+
166+
private static isSystemMetadataKey(key: string): boolean {
167+
return key[0] === "@" || key === "Raven-Node-Type";
168+
}
169+
154170
public static mapAttachmentDetailsToLocalObject(json: any): AttachmentDetails {
155171
return {
156172
changeVector: json.ChangeVector,
157173
contentType: json.ContentType,
158174
documentId: json.DocumentId,
159175
hash: json.Hash,
160176
name: json.Name,
161-
size: json.Size
177+
size: json.Size,
178+
remoteParameters: json.RemoteParameters,
162179
};
163180
}
164181

‎test/Ported/Attachments/DocumentSessionRemoteAttachmentsTests.ts‎

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import {testContext, disposeTestDocumentStore, RavenTestContext} from "../../Utils/TestUtil.js";
1+
import {disposeTestDocumentStore, RavenTestContext, testContext} from "../../Utils/TestUtil.js";
22
import {
3-
IDocumentStore,
43
ConfigureRemoteAttachmentsOperation,
4+
DateUtil,
5+
IDocumentStore,
6+
RemoteAttachmentParameters,
57
RemoteAttachmentsConfiguration,
68
RemoteAttachmentsDestinationConfiguration,
79
RemoteAttachmentsS3Settings,
8-
RemoteAttachmentParameters,
910
StoreAttachmentParameters
1011
} from "../../../src/index.js";
1112
import {assertThat} from "../../Utils/AssertExtensions.js";
1213
import {Readable} from "node:stream";
1314
import {Buffer} from "node:buffer";
15+
import {addHours, addMinutes, format} from "date-fns";
1416

1517
interface User {
1618
id?: string;
@@ -49,6 +51,7 @@ interface User {
4951
it("can store attachment with remote parameters using StoreAttachmentParameters", async () => {
5052
const identifier = await setupRemoteAttachmentsConfig();
5153
const userId = "users/1";
54+
const remoteAt = new Date();
5255

5356
{
5457
const session = store.openSession();
@@ -60,7 +63,7 @@ interface User {
6063
{
6164
const session = store.openSession();
6265
const attachmentData = Buffer.from([1, 2, 3, 4, 5]);
63-
const remoteAt = new Date(Date.now() + 60000); // 1 minute from now
66+
6467

6568
const parameters = new StoreAttachmentParameters(
6669
"profile.png",
@@ -84,12 +87,19 @@ interface User {
8487
.isEqualTo("profile.png");
8588
assertThat(attachment.details.contentType)
8689
.isEqualTo("image/png");
90+
assertThat(format(attachment.details.remoteParameters.at, DateUtil.DEFAULT_DATE_TZ_FORMAT))
91+
.isEqualTo(format(remoteAt, DateUtil.DEFAULT_DATE_TZ_FORMAT));
92+
assertThat(attachment.details.remoteParameters.identifier)
93+
.isEqualTo(identifier);
94+
assertThat(attachment.details.remoteParameters.flags)
95+
.isEqualTo("None");
8796
}
8897
});
8998

9099
it("can store attachment with remote parameters using direct method", async () => {
91100
const identifier = await setupRemoteAttachmentsConfig();
92101
const userId = "users/2";
102+
const remoteAt = new Date();
93103

94104
{
95105
const session = store.openSession();
@@ -101,7 +111,6 @@ interface User {
101111
{
102112
const session = store.openSession();
103113
const attachmentData = Buffer.from([10, 20, 30]);
104-
const remoteAt = new Date(Date.now() + 120000); // 2 minutes from now
105114

106115
const parameters = new StoreAttachmentParameters(
107116
"document.pdf",
@@ -126,6 +135,12 @@ interface User {
126135
.isEqualTo(1);
127136
assertThat(names[0].name)
128137
.isEqualTo("document.pdf");
138+
assertThat(format(names[0].remoteParameters.at, DateUtil.DEFAULT_DATE_TZ_FORMAT))
139+
.isEqualTo(format(remoteAt, DateUtil.DEFAULT_DATE_TZ_FORMAT));
140+
assertThat(names[0].remoteParameters.identifier)
141+
.isEqualTo(identifier);
142+
assertThat(names[0].remoteParameters.flags)
143+
.isEqualTo("None");
129144
}
130145
});
131146

@@ -160,6 +175,8 @@ interface User {
160175

161176
assertThat(attachment)
162177
.isNotNull();
178+
assertThat(attachment.details.remoteParameters)
179+
.isUndefined();
163180
assertThat(attachment.details.name)
164181
.isEqualTo("local-file.txt");
165182
}
@@ -206,7 +223,7 @@ interface User {
206223
Readable.from(data1),
207224
"application/octet-stream",
208225
null,
209-
new RemoteAttachmentParameters("S3-Primary", new Date(Date.now() + 60000))
226+
new RemoteAttachmentParameters("S3-Primary", addMinutes(new Date(), 1))
210227
);
211228
session.advanced.attachments.store(userId, params1);
212229

@@ -217,7 +234,7 @@ interface User {
217234
Readable.from(data2),
218235
"application/octet-stream",
219236
null,
220-
new RemoteAttachmentParameters("S3-Secondary", new Date(Date.now() + 120000))
237+
new RemoteAttachmentParameters("S3-Secondary", addMinutes(new Date(), 2))
221238
);
222239
session.advanced.attachments.store(userId, params2);
223240

@@ -237,6 +254,18 @@ interface User {
237254
.isEqualTo("primary.dat");
238255
assertThat(namesList[1])
239256
.isEqualTo("secondary.dat");
257+
258+
const attachment1 = await session.advanced.attachments.get(userId, "primary.dat");
259+
assertThat(attachment1.details.remoteParameters.identifier)
260+
.isEqualTo("S3-Primary");
261+
assertThat(attachment1.details.remoteParameters.flags)
262+
.isEqualTo("None");
263+
264+
const attachment2 = await session.advanced.attachments.get(userId, "secondary.dat");
265+
assertThat(attachment2.details.remoteParameters.identifier)
266+
.isEqualTo("S3-Secondary");
267+
assertThat(attachment2.details.remoteParameters.flags)
268+
.isEqualTo("None");
240269
}
241270
});
242271

@@ -324,7 +353,7 @@ interface User {
324353
await session.saveChanges();
325354
}
326355

327-
const scheduledTime = new Date(Date.now() + 3600000); // 1 hour from now
356+
const scheduledTime = addHours(new Date(), 1);
328357

329358
{
330359
const session = store.openSession();
@@ -350,6 +379,12 @@ interface User {
350379
.isNotNull();
351380
assertThat(attachment.details.name)
352381
.isEqualTo("scheduled.dat");
382+
assertThat(format(attachment.details.remoteParameters.at, DateUtil.DEFAULT_DATE_TZ_FORMAT))
383+
.isEqualTo(format(scheduledTime, DateUtil.DEFAULT_DATE_TZ_FORMAT));
384+
assertThat(attachment.details.remoteParameters.identifier)
385+
.isEqualTo(identifier);
386+
assertThat(attachment.details.remoteParameters.flags)
387+
.isEqualTo("None");
353388
}
354389
});
355390
});

‎test/Utils/AssertExtensions.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,9 @@ export class JavaAssertionBuilder {
163163
assert.ok(satisfy, "None of items satisfy condition");
164164
return this;
165165
}
166+
167+
public isUndefined() {
168+
assert.ok(this._value === undefined);
169+
return this;
170+
}
166171
}

0 commit comments

Comments
 (0)