mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Use an IdentifierMap for ResourceStore responses
This commit is contained in:
@@ -30,7 +30,8 @@ and all the entries in `config/storage/backend`.
|
||||
This store emits the events that are necessary to emit notifications when resources change.
|
||||
|
||||
There are 4 different events that can be emitted:
|
||||
- `this.emit('changed', identifier, AS.Create | AS.Update | AS.Delete | undefined)`: is emitted for every resource that was changed/effected by a call to the store.
|
||||
- `this.emit('changed', identifier, activity)`: is emitted for every resource that was changed/effected by a call to the store.
|
||||
With activity being undefined or one of the available ActivityStream terms.
|
||||
- `this.emit(AS.Create, identifier)`: is emitted for every resource that was created by the call to the store.
|
||||
- `this.emit(AS.Update, identifier)`: is emitted for every resource that was updated by the call to the store.
|
||||
- `this.emit(AS.Delete, identifier)`: is emitted for every resource that was deleted by the call to the store.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { find } from '../../util/IterableUtil';
|
||||
import { AS, SOLID_AS } from '../../util/Vocabularies';
|
||||
import { CreatedResponseDescription } from '../output/response/CreatedResponseDescription';
|
||||
import type { ResponseDescription } from '../output/response/ResponseDescription';
|
||||
import { createResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import type { OperationHandlerInput } from './OperationHandler';
|
||||
import { OperationHandler } from './OperationHandler';
|
||||
|
||||
@@ -37,10 +38,12 @@ export class PostOperationHandler extends OperationHandler {
|
||||
this.logger.warn('POST requests require the Content-Type header to be set');
|
||||
throw new BadRequestHttpError('POST requests require the Content-Type header to be set');
|
||||
}
|
||||
const result = await this.store.addResource(operation.target, operation.body, operation.conditions);
|
||||
const createdIdentifier = Object.entries(result).find(
|
||||
([ , value ]): boolean => value.get(SOLID_AS.terms.Activity)?.value === AS.Create,
|
||||
)![0];
|
||||
return new CreatedResponseDescription(createResourceIdentifier(createdIdentifier));
|
||||
const changes = await this.store.addResource(operation.target, operation.body, operation.conditions);
|
||||
const createdIdentifier = find(changes.keys(), (identifier): boolean =>
|
||||
Boolean(changes.get(identifier)?.has(SOLID_AS.terms.Activity, AS.terms.Create)));
|
||||
if (!createdIdentifier) {
|
||||
throw new InternalServerError('Operation was successful but no created identifier was returned.');
|
||||
}
|
||||
return new CreatedResponseDescription(createdIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,3 @@ export interface ResourceIdentifier {
|
||||
export function isResourceIdentifier(object: any): object is ResourceIdentifier {
|
||||
return object && (typeof object.path === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function creating a {@link ResourceIdentifier} for convenience.
|
||||
*/
|
||||
export function createResourceIdentifier(resourcePath: string): ResourceIdentifier {
|
||||
return { path: resourcePath };
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
|
||||
import { PreconditionFailedHttpError } from '../util/errors/PreconditionFailedHttpError';
|
||||
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
|
||||
import { concat } from '../util/IterableUtil';
|
||||
import { IdentifierMap } from '../util/map/IdentifierMap';
|
||||
import {
|
||||
ensureTrailingSlash,
|
||||
isContainerIdentifier,
|
||||
@@ -266,24 +268,24 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
// Solid, §5.4: "When a contained resource is deleted,
|
||||
// the server MUST also delete the associated auxiliary resources"
|
||||
// https://solid.github.io/specification/protocol#deleting-resources
|
||||
const changes: ChangeMap = {};
|
||||
const changes: ChangeMap = new IdentifierMap();
|
||||
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||
const auxiliaries = this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier);
|
||||
for (const deletedId of await this.safelyDeleteAuxiliaryResources(auxiliaries)) {
|
||||
changes[deletedId.path] = this.createActivityMetadata(deletedId, AS.Delete);
|
||||
this.addActivityMetadata(changes, deletedId, AS.terms.Delete);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.identifierStrategy.isRootContainer(identifier)) {
|
||||
const container = this.identifierStrategy.getParentContainer(identifier);
|
||||
changes[container.path] = this.createActivityMetadata(container, AS.Update);
|
||||
this.addActivityMetadata(changes, container, AS.terms.Update);
|
||||
|
||||
// Update modified date of parent
|
||||
await this.updateContainerModifiedDate(container);
|
||||
}
|
||||
|
||||
await this.accessor.deleteResource(identifier);
|
||||
changes[identifier.path] = this.createActivityMetadata(identifier, AS.Delete);
|
||||
this.addActivityMetadata(changes, identifier, AS.terms.Delete);
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -385,18 +387,17 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
// Solid, §5.3: "Servers MUST create intermediate containers and include corresponding containment triples
|
||||
// in container representations derived from the URI path component of PUT and PATCH requests."
|
||||
// https://solid.github.io/specification/protocol#writing-resources
|
||||
let changes: ChangeMap = {};
|
||||
let changes: ChangeMap = new IdentifierMap();
|
||||
if (!this.identifierStrategy.isRootContainer(identifier) && !exists) {
|
||||
const parent = this.identifierStrategy.getParentContainer(identifier);
|
||||
if (!createContainers) {
|
||||
changes[parent.path] = this.createActivityMetadata(parent, AS.Update);
|
||||
} else {
|
||||
const createdContainers = await this.createRecursiveContainers(parent);
|
||||
changes = { ...changes, ...createdContainers };
|
||||
|
||||
if (Object.keys(createdContainers).length === 0) {
|
||||
changes[parent.path] = this.createActivityMetadata(parent, AS.Update);
|
||||
}
|
||||
if (createContainers) {
|
||||
changes = await this.createRecursiveContainers(parent);
|
||||
}
|
||||
|
||||
// No changes means the parent container exists and will be updated
|
||||
if (changes.size === 0) {
|
||||
this.addActivityMetadata(changes, parent, AS.terms.Update);
|
||||
}
|
||||
|
||||
// Parent container is also modified
|
||||
@@ -410,7 +411,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
this.accessor.writeContainer(identifier, representation.metadata) :
|
||||
this.accessor.writeDocument(identifier, representation.data, representation.metadata));
|
||||
|
||||
changes[identifier.path] = this.createActivityMetadata(identifier, exists ? AS.Update : AS.Create);
|
||||
this.addActivityMetadata(changes, identifier, exists ? AS.terms.Update : AS.terms.Create);
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -609,7 +610,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
// Verify whether the container already exists
|
||||
try {
|
||||
const metadata = await this.getNormalizedMetadata(container);
|
||||
// See #480
|
||||
// See https://github.com/CommunitySolidServer/CommunitySolidServer/issues/480
|
||||
// Solid, §3.1: "If two URIs differ only in the trailing slash, and the server has associated a resource with
|
||||
// one of them, then the other URI MUST NOT correspond to another resource. Instead, the server MAY respond to
|
||||
// requests for the latter URI with a 301 redirect to the former."
|
||||
@@ -617,7 +618,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
if (!isContainerPath(metadata.identifier.value)) {
|
||||
throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`);
|
||||
}
|
||||
return {};
|
||||
return new IdentifierMap();
|
||||
} catch (error: unknown) {
|
||||
if (!NotFoundHttpError.isInstance(error)) {
|
||||
throw error;
|
||||
@@ -625,15 +626,21 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
}
|
||||
|
||||
// Create the container, starting with its parent
|
||||
const ancestors = this.identifierStrategy.isRootContainer(container) ?
|
||||
{} :
|
||||
const ancestors: ChangeMap = this.identifierStrategy.isRootContainer(container) ?
|
||||
new IdentifierMap() :
|
||||
await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
|
||||
const changes = await this.writeData(container, new BasicRepresentation([], container), true, false, false);
|
||||
|
||||
return { ...changes, ...ancestors };
|
||||
return new IdentifierMap(concat([ changes, ancestors ]));
|
||||
}
|
||||
|
||||
private createActivityMetadata(id: ResourceIdentifier, activity: string): RepresentationMetadata {
|
||||
return new RepresentationMetadata(id, { [SOLID_AS.terms.Activity.value]: activity });
|
||||
/**
|
||||
* Generates activity metadata for a resource and adds it to the {@link ChangeMap}
|
||||
* @param map - ChangeMap to update.
|
||||
* @param id - Identifier of the resource being changed.
|
||||
* @param activity - Which activity is taking place.
|
||||
*/
|
||||
private addActivityMetadata(map: ChangeMap, id: ResourceIdentifier, activity: NamedNode): void {
|
||||
map.set(id, new RepresentationMetadata(id, { [SOLID_AS.Activity]: activity }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import { AS, SOLID_AS } from '../util/Vocabularies';
|
||||
import type { Conditions } from './Conditions';
|
||||
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||
|
||||
// The ActivityStream terms for which we emit an event
|
||||
const emittedActivities: Set<string> = new Set([ AS.Create, AS.Delete, AS.Update ]);
|
||||
|
||||
/**
|
||||
* Store that notifies listeners of changes to its source
|
||||
* by emitting a `changed` event.
|
||||
@@ -50,11 +53,11 @@ export class MonitoringStore<T extends ResourceStore = ResourceStore>
|
||||
}
|
||||
|
||||
private emitChanged(changes: ChangeMap): ChangeMap {
|
||||
for (const [ key, value ] of Object.entries(changes)) {
|
||||
const activity = value.get(SOLID_AS.terms.Activity)?.value;
|
||||
this.emit('changed', { path: key }, activity);
|
||||
if (activity && [ AS.Create, AS.Delete, AS.Update ].includes(activity)) {
|
||||
this.emit(activity, { path: key });
|
||||
for (const [ identifier, metadata ] of changes) {
|
||||
const activity = metadata.get(SOLID_AS.terms.Activity);
|
||||
this.emit('changed', identifier, activity);
|
||||
if (activity && emittedActivities.has(activity.value)) {
|
||||
this.emit(activity.value, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,16 @@ import type { Representation } from '../http/representation/Representation';
|
||||
import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
||||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||
import type { IdentifierMap } from '../util/map/IdentifierMap';
|
||||
import type { Conditions } from './Conditions';
|
||||
import type { ResourceSet } from './ResourceSet';
|
||||
|
||||
/**
|
||||
* An object containing one property for each resource that was created, updated or deleted
|
||||
* by this operation. Where the key of the property is the path of the resource (string) and the value is an
|
||||
* instance of RepresentationMetadata containing extra information about the change of the resource.
|
||||
* An {@link IdentifierMap} containing one entry for each resource that was created, updated or deleted
|
||||
* by this operation. Where the value is a {@link RepresentationMetadata}
|
||||
* containing extra information about the change of the resource.
|
||||
*/
|
||||
export type ChangeMap = Record<string, RepresentationMetadata>;
|
||||
export type ChangeMap = IdentifierMap<RepresentationMetadata>;
|
||||
|
||||
/**
|
||||
* A ResourceStore represents a collection of resources.
|
||||
|
||||
@@ -4,33 +4,29 @@ import { BasicRepresentation } from '../../../../src/http/representation/BasicRe
|
||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||
import type { ResourceStore, ChangeMap } from '../../../../src/storage/ResourceStore';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
import { IdentifierMap } from '../../../../src/util/map/IdentifierMap';
|
||||
import { AS, SOLID_AS, SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A PostOperationHandler', (): void => {
|
||||
let operation: Operation;
|
||||
let body: Representation;
|
||||
const conditions = new BasicConditions({});
|
||||
let store: ResourceStore;
|
||||
let store: jest.Mocked<ResourceStore>;
|
||||
let handler: PostOperationHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
body = new BasicRepresentation('', 'text/turtle');
|
||||
operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||
store = {
|
||||
addResource: jest.fn(async(): Promise<ChangeMap> => ({
|
||||
'https://example.com/parent/newPath': new RepresentationMetadata(
|
||||
{ path: 'https://example.com/parent/newPath' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Create },
|
||||
),
|
||||
'https://example.com/parent/': new RepresentationMetadata(
|
||||
{ path: 'https://example.come/parent/' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Update },
|
||||
),
|
||||
})),
|
||||
} as unknown as ResourceStore;
|
||||
addResource: jest.fn().mockResolvedValue(new IdentifierMap([
|
||||
[{ path: 'https://example.com/parent/newPath' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Create }) ],
|
||||
[{ path: 'https://example.com/parent/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ],
|
||||
])),
|
||||
} as any;
|
||||
handler = new PostOperationHandler(store);
|
||||
});
|
||||
|
||||
@@ -54,4 +50,11 @@ describe('A PostOperationHandler', (): void => {
|
||||
expect(store.addResource).toHaveBeenCalledTimes(1);
|
||||
expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||
});
|
||||
|
||||
it('errors if the store returns no created identifier.', async(): Promise<void> => {
|
||||
store.addResource.mockResolvedValueOnce(new IdentifierMap([
|
||||
[{ path: 'https://example.com/parent/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ],
|
||||
]));
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(InternalServerError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,21 +266,17 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.removeAll(RDF.terms.type);
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
|
||||
expect(Object.keys(result)).toEqual([
|
||||
root,
|
||||
expect.stringMatching(new RegExp(`^${root}[^/]+$`, 'u')),
|
||||
]);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
|
||||
expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
|
||||
const generatedID = Object.keys(result).find((key): boolean => key !== root)!;
|
||||
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||
expect(generatedID).toBeDefined();
|
||||
expect(generatedID).toMatch(new RegExp(`^${root}[^/]+$`, 'u'));
|
||||
expect(generatedID.path).toMatch(new RegExp(`^${root}[^/]+$`, 'u'));
|
||||
|
||||
await expect(arrayifyStream(accessor.data[generatedID].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[generatedID]).toBeTruthy();
|
||||
expect(accessor.data[generatedID].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(result[generatedID].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
expect(accessor.data[generatedID.path]).toBeDefined();
|
||||
await expect(arrayifyStream(accessor.data[generatedID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[generatedID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(result.get(generatedID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
it('can write containers.', async(): Promise<void> => {
|
||||
@@ -290,24 +286,21 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.data = guardedStreamFrom([ '<> a <http://test.com/coolContainer>.' ]);
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
|
||||
expect(Object.keys(result)).toEqual([
|
||||
root,
|
||||
expect.stringMatching(new RegExp(`^${root}[^/]+?/$`, 'u')),
|
||||
]);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
|
||||
expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
|
||||
const generatedID = Object.keys(result).find((key): boolean => key !== root)!;
|
||||
const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!;
|
||||
expect(generatedID).toBeDefined();
|
||||
expect(generatedID).toMatch(new RegExp(`^${root}[^/]+?/$`, 'u'));
|
||||
expect(accessor.data[generatedID]).toBeTruthy();
|
||||
expect(accessor.data[generatedID].metadata.contentType).toBeUndefined();
|
||||
expect(result[generatedID].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
expect(generatedID.path).toMatch(new RegExp(`^${root}[^/]+?/$`, 'u'));
|
||||
|
||||
const { data, metadata } = await store.getRepresentation({ path: generatedID });
|
||||
expect(accessor.data[generatedID.path]).toBeDefined();
|
||||
expect(accessor.data[generatedID.path].metadata.contentType).toBeUndefined();
|
||||
expect(result.get(generatedID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
|
||||
const { data, metadata } = await store.getRepresentation(generatedID);
|
||||
const quads = await arrayifyStream<Quad>(data);
|
||||
expect(metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(quads.some((entry): boolean => entry.subject.value === generatedID &&
|
||||
expect(quads.some((entry): boolean => entry.subject.value === generatedID.path &&
|
||||
entry.object.value === 'http://test.com/coolContainer')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -316,13 +309,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.removeAll(RDF.terms.type);
|
||||
representation.metadata.add(SOLID_HTTP.terms.slug, 'newName');
|
||||
|
||||
const result = store.addResource(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[`${root}newName`]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[`${root}newName`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: `${root}newName` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
it('errors on a slug ending on / without Link rel:type Container header.', async(): Promise<void> => {
|
||||
@@ -337,8 +327,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
.toThrow('Only slugs used to create containers can end with a `/`.');
|
||||
});
|
||||
|
||||
it('creates a URI when the incoming slug does not end with /, ' +
|
||||
'but has a Link rel:type Container header.', async(): Promise<void> => {
|
||||
it('adds a / at the end if the request metadata contains rdf:type ldp:Container.', async(): Promise<void> => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.removeAll(RDF.terms.type);
|
||||
representation.metadata.add(RDF.terms.type, LDP.terms.Container);
|
||||
@@ -346,12 +335,9 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.data = guardedStreamFrom([ `` ]);
|
||||
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result).toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[`${root}newContainer/`]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect(result[`${root}newContainer/`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: `${root}newContainer/` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
it('generates a new URI if adding the slug would create an existing URI.', async(): Promise<void> => {
|
||||
@@ -372,13 +358,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.removeAll(RDF.terms.type);
|
||||
representation.metadata.add(SOLID_HTTP.terms.slug, '&%26');
|
||||
|
||||
const result = store.addResource(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[`${root}%26%26`]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[`${root}%26%26`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: `${root}%26%26` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
});
|
||||
|
||||
it('errors if the slug contains a slash.', async(): Promise<void> => {
|
||||
@@ -442,11 +425,9 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.contentType = 'text/turtle';
|
||||
representation.data = guardedStreamFrom([ `<${root}> a <coolContainer>.` ]);
|
||||
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
expect(mock).toHaveBeenLastCalledWith(resourceID);
|
||||
|
||||
@@ -475,13 +456,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
it('can write resources.', async(): Promise<void> => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -495,13 +473,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.removeAll(RDF.terms.type);
|
||||
representation.metadata.contentType = 'text/turtle';
|
||||
representation.data = guardedStreamFrom([ `<${root}resource/> a <coolContainer>.` ]);
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -509,15 +484,12 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
expect(accessor.data[root].metadata.get(GENERATED_PREDICATE)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can overwrite resources which does not update parent metadata.', async(): Promise<void> => {
|
||||
it('can overwrite resources that do not update parent metadata.', async(): Promise<void> => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -525,11 +497,9 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
// Parent metadata does not get updated if the resource already exists
|
||||
representation = new BasicRepresentation('updatedText', 'text/plain');
|
||||
mockDate.mockReturnValue(later);
|
||||
const result2 = store.setRepresentation(resourceID, representation);
|
||||
await expect(result2).resolves.toEqual({
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result2)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
const result2 = await store.setRepresentation(resourceID, representation);
|
||||
expect(result2.size).toBe(1);
|
||||
expect(result2.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ 'updatedText' ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(later.toISOString());
|
||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
@@ -540,13 +510,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
representation.metadata.add(namedNode('notGen'), 'value');
|
||||
representation.metadata.add(namedNode('gen'), 'value', SOLID_META.terms.ResponseMetadata);
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[resourceID.path].metadata.get(namedNode('notGen'))?.value).toBe('value');
|
||||
expect(accessor.data[resourceID.path].metadata.get(namedNode('gen'))).toBeUndefined();
|
||||
@@ -556,13 +523,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete accessor.data[root];
|
||||
const resourceID = { path: `${root}resource` };
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
});
|
||||
|
||||
@@ -575,13 +539,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.data = guardedStreamFrom(
|
||||
[ quad(namedNode(`${root}resource/`), namedNode('a'), namedNode('coolContainer')) ],
|
||||
);
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||
});
|
||||
@@ -601,17 +562,12 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
it('creates recursive containers when needed.', async(): Promise<void> => {
|
||||
const resourceID = { path: `${root}a/b/resource` };
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[`${root}`]: expect.any(RepresentationMetadata),
|
||||
[`${root}a/`]: expect.any(RepresentationMetadata),
|
||||
[`${root}a/b/`]: expect.any(RepresentationMetadata),
|
||||
[`${root}a/b/resource`]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[`${root}`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[`${root}a/`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
expect((await result)[`${root}a/b/`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
expect((await result)[`${root}a/b/resource`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(4);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get({ path: `${root}a/` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(result.get({ path: `${root}a/b/` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(result.get({ path: `${root}a/b/resource` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||
expect(accessor.data[`${root}a/`].metadata.getAll(RDF.terms.type).map((type): string => type.value))
|
||||
.toContain(LDP.Container);
|
||||
@@ -636,11 +592,9 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
representation.metadata.removeAll(RDF.terms.type);
|
||||
representation.metadata.contentType = 'text/turtle';
|
||||
representation.data = guardedStreamFrom([]);
|
||||
const result = store.setRepresentation(resourceID, representation);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||
const result = await store.setRepresentation(resourceID, representation);
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create);
|
||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||
expect(Object.keys(accessor.data)).toHaveLength(1);
|
||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||
@@ -725,10 +679,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
it('will delete resources.', async(): Promise<void> => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
accessor.data[resourceID.path] = representation;
|
||||
await expect(store.deleteResource(resourceID)).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
const result = await store.deleteResource(resourceID);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||
expect(accessor.data[root].metadata.get(GENERATED_PREDICATE)).toBeUndefined();
|
||||
@@ -736,9 +690,9 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
it('will delete root non-storage containers.', async(): Promise<void> => {
|
||||
accessor.data[root] = new BasicRepresentation(representation.data, containerMetadata);
|
||||
await expect(store.deleteResource({ path: root })).resolves.toEqual(
|
||||
{ [root]: expect.any(RepresentationMetadata) },
|
||||
);
|
||||
const result = await store.deleteResource({ path: root });
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[root]).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -749,15 +703,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
accessor.data[resourceID.path] = new BasicRepresentation(representation.data, storageMetadata);
|
||||
accessor.data[auxResourceID.path] = representation;
|
||||
auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
|
||||
const result = store.deleteResource(auxResourceID);
|
||||
await expect(result).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
[auxResourceID.path]: expect.any(RepresentationMetadata),
|
||||
}),
|
||||
);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[auxResourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Delete);
|
||||
const result = await store.deleteResource(auxResourceID);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(auxResourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -767,17 +716,11 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
accessor.data[resourceID.path] = representation;
|
||||
accessor.data[auxResourceID.path] = representation;
|
||||
|
||||
const result = store.deleteResource(resourceID);
|
||||
await expect(result).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
[auxResourceID.path]: expect.any(RepresentationMetadata),
|
||||
}),
|
||||
);
|
||||
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||
expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Delete);
|
||||
expect((await result)[auxResourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Delete);
|
||||
const result = await store.deleteResource(resourceID);
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete);
|
||||
expect(result.get(auxResourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
||||
});
|
||||
@@ -796,12 +739,10 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
});
|
||||
const { logger } = store as any;
|
||||
logger.error = jest.fn();
|
||||
const result = store.deleteResource(resourceID);
|
||||
expect(Object.keys(await result)).toHaveLength(2);
|
||||
await expect(result).resolves.toEqual({
|
||||
[root]: expect.any(RepresentationMetadata),
|
||||
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||
});
|
||||
const result = await store.deleteResource(resourceID);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update);
|
||||
expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete);
|
||||
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||
expect(accessor.data[auxResourceID.path]).toBeDefined();
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { Patch } from '../../../src/http/representation/Patch';
|
||||
import type { Representation } from '../../../src/http/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
||||
import { MonitoringStore } from '../../../src/storage/MonitoringStore';
|
||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||
import type { ChangeMap, ResourceStore } from '../../../src/storage/ResourceStore';
|
||||
import { IdentifierMap } from '../../../src/util/map/IdentifierMap';
|
||||
import { AS, SOLID_AS } from '../../../src/util/Vocabularies';
|
||||
|
||||
describe('A MonitoringStore', (): void => {
|
||||
@@ -14,55 +15,31 @@ describe('A MonitoringStore', (): void => {
|
||||
let updatedCallback: () => void;
|
||||
let deletedCallback: () => void;
|
||||
|
||||
const addResourceReturnMock = {
|
||||
'http://example.org/foo/bar/new': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/new' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Create },
|
||||
),
|
||||
'http://example.org/foo/bar/': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Update },
|
||||
),
|
||||
};
|
||||
const setRepresentationReturnMock = {
|
||||
'http://example.org/foo/bar/new': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/new' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Update },
|
||||
),
|
||||
};
|
||||
const deleteResourceReturnMock = {
|
||||
'http://example.org/foo/bar/new': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/new' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Delete },
|
||||
),
|
||||
'http://example.org/foo/bar/': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Update },
|
||||
),
|
||||
};
|
||||
const modifyResourceReturnMock = {
|
||||
'http://example.org/foo/bar/old': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/new' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Delete },
|
||||
),
|
||||
'http://example.org/foo/bar/new': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/new' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Create },
|
||||
),
|
||||
'http://example.org/foo/bar/': new RepresentationMetadata(
|
||||
{ path: 'http://example.org/foo/bar/' },
|
||||
{ [SOLID_AS.terms.Activity.value]: AS.Update },
|
||||
),
|
||||
};
|
||||
const addResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Create }) ],
|
||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
const setRepresentationReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
const deleteResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Delete }) ],
|
||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
const modifyResourceReturnMock: ChangeMap = new IdentifierMap([
|
||||
[{ path: 'http://example.org/foo/bar/old' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Delete }) ],
|
||||
[{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Create }) ],
|
||||
[{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ],
|
||||
]);
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
source = {
|
||||
getRepresentation: jest.fn(async(): Promise<any> => ({ success: true })),
|
||||
addResource: jest.fn(async(): Promise<any> => addResourceReturnMock),
|
||||
setRepresentation: jest.fn(async(): Promise<any> => setRepresentationReturnMock),
|
||||
deleteResource: jest.fn(async(): Promise<any> => deleteResourceReturnMock),
|
||||
modifyResource: jest.fn(async(): Promise<any> => modifyResourceReturnMock),
|
||||
hasResource: jest.fn(async(): Promise<any> => undefined),
|
||||
getRepresentation: jest.fn().mockResolvedValue({ success: true }),
|
||||
addResource: jest.fn().mockResolvedValue(addResourceReturnMock),
|
||||
setRepresentation: jest.fn().mockResolvedValue(setRepresentationReturnMock),
|
||||
deleteResource: jest.fn().mockResolvedValue(deleteResourceReturnMock),
|
||||
modifyResource: jest.fn().mockResolvedValue(modifyResourceReturnMock),
|
||||
hasResource: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
store = new MonitoringStore(source);
|
||||
|
||||
@@ -76,13 +53,6 @@ describe('A MonitoringStore', (): void => {
|
||||
store.on(AS.Delete, deletedCallback);
|
||||
});
|
||||
|
||||
afterEach(async(): Promise<void> => {
|
||||
store.removeListener('changed', changedCallback);
|
||||
store.removeListener(AS.Create, createdCallback);
|
||||
store.removeListener(AS.Update, updatedCallback);
|
||||
store.removeListener(AS.Delete, deletedCallback);
|
||||
});
|
||||
|
||||
it('calls getRepresentation directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.getRepresentation({ path: 'getPath' }, {})).resolves.toEqual({ success: true });
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
@@ -96,10 +66,8 @@ describe('A MonitoringStore', (): void => {
|
||||
});
|
||||
|
||||
it('calls addResource directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation)).resolves.toEqual({
|
||||
'http://example.org/foo/bar/new': expect.any(RepresentationMetadata),
|
||||
'http://example.org/foo/bar/': expect.any(RepresentationMetadata),
|
||||
});
|
||||
await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation))
|
||||
.resolves.toBe(addResourceReturnMock);
|
||||
expect(source.addResource).toHaveBeenCalledTimes(1);
|
||||
expect(source.addResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||
});
|
||||
@@ -109,8 +77,8 @@ describe('A MonitoringStore', (): void => {
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Create);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create);
|
||||
expect(createdCallback).toHaveBeenCalledTimes(1);
|
||||
expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
@@ -130,7 +98,7 @@ describe('A MonitoringStore', (): void => {
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Update);
|
||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
@@ -149,8 +117,8 @@ describe('A MonitoringStore', (): void => {
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Delete);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Delete);
|
||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||
@@ -170,9 +138,9 @@ describe('A MonitoringStore', (): void => {
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
await result;
|
||||
expect(changedCallback).toHaveBeenCalledTimes(3);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Create);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.Delete);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update);
|
||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.terms.Delete);
|
||||
expect(createdCallback).toHaveBeenCalledTimes(1);
|
||||
expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
@@ -182,18 +150,15 @@ describe('A MonitoringStore', (): void => {
|
||||
});
|
||||
|
||||
it('calls hasResource directly from the source.', async(): Promise<void> => {
|
||||
await expect(store.hasResource({ path: 'http://example.org/foo/bar' })).resolves.toBeUndefined();
|
||||
await expect(store.hasResource({ path: 'http://example.org/foo/bar' })).resolves.toBe(true);
|
||||
expect(source.hasResource).toHaveBeenCalledTimes(1);
|
||||
expect(source.hasResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' });
|
||||
});
|
||||
|
||||
it('should not emit an extra event when the Activity is not a valid AS value.', async(): Promise<void> => {
|
||||
source.addResource = jest.fn(async(): Promise<any> => ({
|
||||
'http://example.com/path': new RepresentationMetadata(
|
||||
{ path: 'http://example.com/path' },
|
||||
{ [SOLID_AS.terms.Activity.value]: 'SomethingRandom' },
|
||||
),
|
||||
}));
|
||||
source.addResource = jest.fn().mockResolvedValue(new IdentifierMap([
|
||||
[{ path: 'http://example.org/path' }, new RepresentationMetadata({ [SOLID_AS.Activity]: 'SomethingRandom' }) ],
|
||||
]));
|
||||
|
||||
await store.addResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user