mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Support new ETag format in notification states
This commit is contained in:
parent
4ff6fe66ea
commit
b250beaec9
@ -1,3 +1,4 @@
|
|||||||
|
import { sameResourceState } from '../../storage/Conditions';
|
||||||
import type { NotificationGenerator } from './generate/NotificationGenerator';
|
import type { NotificationGenerator } from './generate/NotificationGenerator';
|
||||||
import type { NotificationEmitter } from './NotificationEmitter';
|
import type { NotificationEmitter } from './NotificationEmitter';
|
||||||
import type { NotificationHandlerInput } from './NotificationHandler';
|
import type { NotificationHandlerInput } from './NotificationHandler';
|
||||||
@ -37,7 +38,7 @@ export class ComposedNotificationHandler extends NotificationHandler {
|
|||||||
|
|
||||||
const { state } = input.channel;
|
const { state } = input.channel;
|
||||||
// In case the state matches there is no need to send the notification
|
// In case the state matches there is no need to send the notification
|
||||||
if (typeof state === 'string' && state === notification.state) {
|
if (typeof state === 'string' && notification.state && sameResourceState(state, notification.state)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
||||||
import { DC } from '../util/Vocabularies';
|
import { DC } from '../util/Vocabularies';
|
||||||
import { getETag, isCurrentETag } from './Conditions';
|
import { getETag, sameResourceState } from './Conditions';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
|
|
||||||
export interface BasicConditionsOptions {
|
export interface BasicConditionsOptions {
|
||||||
@ -39,19 +39,21 @@ export class BasicConditions implements Conditions {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to see if an ETag matches the provided metadata
|
const eTag = getETag(metadata);
|
||||||
// eslint-disable-next-line func-style
|
if (eTag) {
|
||||||
let eTagMatches = (tag: string): boolean => isCurrentETag(tag, metadata);
|
// Helper function to see if an ETag matches the provided metadata
|
||||||
if (strict) {
|
// eslint-disable-next-line func-style
|
||||||
const eTag = getETag(metadata);
|
let eTagMatches = (tag: string): boolean => sameResourceState(tag, eTag);
|
||||||
eTagMatches = (tag: string): boolean => tag === eTag;
|
if (strict) {
|
||||||
}
|
eTagMatches = (tag: string): boolean => tag === eTag;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.matchesETag && !this.matchesETag.includes('*') && !this.matchesETag.some(eTagMatches)) {
|
if (this.matchesETag && !this.matchesETag.includes('*') && !this.matchesETag.some(eTagMatches)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.notMatchesETag?.some(eTagMatches)) {
|
if (this.notMatchesETag?.some(eTagMatches)) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In practice, this will only be undefined on a backend
|
// In practice, this will only be undefined on a backend
|
||||||
|
@ -42,31 +42,21 @@ export interface Conditions {
|
|||||||
export function getETag(metadata: RepresentationMetadata): string | undefined {
|
export function getETag(metadata: RepresentationMetadata): string | undefined {
|
||||||
const modified = metadata.get(DC.terms.modified);
|
const modified = metadata.get(DC.terms.modified);
|
||||||
const { contentType } = metadata;
|
const { contentType } = metadata;
|
||||||
if (modified && contentType) {
|
if (modified) {
|
||||||
const date = new Date(modified.value);
|
const date = new Date(modified.value);
|
||||||
return `"${date.getTime()}-${contentType}"`;
|
// It is possible for the content type to be undefined,
|
||||||
|
// such as when only the metadata returned by a `DataAccessor` is used.
|
||||||
|
return `"${date.getTime()}-${contentType ?? ''}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates whether a given ETag corresponds to the current state of the resource,
|
* Validates whether 2 ETags correspond to the same state of a resource,
|
||||||
* independent of the representation the ETag corresponds to.
|
* independent of the representation the ETags correspond to.
|
||||||
* Assumes ETags are made with the {@link getETag} function.
|
* Assumes ETags are made with the {@link getETag} function.
|
||||||
* Since we base the ETag on the last modified date,
|
|
||||||
* we know the ETag still matches as long as that didn't change.
|
|
||||||
*
|
|
||||||
* @param eTag - ETag to validate.
|
|
||||||
* @param metadata - Metadata of the resource.
|
|
||||||
*
|
|
||||||
* @returns `true` if the ETag represents the current state of the resource.
|
|
||||||
*/
|
*/
|
||||||
export function isCurrentETag(eTag: string, metadata: RepresentationMetadata): boolean {
|
export function sameResourceState(eTag1: string, eTag2: string): boolean {
|
||||||
const modified = metadata.get(DC.terms.modified);
|
// Since we base the ETag on the last modified date,
|
||||||
if (!modified) {
|
// we know the ETags match as long as the date part is the same.
|
||||||
return false;
|
return eTag1.split('-')[0] === eTag2.split('-')[0];
|
||||||
}
|
|
||||||
const time = eTag.split('-', 1)[0];
|
|
||||||
const date = new Date(modified.value);
|
|
||||||
// `time` will still have the initial`"` of the ETag string
|
|
||||||
return time === `"${date.getTime()}`;
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ describe('A ComposedNotificationHandler', (): void => {
|
|||||||
type: 'Update',
|
type: 'Update',
|
||||||
object: 'http://example.com/foo',
|
object: 'http://example.com/foo',
|
||||||
published: '123',
|
published: '123',
|
||||||
state: '123',
|
state: '"123456-text/turtle"',
|
||||||
};
|
};
|
||||||
let channel: NotificationChannel;
|
let channel: NotificationChannel;
|
||||||
const representation = new BasicRepresentation();
|
const representation = new BasicRepresentation();
|
||||||
@ -66,8 +66,8 @@ describe('A ComposedNotificationHandler', (): void => {
|
|||||||
expect(emitter.handleSafe).toHaveBeenLastCalledWith({ channel, representation });
|
expect(emitter.handleSafe).toHaveBeenLastCalledWith({ channel, representation });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not emit the notification if its state matches the channel state.', async(): Promise<void> => {
|
it('does not emit the notification if it has the same resource state as the channel.', async(): Promise<void> => {
|
||||||
channel.state = notification.state;
|
channel.state = '"123456-application/ld+json"';
|
||||||
await expect(handler.handle({ channel, topic })).resolves.toBeUndefined();
|
await expect(handler.handle({ channel, topic })).resolves.toBeUndefined();
|
||||||
expect(generator.handle).toHaveBeenCalledTimes(1);
|
expect(generator.handle).toHaveBeenCalledTimes(1);
|
||||||
expect(generator.handle).toHaveBeenLastCalledWith({ channel, topic });
|
expect(generator.handle).toHaveBeenLastCalledWith({ channel, topic });
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
||||||
import { getETag, isCurrentETag } from '../../../src/storage/Conditions';
|
import { getETag, sameResourceState } from '../../../src/storage/Conditions';
|
||||||
import { CONTENT_TYPE, DC } from '../../../src/util/Vocabularies';
|
import { CONTENT_TYPE, DC } from '../../../src/util/Vocabularies';
|
||||||
|
|
||||||
describe('Conditions', (): void => {
|
describe('Conditions', (): void => {
|
||||||
@ -13,41 +13,32 @@ describe('Conditions', (): void => {
|
|||||||
expect(getETag(metadata)).toBe(`"${now.getTime()}-text/turtle"`);
|
expect(getETag(metadata)).toBe(`"${now.getTime()}-text/turtle"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined if no date or content-type was found.', async(): Promise<void> => {
|
it('creates a simpler ETag if no content type was found.', async(): Promise<void> => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
expect(getETag(new RepresentationMetadata({ [DC.modified]: now.toISOString() }))).toBe(`"${now.getTime()}-"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined if no date found.', async(): Promise<void> => {
|
||||||
expect(getETag(new RepresentationMetadata())).toBeUndefined();
|
expect(getETag(new RepresentationMetadata())).toBeUndefined();
|
||||||
expect(getETag(new RepresentationMetadata({ [DC.modified]: now.toISOString() }))).toBeUndefined();
|
|
||||||
expect(getETag(new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }))).toBeUndefined();
|
expect(getETag(new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }))).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#isCurrentETag', (): void => {
|
describe('sameResourceState', (): void => {
|
||||||
const now = new Date();
|
const eTag = '"123456-text/turtle"';
|
||||||
|
const eTagJson = '"123456-application/ld+json"';
|
||||||
|
const eTagWrongTime = '"654321-text/turtle"';
|
||||||
|
|
||||||
it('compares an ETag with the current resource state.', async(): Promise<void> => {
|
it('returns true if the ETags are the same.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata({
|
expect(sameResourceState(eTag, eTag)).toBe(true);
|
||||||
[DC.modified]: now.toISOString(),
|
|
||||||
[CONTENT_TYPE]: 'text/turtle',
|
|
||||||
});
|
|
||||||
const eTag = getETag(metadata)!;
|
|
||||||
expect(isCurrentETag(eTag, metadata)).toBe(true);
|
|
||||||
expect(isCurrentETag('"ETag"', metadata)).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores the content-type.', async(): Promise<void> => {
|
it('returns true if the ETags target the same timestamp.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata({
|
expect(sameResourceState(eTag, eTagJson)).toBe(true);
|
||||||
[DC.modified]: now.toISOString(),
|
|
||||||
[CONTENT_TYPE]: 'text/turtle',
|
|
||||||
});
|
|
||||||
const eTag = getETag(metadata)!;
|
|
||||||
metadata.contentType = 'application/ld+json';
|
|
||||||
expect(isCurrentETag(eTag, metadata)).toBe(true);
|
|
||||||
expect(isCurrentETag('"ETag"', metadata)).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if the metadata has no last modified date.', async(): Promise<void> => {
|
it('returns false if the timestamp differs.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata();
|
expect(sameResourceState(eTag, eTagWrongTime)).toBe(false);
|
||||||
expect(isCurrentETag('"ETag"', metadata)).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user