Skip to content

Commit 77d695c

Browse files
committedAug 18, 2021
feat: Expose Last-Modified and ETag headers
1 parent 97c534b commit 77d695c

File tree

8 files changed

+92
-3
lines changed

8 files changed

+92
-3
lines changed
 

‎config/http/middleware/handlers/cors.json

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"options_credentials": true,
1818
"options_exposedHeaders": [
1919
"Accept-Patch",
20+
"ETag",
21+
"Last-Modified",
2022
"Link",
2123
"Location",
2224
"MS-Author-Via",

‎config/ldp/metadata-writer/default.json

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"files-scs:config/ldp/metadata-writer/writers/constant.json",
55
"files-scs:config/ldp/metadata-writer/writers/link-rel.json",
66
"files-scs:config/ldp/metadata-writer/writers/mapped.json",
7+
"files-scs:config/ldp/metadata-writer/writers/modified.json",
78
"files-scs:config/ldp/metadata-writer/writers/wac-allow.json",
89
"files-scs:config/ldp/metadata-writer/writers/www-auth.json"
910
],
@@ -15,6 +16,7 @@
1516
"handlers": [
1617
{ "@id": "urn:solid-server:default:MetadataWriter_Constant" },
1718
{ "@id": "urn:solid-server:default:MetadataWriter_Mapped" },
19+
{ "@id": "urn:solid-server:default:MetadataWriter_Modified" },
1820
{ "@id": "urn:solid-server:default:MetadataWriter_LinkRel" },
1921
{ "@id": "urn:solid-server:default:MetadataWriter_WacAllow" },
2022
{ "@id": "urn:solid-server:default:MetadataWriter_WwwAuth" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
3+
"@graph": [
4+
{
5+
"comment": "Adds the Last-Modified and ETag headers.",
6+
"@id": "urn:solid-server:default:MetadataWriter_Modified",
7+
"@type": "ModifiedMetadataWriter"
8+
}
9+
]
10+
}

‎src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export * from './ldp/http/metadata/LinkTypeParser';
9191
export * from './ldp/http/metadata/MappedMetadataWriter';
9292
export * from './ldp/http/metadata/MetadataParser';
9393
export * from './ldp/http/metadata/MetadataWriter';
94+
export * from './ldp/http/metadata/ModifiedMetadataWriter';
9495
export * from './ldp/http/metadata/SlugParser';
9596
export * from './ldp/http/metadata/WacAllowMetadataWriter';
9697
export * from './ldp/http/metadata/WwwAuthMetadataWriter';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { HttpResponse } from '../../../server/HttpResponse';
2+
import { getETag } from '../../../storage/Conditions';
3+
import { addHeader } from '../../../util/HeaderUtil';
4+
import { DC } from '../../../util/Vocabularies';
5+
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
6+
import { MetadataWriter } from './MetadataWriter';
7+
8+
/**
9+
* A {@link MetadataWriter} that generates all the necessary headers related to the modification date of a resource.
10+
*/
11+
export class ModifiedMetadataWriter extends MetadataWriter {
12+
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
13+
const modified = input.metadata.get(DC.terms.modified);
14+
if (modified) {
15+
const date = new Date(modified.value);
16+
addHeader(input.response, 'Last-Modified', date.toUTCString());
17+
}
18+
const etag = getETag(input.metadata);
19+
if (etag) {
20+
addHeader(input.response, 'ETag', etag);
21+
}
22+
}
23+
}

‎src/storage/Conditions.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
2+
import { DC } from '../util/Vocabularies';
23

34
/**
45
* The conditions of an HTTP conditional request.
@@ -7,11 +8,11 @@ export interface Conditions {
78
/**
89
* Valid if matching any of the given ETags.
910
*/
10-
matchesEtag: string[];
11+
matchesETag?: string[];
1112
/**
1213
* Valid if not matching any of the given ETags.
1314
*/
14-
notMatchesEtag: string[];
15+
notMatchesETag?: string[];
1516
/**
1617
* Valid if modified since the given date.
1718
*/
@@ -27,9 +28,23 @@ export interface Conditions {
2728
*/
2829
matchesMetadata: (metadata: RepresentationMetadata) => boolean;
2930
/**
30-
* Checks validity based on the given ETag and/org date.
31+
* Checks validity based on the given ETag and/or date.
3132
* @param eTag - Condition based on ETag.
3233
* @param lastModified - Condition based on last modified date.
3334
*/
3435
matches: (eTag?: string, lastModified?: Date) => boolean;
3536
}
37+
38+
/**
39+
* Generates an ETag based on the last modified date of a resource.
40+
* @param metadata - Metadata of the resource.
41+
*
42+
* @returns the generated ETag. Undefined if no last modified date was found.
43+
*/
44+
export function getETag(metadata: RepresentationMetadata): string | undefined {
45+
const modified = metadata.get(DC.terms.modified);
46+
if (modified) {
47+
const date = new Date(modified.value);
48+
return `"${date.getTime()}"`;
49+
}
50+
}

‎test/integration/Middleware.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ describe('An http server with middleware', (): void => {
9999
expect(exposed.split(/\s*,\s*/u)).toContain('Accept-Patch');
100100
});
101101

102+
it('exposes the Last-Modified and ETag headers via CORS.', async(): Promise<void> => {
103+
const res = await request(server).get('/').expect(200);
104+
const exposed = res.header['access-control-expose-headers'];
105+
expect(exposed.split(/\s*,\s*/u)).toContain('ETag');
106+
expect(exposed.split(/\s*,\s*/u)).toContain('Last-Modified');
107+
});
108+
102109
it('exposes the Link header via CORS.', async(): Promise<void> => {
103110
const res = await request(server).get('/').expect(200);
104111
const exposed = res.header['access-control-expose-headers'];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createResponse } from 'node-mocks-http';
2+
import { ModifiedMetadataWriter } from '../../../../../src/ldp/http/metadata/ModifiedMetadataWriter';
3+
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
4+
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
5+
import { updateModifiedDate } from '../../../../../src/util/ResourceUtil';
6+
import { DC } from '../../../../../src/util/Vocabularies';
7+
8+
describe('A ModifiedMetadataWriter', (): void => {
9+
const writer = new ModifiedMetadataWriter();
10+
11+
it('adds the Last-Modified and ETag header if there is dc:modified metadata.', async(): Promise<void> => {
12+
const response = createResponse() as HttpResponse;
13+
const metadata = new RepresentationMetadata();
14+
updateModifiedDate(metadata);
15+
const dateTime = metadata.get(DC.terms.modified)!.value;
16+
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
17+
expect(response.getHeaders()).toEqual({
18+
'last-modified': new Date(dateTime).toUTCString(),
19+
etag: `"${new Date(dateTime).getTime()}"`,
20+
});
21+
});
22+
23+
it('does nothing if there is no matching metadata.', async(): Promise<void> => {
24+
const response = createResponse() as HttpResponse;
25+
const metadata = new RepresentationMetadata();
26+
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
27+
expect(response.getHeaders()).toEqual({});
28+
});
29+
});

0 commit comments

Comments
 (0)
Please sign in to comment.