mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Rework ResourceStore to return extra info
* feat: change return types in ResourceStore.ts * feat: change return types in BaseResourceStore.ts * feat: change return types in LockingResourceStore.ts * feat: change return types in RoutingResourceStore.ts * feat: change return types in MonitoringStore.ts * feat: change return types in PassthroughStore.ts * feat: change return types in ReadOnlyStore.ts * feat: change return types in PatchHandler.ts * feat: change return types in PatchingStore.ts * feat: change return types in RepresentationPatchHandler.ts * feat: create createResourceIdentifier() function for convenience * feat: adapt PostOperationHandler.ts to new typing * feat: change return types in RepresentationConvertingStore.ts * feat: adapt DataAccessorBasedStore.ts implementation to new typings * feat: adapt UnsecureWebSocketsProtocol.ts to new typing * chore: add temporary comments * fix: return correct Location header on POST request with slug * fix: npm run lint command needs more packages * fix: linting errors * chore: revert ed9952b * test: adapt PostOperationHandler tests * test: adapt UnsecureWebSocketsProtocol tests * test: adapt DataAccessorBasedStore tests * fix: linting errors * feat: emit specific created, deleted, updated events in MonitoringStore * test: adapt RepresentationPatchHandler tests * fix: revert UnsecureWebSocketsProtocol changes * feat: emit extra parameter on changed * test: adapt MonitoringStore tests * fix: linting errors * test: add test to MonitorStore.test for coverage * fix: linting error * chore: update doc in ResourceStore.ts * test: improve MonitoringStore tests * chore: update RELEASE_NOTES.md * chore: add extra info about the MonitoringStore to documentation/resource-store.md * chore: Update RELEASE_NOTES.md Co-authored-by: Anton Wiklund <ixuz07@gmail.com> * chore: Update documentation/resource-store.md Co-authored-by: Anton Wiklund <ixuz07@gmail.com> * chore: very small changes * chore: simplify metadata creation * fix: DataAccessorBasedStore improvement and bugfix * chore: improve resource-store.md * chore: adapt MonitoringStore event names, update docs and apply code suggestion * chore: use ResourceStoreResponse type * fix: typo * chore: rename ResourceStoreResponse type to ChangeMap * chore: adapt .gitignore to name change Co-authored-by: Anton Wiklund <ixuz07@gmail.com>
This commit is contained in:
parent
1c65b06392
commit
e0954cf2a7
@ -3,6 +3,7 @@
|
|||||||
"BaseHttpError",
|
"BaseHttpError",
|
||||||
"BasicConditions",
|
"BasicConditions",
|
||||||
"BasicRepresentation",
|
"BasicRepresentation",
|
||||||
|
"ChangeMap",
|
||||||
"CredentialSet",
|
"CredentialSet",
|
||||||
"Dict",
|
"Dict",
|
||||||
"Error",
|
"Error",
|
||||||
|
@ -65,6 +65,7 @@ These changes are relevant if you wrote custom modules for the server that depen
|
|||||||
* Changed type for `finalizer` attribute in `App` from `Finalizable` to `Finalizer` and updated the calling code in `App.stop()`.
|
* Changed type for `finalizer` attribute in `App` from `Finalizable` to `Finalizer` and updated the calling code in `App.stop()`.
|
||||||
* Removed the now obsolete `ParallelFinalizer` util class.
|
* Removed the now obsolete `ParallelFinalizer` util class.
|
||||||
- Added a lock cleanup on initialize for lock implementations `RedisLocker` and `FileSystemResourceLocker`.
|
- Added a lock cleanup on initialize for lock implementations `RedisLocker` and `FileSystemResourceLocker`.
|
||||||
|
- `ResourceStore` functions that change a resource now return metadata for every changed resource.
|
||||||
|
|
||||||
A new interface `SingleThreaded` has been added. This empty interface can be implemented to mark a component as not-threadsafe. When the CSS starts in multithreaded mode, it will error and halt if any SingleThreaded components are instantiated.
|
A new interface `SingleThreaded` has been added. This empty interface can be implemented to mark a component as not-threadsafe. When the CSS starts in multithreaded mode, it will error and halt if any SingleThreaded components are instantiated.
|
||||||
|
|
||||||
|
@ -29,6 +29,16 @@ and all the entries in `config/storage/backend`.
|
|||||||
## MonitoringStore
|
## MonitoringStore
|
||||||
This store emits the events that are necessary to emit notifications when resources change.
|
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(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.
|
||||||
|
|
||||||
|
A `changed` event will always be emitted if a resource was changed.
|
||||||
|
If the correct metadata was set by the source `ResourceStore`, an additional field will be sent along indicating the type of change,
|
||||||
|
and an additional corresponding event will be emitted, depending on what the change is.
|
||||||
|
|
||||||
## IndexRepresentationStore
|
## IndexRepresentationStore
|
||||||
When doing a GET request on a container `/container/`,
|
When doing a GET request on a container `/container/`,
|
||||||
this container returns the contents of `/container/index.html` instead if HTML is the preferred response type.
|
this container returns the contents of `/container/index.html` instead if HTML is the preferred response type.
|
||||||
|
@ -2,8 +2,10 @@ import { getLoggerFor } from '../../logging/LogUtil';
|
|||||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
|
import { AS, SOLID_AS } from '../../util/Vocabularies';
|
||||||
import { CreatedResponseDescription } from '../output/response/CreatedResponseDescription';
|
import { CreatedResponseDescription } from '../output/response/CreatedResponseDescription';
|
||||||
import type { ResponseDescription } from '../output/response/ResponseDescription';
|
import type { ResponseDescription } from '../output/response/ResponseDescription';
|
||||||
|
import { createResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||||
import type { OperationHandlerInput } from './OperationHandler';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
@ -35,7 +37,10 @@ export class PostOperationHandler extends OperationHandler {
|
|||||||
this.logger.warn('POST requests require the Content-Type header to be set');
|
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');
|
throw new BadRequestHttpError('POST requests require the Content-Type header to be set');
|
||||||
}
|
}
|
||||||
const identifier = await this.store.addResource(operation.target, operation.body, operation.conditions);
|
const result = await this.store.addResource(operation.target, operation.body, operation.conditions);
|
||||||
return new CreatedResponseDescription(identifier);
|
const createdIdentifier = Object.entries(result).find(
|
||||||
|
([ , value ]): boolean => value.get(SOLID_AS.terms.Activity)?.value === AS.Create,
|
||||||
|
)![0];
|
||||||
|
return new CreatedResponseDescription(createResourceIdentifier(createdIdentifier));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,15 @@ export interface ResourceIdentifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the object is a `ResourceIdentifier`.
|
* Determines whether the object is a {@link ResourceIdentifier}.
|
||||||
*/
|
*/
|
||||||
export function isResourceIdentifier(object: any): object is ResourceIdentifier {
|
export function isResourceIdentifier(object: any): object is ResourceIdentifier {
|
||||||
return object && (typeof object.path === 'string');
|
return object && (typeof object.path === 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function creating a {@link ResourceIdentifier} for convenience.
|
||||||
|
*/
|
||||||
|
export function createResourceIdentifier(resourcePath: string): ResourceIdentifier {
|
||||||
|
return { path: resourcePath };
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import type { RepresentationPreferences } from '../http/representation/Represent
|
|||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||||
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base implementation of ResourceStore for implementers of custom stores.
|
* Base implementation of ResourceStore for implementers of custom stores.
|
||||||
@ -21,22 +21,22 @@ export class BaseResourceStore implements ResourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new NotImplementedHttpError();
|
throw new NotImplementedHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation,
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new NotImplementedHttpError();
|
throw new NotImplementedHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier,
|
public async deleteResource(identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new NotImplementedHttpError();
|
throw new NotImplementedHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new NotImplementedHttpError();
|
throw new NotImplementedHttpError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import type { AuxiliaryStrategy } from '../http/auxiliary/AuxiliaryStrategy';
|
|||||||
import { BasicRepresentation } from '../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../http/representation/BasicRepresentation';
|
||||||
import type { Patch } from '../http/representation/Patch';
|
import type { Patch } from '../http/representation/Patch';
|
||||||
import type { Representation } from '../http/representation/Representation';
|
import type { Representation } from '../http/representation/Representation';
|
||||||
import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import { INTERNAL_QUADS } from '../util/ContentTypes';
|
import { INTERNAL_QUADS } from '../util/ContentTypes';
|
||||||
@ -39,10 +39,12 @@ import {
|
|||||||
SOLID_META,
|
SOLID_META,
|
||||||
PREFERRED_PREFIX_TERM,
|
PREFERRED_PREFIX_TERM,
|
||||||
CONTENT_TYPE_TERM,
|
CONTENT_TYPE_TERM,
|
||||||
|
SOLID_AS,
|
||||||
|
AS,
|
||||||
} from '../util/Vocabularies';
|
} from '../util/Vocabularies';
|
||||||
import type { DataAccessor } from './accessors/DataAccessor';
|
import type { DataAccessor } from './accessors/DataAccessor';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResourceStore which uses a DataAccessor for backend access.
|
* ResourceStore which uses a DataAccessor for backend access.
|
||||||
@ -138,7 +140,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation, conditions?: Conditions):
|
public async addResource(container: ResourceIdentifier, representation: Representation, conditions?: Conditions):
|
||||||
Promise<ResourceIdentifier> {
|
Promise<ChangeMap> {
|
||||||
this.validateIdentifier(container);
|
this.validateIdentifier(container);
|
||||||
|
|
||||||
const parentMetadata = await this.getSafeNormalizedMetadata(container);
|
const parentMetadata = await this.getSafeNormalizedMetadata(container);
|
||||||
@ -174,13 +176,11 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the data. New containers should never be made for a POST request.
|
// Write the data. New containers should never be made for a POST request.
|
||||||
await this.writeData(newID, representation, isContainer, false, false);
|
return this.writeData(newID, representation, isContainer, false, false);
|
||||||
|
|
||||||
return newID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
this.validateIdentifier(identifier);
|
this.validateIdentifier(identifier);
|
||||||
|
|
||||||
// Check if the resource already exists
|
// Check if the resource already exists
|
||||||
@ -216,7 +216,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<never> {
|
||||||
if (conditions) {
|
if (conditions) {
|
||||||
let metadata: RepresentationMetadata | undefined;
|
let metadata: RepresentationMetadata | undefined;
|
||||||
try {
|
try {
|
||||||
@ -233,7 +233,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
throw new NotImplementedHttpError('Patches are not supported by the default store.');
|
throw new NotImplementedHttpError('Patches are not supported by the default store.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<ChangeMap> {
|
||||||
this.validateIdentifier(identifier);
|
this.validateIdentifier(identifier);
|
||||||
const metadata = await this.accessor.getMetadata(identifier);
|
const metadata = await this.accessor.getMetadata(identifier);
|
||||||
// Solid, §5.4: "When a DELETE request targets storage’s root container or its associated ACL resource,
|
// Solid, §5.4: "When a DELETE request targets storage’s root container or its associated ACL resource,
|
||||||
@ -266,22 +266,25 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
// Solid, §5.4: "When a contained resource is deleted,
|
// Solid, §5.4: "When a contained resource is deleted,
|
||||||
// the server MUST also delete the associated auxiliary resources"
|
// the server MUST also delete the associated auxiliary resources"
|
||||||
// https://solid.github.io/specification/protocol#deleting-resources
|
// https://solid.github.io/specification/protocol#deleting-resources
|
||||||
const deleted = [ identifier ];
|
const changes: ChangeMap = {};
|
||||||
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||||
const auxiliaries = this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier);
|
const auxiliaries = this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier);
|
||||||
deleted.push(...await this.safelyDeleteAuxiliaryResources(auxiliaries));
|
for (const deletedId of await this.safelyDeleteAuxiliaryResources(auxiliaries)) {
|
||||||
|
changes[deletedId.path] = this.createActivityMetadata(deletedId, AS.Delete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.identifierStrategy.isRootContainer(identifier)) {
|
if (!this.identifierStrategy.isRootContainer(identifier)) {
|
||||||
const container = this.identifierStrategy.getParentContainer(identifier);
|
const container = this.identifierStrategy.getParentContainer(identifier);
|
||||||
deleted.push(container);
|
changes[container.path] = this.createActivityMetadata(container, AS.Update);
|
||||||
|
|
||||||
// Update modified date of parent
|
// Update modified date of parent
|
||||||
await this.updateContainerModifiedDate(container);
|
await this.updateContainerModifiedDate(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.accessor.deleteResource(identifier);
|
await this.accessor.deleteResource(identifier);
|
||||||
return deleted;
|
changes[identifier.path] = this.createActivityMetadata(identifier, AS.Delete);
|
||||||
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -359,7 +362,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
* @returns Identifiers of resources that were possibly modified.
|
* @returns Identifiers of resources that were possibly modified.
|
||||||
*/
|
*/
|
||||||
protected async writeData(identifier: ResourceIdentifier, representation: Representation, isContainer: boolean,
|
protected async writeData(identifier: ResourceIdentifier, representation: Representation, isContainer: boolean,
|
||||||
createContainers: boolean, exists: boolean): Promise<ResourceIdentifier[]> {
|
createContainers: boolean, exists: boolean): Promise<ChangeMap> {
|
||||||
// Make sure the metadata has the correct identifier and correct type quads
|
// Make sure the metadata has the correct identifier and correct type quads
|
||||||
// Need to do this before handling container data to have the correct identifier
|
// Need to do this before handling container data to have the correct identifier
|
||||||
representation.metadata.identifier = DataFactory.namedNode(identifier.path);
|
representation.metadata.identifier = DataFactory.namedNode(identifier.path);
|
||||||
@ -382,18 +385,22 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
// Solid, §5.3: "Servers MUST create intermediate containers and include corresponding containment triples
|
// 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."
|
// in container representations derived from the URI path component of PUT and PATCH requests."
|
||||||
// https://solid.github.io/specification/protocol#writing-resources
|
// https://solid.github.io/specification/protocol#writing-resources
|
||||||
const modified = [];
|
let changes: ChangeMap = {};
|
||||||
if (!this.identifierStrategy.isRootContainer(identifier) && !exists) {
|
if (!this.identifierStrategy.isRootContainer(identifier) && !exists) {
|
||||||
const container = this.identifierStrategy.getParentContainer(identifier);
|
const parent = this.identifierStrategy.getParentContainer(identifier);
|
||||||
if (!createContainers) {
|
if (!createContainers) {
|
||||||
modified.push(container);
|
changes[parent.path] = this.createActivityMetadata(parent, AS.Update);
|
||||||
} else {
|
} else {
|
||||||
const created = await this.createRecursiveContainers(container);
|
const createdContainers = await this.createRecursiveContainers(parent);
|
||||||
modified.push(...created.length === 0 ? [ container ] : created);
|
changes = { ...changes, ...createdContainers };
|
||||||
|
|
||||||
|
if (Object.keys(createdContainers).length === 0) {
|
||||||
|
changes[parent.path] = this.createActivityMetadata(parent, AS.Update);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent container is also modified
|
// Parent container is also modified
|
||||||
await this.updateContainerModifiedDate(container);
|
await this.updateContainerModifiedDate(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all generated metadata to prevent it from being stored permanently
|
// Remove all generated metadata to prevent it from being stored permanently
|
||||||
@ -403,7 +410,8 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
this.accessor.writeContainer(identifier, representation.metadata) :
|
this.accessor.writeContainer(identifier, representation.metadata) :
|
||||||
this.accessor.writeDocument(identifier, representation.data, representation.metadata));
|
this.accessor.writeDocument(identifier, representation.data, representation.metadata));
|
||||||
|
|
||||||
return [ ...modified, identifier ];
|
changes[identifier.path] = this.createActivityMetadata(identifier, exists ? AS.Update : AS.Create);
|
||||||
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -597,7 +605,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
* Will throw errors if the identifier of the last existing "container" corresponds to an existing document.
|
* Will throw errors if the identifier of the last existing "container" corresponds to an existing document.
|
||||||
* @param container - Identifier of the container which will need to exist.
|
* @param container - Identifier of the container which will need to exist.
|
||||||
*/
|
*/
|
||||||
protected async createRecursiveContainers(container: ResourceIdentifier): Promise<ResourceIdentifier[]> {
|
protected async createRecursiveContainers(container: ResourceIdentifier): Promise<ChangeMap> {
|
||||||
// Verify whether the container already exists
|
// Verify whether the container already exists
|
||||||
try {
|
try {
|
||||||
const metadata = await this.getNormalizedMetadata(container);
|
const metadata = await this.getNormalizedMetadata(container);
|
||||||
@ -609,7 +617,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
if (!isContainerPath(metadata.identifier.value)) {
|
if (!isContainerPath(metadata.identifier.value)) {
|
||||||
throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`);
|
throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`);
|
||||||
}
|
}
|
||||||
return [];
|
return {};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (!NotFoundHttpError.isInstance(error)) {
|
if (!NotFoundHttpError.isInstance(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -618,9 +626,14 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
|
|
||||||
// Create the container, starting with its parent
|
// Create the container, starting with its parent
|
||||||
const ancestors = this.identifierStrategy.isRootContainer(container) ?
|
const ancestors = this.identifierStrategy.isRootContainer(container) ?
|
||||||
[] :
|
{} :
|
||||||
await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
|
await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
|
||||||
await this.writeData(container, new BasicRepresentation([], container), true, false, false);
|
const changes = await this.writeData(container, new BasicRepresentation([], container), true, false, false);
|
||||||
return [ ...ancestors, container ];
|
|
||||||
|
return { ...changes, ...ancestors };
|
||||||
|
}
|
||||||
|
|
||||||
|
private createActivityMetadata(id: ResourceIdentifier, activity: string): RepresentationMetadata {
|
||||||
|
return new RepresentationMetadata(id, { [SOLID_AS.terms.Activity.value]: activity });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import type { ExpiringReadWriteLocker } from '../util/locking/ExpiringReadWriteL
|
|||||||
import { endOfStream } from '../util/StreamUtil';
|
import { endOfStream } from '../util/StreamUtil';
|
||||||
import type { AtomicResourceStore } from './AtomicResourceStore';
|
import type { AtomicResourceStore } from './AtomicResourceStore';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that for every call acquires a lock before executing it on the requested resource,
|
* Store that for every call acquires a lock before executing it on the requested resource,
|
||||||
@ -46,27 +46,27 @@ export class LockingResourceStore implements AtomicResourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation,
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.locks.withWriteLock(this.getLockIdentifier(container),
|
return this.locks.withWriteLock(this.getLockIdentifier(container),
|
||||||
async(): Promise<ResourceIdentifier> => this.source.addResource(container, representation, conditions));
|
async(): Promise<ChangeMap> => this.source.addResource(container, representation, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
|
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
|
||||||
async(): Promise<ResourceIdentifier[]> => this.source.setRepresentation(identifier, representation, conditions));
|
async(): Promise<ChangeMap> => this.source.setRepresentation(identifier, representation, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier,
|
public async deleteResource(identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
|
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
|
||||||
async(): Promise<ResourceIdentifier[]> => this.source.deleteResource(identifier, conditions));
|
async(): Promise<ChangeMap> => this.source.deleteResource(identifier, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
|
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
|
||||||
async(): Promise<ResourceIdentifier[]> => this.source.modifyResource(identifier, patch, conditions));
|
async(): Promise<ChangeMap> => this.source.modifyResource(identifier, patch, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,8 +3,9 @@ import type { Patch } from '../http/representation/Patch';
|
|||||||
import type { Representation } from '../http/representation/Representation';
|
import type { Representation } from '../http/representation/Representation';
|
||||||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||||
|
import { AS, SOLID_AS } from '../util/Vocabularies';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that notifies listeners of changes to its source
|
* Store that notifies listeners of changes to its source
|
||||||
@ -29,31 +30,34 @@ export class MonitoringStore<T extends ResourceStore = ResourceStore>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation,
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
const identifier = await this.source.addResource(container, representation, conditions);
|
return this.emitChanged(await this.source.addResource(container, representation, conditions));
|
||||||
this.emitChanged([ container, identifier ]);
|
|
||||||
return identifier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier,
|
public async deleteResource(identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.emitChanged(await this.source.deleteResource(identifier, conditions));
|
return this.emitChanged(await this.source.deleteResource(identifier, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.emitChanged(await this.source.setRepresentation(identifier, representation, conditions));
|
return this.emitChanged(await this.source.setRepresentation(identifier, representation, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.emitChanged(await this.source.modifyResource(identifier, patch, conditions));
|
return this.emitChanged(await this.source.modifyResource(identifier, patch, conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitChanged(identifiers: ResourceIdentifier[]): typeof identifiers {
|
private emitChanged(changes: ChangeMap): ChangeMap {
|
||||||
for (const identifier of identifiers) {
|
for (const [ key, value ] of Object.entries(changes)) {
|
||||||
this.emit('changed', identifier);
|
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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return identifiers;
|
|
||||||
|
return changes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import type { Representation } from '../http/representation/Representation';
|
|||||||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that calls the corresponding functions of the source Store.
|
* Store that calls the corresponding functions of the source Store.
|
||||||
@ -27,22 +27,22 @@ export class PassthroughStore<T extends ResourceStore = ResourceStore> implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation,
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.source.addResource(container, representation, conditions);
|
return this.source.addResource(container, representation, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier,
|
public async deleteResource(identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.source.deleteResource(identifier, conditions);
|
return this.source.deleteResource(identifier, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.source.modifyResource(identifier, patch, conditions);
|
return this.source.modifyResource(identifier, patch, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return this.source.setRepresentation(identifier, representation, conditions);
|
return this.source.setRepresentation(identifier, representation, conditions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'
|
|||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import { PassthroughStore } from './PassthroughStore';
|
import { PassthroughStore } from './PassthroughStore';
|
||||||
import type { PatchHandler } from './patch/PatchHandler';
|
import type { PatchHandler } from './patch/PatchHandler';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ResourceStore} using decorator pattern for the `modifyResource` function.
|
* {@link ResourceStore} using decorator pattern for the `modifyResource` function.
|
||||||
@ -20,7 +20,7 @@ export class PatchingStore<T extends ResourceStore = ResourceStore> extends Pass
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
try {
|
try {
|
||||||
return await this.source.modifyResource(identifier, patch, conditions);
|
return await this.source.modifyResource(identifier, patch, conditions);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
@ -4,7 +4,7 @@ import type { ResourceIdentifier } from '../http/representation/ResourceIdentifi
|
|||||||
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
|
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import { PassthroughStore } from './PassthroughStore';
|
import { PassthroughStore } from './PassthroughStore';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that only allow read operations on the underlying source.
|
* Store that only allow read operations on the underlying source.
|
||||||
@ -16,22 +16,22 @@ export class ReadOnlyStore<T extends ResourceStore = ResourceStore> extends Pass
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation,
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new ForbiddenHttpError();
|
throw new ForbiddenHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier,
|
public async deleteResource(identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new ForbiddenHttpError();
|
throw new ForbiddenHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new ForbiddenHttpError();
|
throw new ForbiddenHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
throw new ForbiddenHttpError();
|
throw new ForbiddenHttpError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import type { Conditions } from './Conditions';
|
|||||||
import { PassthroughConverter } from './conversion/PassthroughConverter';
|
import { PassthroughConverter } from './conversion/PassthroughConverter';
|
||||||
import type { RepresentationConverter } from './conversion/RepresentationConverter';
|
import type { RepresentationConverter } from './conversion/RepresentationConverter';
|
||||||
import { PassthroughStore } from './PassthroughStore';
|
import { PassthroughStore } from './PassthroughStore';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that provides (optional) conversion of incoming and outgoing {@link Representation}s.
|
* Store that provides (optional) conversion of incoming and outgoing {@link Representation}s.
|
||||||
@ -40,7 +40,7 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(identifier: ResourceIdentifier, representation: Representation,
|
public async addResource(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
// We can potentially run into problems here if we convert a turtle document where the base IRI is required,
|
// We can potentially run into problems here if we convert a turtle document where the base IRI is required,
|
||||||
// since we don't know the resource IRI yet at this point.
|
// since we don't know the resource IRI yet at this point.
|
||||||
representation = await this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences });
|
representation = await this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences });
|
||||||
@ -48,7 +48,7 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
representation = await this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences });
|
representation = await this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences });
|
||||||
return this.source.setRepresentation(identifier, representation, conditions);
|
return this.source.setRepresentation(identifier, representation, conditions);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import type { Patch } from '../http/representation/Patch';
|
import type { Patch } from '../http/representation/Patch';
|
||||||
import type { Representation } from '../http/representation/Representation';
|
import type { Representation } from '../http/representation/Representation';
|
||||||
|
import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
||||||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceSet } from './ResourceSet';
|
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.
|
||||||
|
*/
|
||||||
|
export type ChangeMap = Record<string, RepresentationMetadata>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ResourceStore represents a collection of resources.
|
* A ResourceStore represents a collection of resources.
|
||||||
* It has been designed such that each of its methods
|
* It has been designed such that each of its methods
|
||||||
@ -38,13 +46,13 @@ export interface ResourceStore extends ResourceSet {
|
|||||||
* @param representation - New representation of the resource.
|
* @param representation - New representation of the resource.
|
||||||
* @param conditions - Optional conditions under which to proceed.
|
* @param conditions - Optional conditions under which to proceed.
|
||||||
*
|
*
|
||||||
* @returns Identifiers of resources that were possibly modified.
|
* @returns A {@link ChangeMap}.
|
||||||
*/
|
*/
|
||||||
setRepresentation: (
|
setRepresentation: (
|
||||||
identifier: ResourceIdentifier,
|
identifier: ResourceIdentifier,
|
||||||
representation: Representation,
|
representation: Representation,
|
||||||
conditions?: Conditions,
|
conditions?: Conditions,
|
||||||
) => Promise<ResourceIdentifier[]>;
|
) => Promise<ChangeMap>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new resource in the container.
|
* Creates a new resource in the container.
|
||||||
@ -52,25 +60,25 @@ export interface ResourceStore extends ResourceSet {
|
|||||||
* @param representation - Representation of the new resource
|
* @param representation - Representation of the new resource
|
||||||
* @param conditions - Optional conditions under which to proceed.
|
* @param conditions - Optional conditions under which to proceed.
|
||||||
*
|
*
|
||||||
* @returns The identifier of the newly created resource.
|
* @returns A {@link ChangeMap}.
|
||||||
*/
|
*/
|
||||||
addResource: (
|
addResource: (
|
||||||
container: ResourceIdentifier,
|
container: ResourceIdentifier,
|
||||||
representation: Representation,
|
representation: Representation,
|
||||||
conditions?: Conditions,
|
conditions?: Conditions,
|
||||||
) => Promise<ResourceIdentifier>;
|
) => Promise<ChangeMap>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a resource.
|
* Deletes a resource.
|
||||||
* @param identifier - Identifier of resource to delete.
|
* @param identifier - Identifier of resource to delete.
|
||||||
* @param conditions - Optional conditions under which to proceed.
|
* @param conditions - Optional conditions under which to proceed.
|
||||||
*
|
*
|
||||||
* @returns Identifiers of resources that were possibly modified.
|
* @returns A {@link ChangeMap}.
|
||||||
*/
|
*/
|
||||||
deleteResource: (
|
deleteResource: (
|
||||||
identifier: ResourceIdentifier,
|
identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions,
|
conditions?: Conditions,
|
||||||
) => Promise<ResourceIdentifier[]>;
|
) => Promise<ChangeMap>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets or updates the representation of a resource,
|
* Sets or updates the representation of a resource,
|
||||||
@ -79,11 +87,11 @@ export interface ResourceStore extends ResourceSet {
|
|||||||
* @param patch - Description of which parts to update.
|
* @param patch - Description of which parts to update.
|
||||||
* @param conditions - Optional conditions under which to proceed.
|
* @param conditions - Optional conditions under which to proceed.
|
||||||
*
|
*
|
||||||
* @returns Identifiers of resources that were possibly modified.
|
* @returns A {@link ChangeMap}.
|
||||||
*/
|
*/
|
||||||
modifyResource: (
|
modifyResource: (
|
||||||
identifier: ResourceIdentifier,
|
identifier: ResourceIdentifier,
|
||||||
patch: Patch,
|
patch: Patch,
|
||||||
conditions?: Conditions,
|
conditions?: Conditions,
|
||||||
) => Promise<ResourceIdentifier[]>;
|
) => Promise<ChangeMap>;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import type { ResourceIdentifier } from '../http/representation/ResourceIdentifi
|
|||||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||||
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||||
import type { RouterRule } from './routing/RouterRule';
|
import type { RouterRule } from './routing/RouterRule';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,22 +31,22 @@ export class RoutingResourceStore implements ResourceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation,
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return (await this.getStore(container, representation)).addResource(container, representation, conditions);
|
return (await this.getStore(container, representation)).addResource(container, representation, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return (await this.getStore(identifier, representation)).setRepresentation(identifier, representation, conditions);
|
return (await this.getStore(identifier, representation)).setRepresentation(identifier, representation, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier,
|
public async deleteResource(identifier: ResourceIdentifier,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return (await this.getStore(identifier)).deleteResource(identifier, conditions);
|
return (await this.getStore(identifier)).deleteResource(identifier, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
|
||||||
conditions?: Conditions): Promise<ResourceIdentifier[]> {
|
conditions?: Conditions): Promise<ChangeMap> {
|
||||||
return (await this.getStore(identifier)).modifyResource(identifier, patch, conditions);
|
return (await this.getStore(identifier)).modifyResource(identifier, patch, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Patch } from '../../http/representation/Patch';
|
import type { Patch } from '../../http/representation/Patch';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||||
import type { ResourceStore } from '../ResourceStore';
|
import type { ResourceStore, ChangeMap } from '../ResourceStore';
|
||||||
|
|
||||||
export type PatchHandlerInput<T extends ResourceStore = ResourceStore> = {
|
export type PatchHandlerInput<T extends ResourceStore = ResourceStore> = {
|
||||||
source: T;
|
source: T;
|
||||||
@ -13,4 +13,4 @@ export type PatchHandlerInput<T extends ResourceStore = ResourceStore> = {
|
|||||||
* Executes the given Patch.
|
* Executes the given Patch.
|
||||||
*/
|
*/
|
||||||
export abstract class PatchHandler<T extends ResourceStore = ResourceStore>
|
export abstract class PatchHandler<T extends ResourceStore = ResourceStore>
|
||||||
extends AsyncHandler<PatchHandlerInput<T>, ResourceIdentifier[]> {}
|
extends AsyncHandler<PatchHandlerInput<T>, ChangeMap> {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
|
import type { ChangeMap } from '../ResourceStore';
|
||||||
import type { PatchHandlerInput } from './PatchHandler';
|
import type { PatchHandlerInput } from './PatchHandler';
|
||||||
import { PatchHandler } from './PatchHandler';
|
import { PatchHandler } from './PatchHandler';
|
||||||
import type { RepresentationPatcher } from './RepresentationPatcher';
|
import type { RepresentationPatcher } from './RepresentationPatcher';
|
||||||
@ -23,7 +23,7 @@ export class RepresentationPatchHandler extends PatchHandler {
|
|||||||
this.patcher = patcher;
|
this.patcher = patcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ source, patch, identifier }: PatchHandlerInput): Promise<ResourceIdentifier[]> {
|
public async handle({ source, patch, identifier }: PatchHandlerInput): Promise<ChangeMap> {
|
||||||
// Get the representation from the store
|
// Get the representation from the store
|
||||||
let representation: Representation | undefined;
|
let representation: Representation | undefined;
|
||||||
try {
|
try {
|
||||||
|
@ -71,6 +71,12 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#',
|
|||||||
'Control',
|
'Control',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const AS = createUriAndTermNamespace('https://www.w3.org/ns/activitystreams#',
|
||||||
|
'Create',
|
||||||
|
'Delete',
|
||||||
|
'Update',
|
||||||
|
);
|
||||||
|
|
||||||
export const AUTH = createUriAndTermNamespace('urn:solid:auth:',
|
export const AUTH = createUriAndTermNamespace('urn:solid:auth:',
|
||||||
'userMode',
|
'userMode',
|
||||||
'publicMode',
|
'publicMode',
|
||||||
@ -144,6 +150,10 @@ export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms
|
|||||||
'InsertDeletePatch',
|
'InsertDeletePatch',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SOLID_AS = createUriAndTermNamespace('http://www.w3.org/ns/solid/activitystreams#',
|
||||||
|
'Activity',
|
||||||
|
);
|
||||||
|
|
||||||
export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:',
|
export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:',
|
||||||
'disallowedMethod',
|
'disallowedMethod',
|
||||||
'errorResponse',
|
'errorResponse',
|
||||||
|
@ -3,12 +3,11 @@ import type { Operation } from '../../../../src/http/Operation';
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
|
||||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore, ChangeMap } from '../../../../src/storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
import { AS, SOLID_AS, SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
describe('A PostOperationHandler', (): void => {
|
describe('A PostOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
@ -21,7 +20,16 @@ describe('A PostOperationHandler', (): void => {
|
|||||||
body = new BasicRepresentation('', 'text/turtle');
|
body = new BasicRepresentation('', 'text/turtle');
|
||||||
operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||||
store = {
|
store = {
|
||||||
addResource: jest.fn(async(): Promise<ResourceIdentifier> => ({ path: 'newPath' } as ResourceIdentifier)),
|
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;
|
} as unknown as ResourceStore;
|
||||||
handler = new PostOperationHandler(store);
|
handler = new PostOperationHandler(store);
|
||||||
});
|
});
|
||||||
@ -41,7 +49,7 @@ describe('A PostOperationHandler', (): void => {
|
|||||||
const result = await handler.handle({ operation });
|
const result = await handler.handle({ operation });
|
||||||
expect(result.statusCode).toBe(201);
|
expect(result.statusCode).toBe(201);
|
||||||
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
|
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
|
||||||
expect(result.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe('newPath');
|
expect(result.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe('https://example.com/parent/newPath');
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
expect(store.addResource).toHaveBeenCalledTimes(1);
|
expect(store.addResource).toHaveBeenCalledTimes(1);
|
||||||
expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||||
|
@ -23,7 +23,7 @@ import type { Guarded } from '../../../src/util/GuardedStream';
|
|||||||
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
|
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
|
||||||
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
|
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
|
||||||
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF, SOLID_META, DC } from '../../../src/util/Vocabularies';
|
import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF, SOLID_META, DC, SOLID_AS, AS } from '../../../src/util/Vocabularies';
|
||||||
const { namedNode, quad } = DataFactory;
|
const { namedNode, quad } = DataFactory;
|
||||||
|
|
||||||
const GENERATED_PREDICATE = namedNode('generated');
|
const GENERATED_PREDICATE = namedNode('generated');
|
||||||
@ -265,11 +265,22 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
representation.metadata.removeAll(RDF.terms.type);
|
representation.metadata.removeAll(RDF.terms.type);
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result).toEqual({
|
|
||||||
path: expect.stringMatching(new RegExp(`^${root}[^/]+$`, 'u')),
|
expect(Object.keys(result)).toEqual([
|
||||||
});
|
root,
|
||||||
await expect(arrayifyStream(accessor.data[result.path].data)).resolves.toEqual([ resourceData ]);
|
expect.stringMatching(new RegExp(`^${root}[^/]+$`, 'u')),
|
||||||
expect(accessor.data[result.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
]);
|
||||||
|
|
||||||
|
expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||||
|
|
||||||
|
const generatedID = Object.keys(result).find((key): boolean => key !== root)!;
|
||||||
|
expect(generatedID).toBeDefined();
|
||||||
|
expect(generatedID).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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can write containers.', async(): Promise<void> => {
|
it('can write containers.', async(): Promise<void> => {
|
||||||
@ -278,16 +289,25 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.data = guardedStreamFrom([ '<> a <http://test.com/coolContainer>.' ]);
|
representation.data = guardedStreamFrom([ '<> a <http://test.com/coolContainer>.' ]);
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result).toEqual({
|
|
||||||
path: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')),
|
|
||||||
});
|
|
||||||
expect(accessor.data[result.path]).toBeTruthy();
|
|
||||||
expect(accessor.data[result.path].metadata.contentType).toBeUndefined();
|
|
||||||
|
|
||||||
const { data, metadata } = await store.getRepresentation(result);
|
expect(Object.keys(result)).toEqual([
|
||||||
|
root,
|
||||||
|
expect.stringMatching(new RegExp(`^${root}[^/]+?/$`, 'u')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||||
|
|
||||||
|
const generatedID = Object.keys(result).find((key): boolean => key !== root)!;
|
||||||
|
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);
|
||||||
|
|
||||||
|
const { data, metadata } = await store.getRepresentation({ path: generatedID });
|
||||||
const quads = await arrayifyStream<Quad>(data);
|
const quads = await arrayifyStream<Quad>(data);
|
||||||
expect(metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
expect(metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
expect(quads.some((entry): boolean => entry.subject.value === result.path &&
|
expect(quads.some((entry): boolean => entry.subject.value === generatedID &&
|
||||||
entry.object.value === 'http://test.com/coolContainer')).toBeTruthy();
|
entry.object.value === 'http://test.com/coolContainer')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -295,10 +315,14 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
representation.metadata.removeAll(RDF.terms.type);
|
representation.metadata.removeAll(RDF.terms.type);
|
||||||
representation.metadata.add(SOLID_HTTP.terms.slug, 'newName');
|
representation.metadata.add(SOLID_HTTP.terms.slug, 'newName');
|
||||||
const result = await store.addResource(resourceID, representation);
|
|
||||||
expect(result).toEqual({
|
const result = store.addResource(resourceID, representation);
|
||||||
path: `${root}newName`,
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors on a slug ending on / without Link rel:type Container header.', async(): Promise<void> => {
|
it('errors on a slug ending on / without Link rel:type Container header.', async(): Promise<void> => {
|
||||||
@ -320,10 +344,14 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.add(RDF.terms.type, LDP.terms.Container);
|
representation.metadata.add(RDF.terms.type, LDP.terms.Container);
|
||||||
representation.metadata.add(SOLID_HTTP.terms.slug, 'newContainer');
|
representation.metadata.add(SOLID_HTTP.terms.slug, 'newContainer');
|
||||||
representation.data = guardedStreamFrom([ `` ]);
|
representation.data = guardedStreamFrom([ `` ]);
|
||||||
|
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
path: `${root}newContainer/`,
|
[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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates a new URI if adding the slug would create an existing URI.', async(): Promise<void> => {
|
it('generates a new URI if adding the slug would create an existing URI.', async(): Promise<void> => {
|
||||||
@ -331,20 +359,26 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.add(SOLID_HTTP.terms.slug, 'newName');
|
representation.metadata.add(SOLID_HTTP.terms.slug, 'newName');
|
||||||
accessor.data[`${root}newName`] = representation;
|
accessor.data[`${root}newName`] = representation;
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result).not.toEqual({
|
expect(result).not.toEqual(expect.objectContaining({
|
||||||
path: `${root}newName`,
|
[`${root}newName`]: expect.any(RepresentationMetadata),
|
||||||
});
|
}));
|
||||||
expect(result).not.toEqual({
|
expect(result).not.toEqual(expect.objectContaining({
|
||||||
path: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')),
|
[expect.any(String)]: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')),
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates http://test.com/%26%26 when slug is &%26.', async(): Promise<void> => {
|
it('generates http://test.com/%26%26 when slug is &%26.', async(): Promise<void> => {
|
||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
representation.metadata.removeAll(RDF.terms.type);
|
representation.metadata.removeAll(RDF.terms.type);
|
||||||
representation.metadata.add(SOLID_HTTP.terms.slug, '&%26');
|
representation.metadata.add(SOLID_HTTP.terms.slug, '&%26');
|
||||||
const result = await store.addResource(resourceID, representation);
|
|
||||||
expect(result).toEqual({ path: `${root}%26%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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if the slug contains a slash.', async(): Promise<void> => {
|
it('errors if the slug contains a slash.', async(): Promise<void> => {
|
||||||
@ -408,8 +442,11 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.data = guardedStreamFrom([ `<${root}> a <coolContainer>.` ]);
|
representation.data = guardedStreamFrom([ `<${root}> a <coolContainer>.` ]);
|
||||||
|
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
.toEqual([{ path: `${root}` }]);
|
await expect(result).resolves.toEqual({
|
||||||
|
[root]: expect.any(RepresentationMetadata),
|
||||||
|
});
|
||||||
|
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||||
expect(mock).toHaveBeenCalledTimes(1);
|
expect(mock).toHaveBeenCalledTimes(1);
|
||||||
expect(mock).toHaveBeenLastCalledWith(resourceID);
|
expect(mock).toHaveBeenLastCalledWith(resourceID);
|
||||||
|
|
||||||
@ -438,10 +475,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
it('can write resources.', async(): Promise<void> => {
|
it('can write resources.', async(): Promise<void> => {
|
||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: root },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}resource` },
|
[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);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
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[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
@ -455,10 +495,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.removeAll(RDF.terms.type);
|
representation.metadata.removeAll(RDF.terms.type);
|
||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.data = guardedStreamFrom([ `<${root}resource/> a <coolContainer>.` ]);
|
representation.data = guardedStreamFrom([ `<${root}resource/> a <coolContainer>.` ]);
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: root },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}container/` },
|
[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);
|
||||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||||
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
@ -468,10 +511,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
it('can overwrite resources which does not update parent metadata.', async(): Promise<void> => {
|
it('can overwrite resources which does not update parent metadata.', async(): Promise<void> => {
|
||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: root },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}resource` },
|
[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);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
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[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
@ -479,9 +525,11 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
// Parent metadata does not get updated if the resource already exists
|
// Parent metadata does not get updated if the resource already exists
|
||||||
representation = new BasicRepresentation('updatedText', 'text/plain');
|
representation = new BasicRepresentation('updatedText', 'text/plain');
|
||||||
mockDate.mockReturnValue(later);
|
mockDate.mockReturnValue(later);
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result2 = store.setRepresentation(resourceID, representation);
|
||||||
{ path: `${root}resource` },
|
await expect(result2).resolves.toEqual({
|
||||||
]);
|
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||||
|
});
|
||||||
|
expect((await result2)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ 'updatedText' ]);
|
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[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(later.toISOString());
|
||||||
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
@ -492,10 +540,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
representation.metadata.add(namedNode('notGen'), 'value');
|
representation.metadata.add(namedNode('notGen'), 'value');
|
||||||
representation.metadata.add(namedNode('gen'), 'value', SOLID_META.terms.ResponseMetadata);
|
representation.metadata.add(namedNode('gen'), 'value', SOLID_META.terms.ResponseMetadata);
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: root },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}resource` },
|
[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);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
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('notGen'))?.value).toBe('value');
|
||||||
expect(accessor.data[resourceID.path].metadata.get(namedNode('gen'))).toBeUndefined();
|
expect(accessor.data[resourceID.path].metadata.get(namedNode('gen'))).toBeUndefined();
|
||||||
@ -505,10 +556,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete accessor.data[root];
|
delete accessor.data[root];
|
||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: `${root}` },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}resource` },
|
[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);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -521,10 +575,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.data = guardedStreamFrom(
|
representation.data = guardedStreamFrom(
|
||||||
[ quad(namedNode(`${root}resource/`), namedNode('a'), namedNode('coolContainer')) ],
|
[ quad(namedNode(`${root}resource/`), namedNode('a'), namedNode('coolContainer')) ],
|
||||||
);
|
);
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: `${root}` },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}container/` },
|
[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);
|
||||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -544,11 +601,17 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
it('creates recursive containers when needed.', async(): Promise<void> => {
|
it('creates recursive containers when needed.', async(): Promise<void> => {
|
||||||
const resourceID = { path: `${root}a/b/resource` };
|
const resourceID = { path: `${root}a/b/resource` };
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: `${root}a/` },
|
await expect(result).resolves.toEqual({
|
||||||
{ path: `${root}a/b/` },
|
[`${root}`]: expect.any(RepresentationMetadata),
|
||||||
{ path: `${root}a/b/resource` },
|
[`${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);
|
||||||
await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
|
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))
|
expect(accessor.data[`${root}a/`].metadata.getAll(RDF.terms.type).map((type): string => type.value))
|
||||||
.toContain(LDP.Container);
|
.toContain(LDP.Container);
|
||||||
@ -573,9 +636,11 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.removeAll(RDF.terms.type);
|
representation.metadata.removeAll(RDF.terms.type);
|
||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.data = guardedStreamFrom([]);
|
representation.data = guardedStreamFrom([]);
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
|
const result = store.setRepresentation(resourceID, representation);
|
||||||
{ path: `${root}` },
|
await expect(result).resolves.toEqual({
|
||||||
]);
|
[root]: expect.any(RepresentationMetadata),
|
||||||
|
});
|
||||||
|
expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create);
|
||||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||||
expect(Object.keys(accessor.data)).toHaveLength(1);
|
expect(Object.keys(accessor.data)).toHaveLength(1);
|
||||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||||
@ -658,51 +723,70 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('will delete resources.', async(): Promise<void> => {
|
it('will delete resources.', async(): Promise<void> => {
|
||||||
accessor.data[`${root}resource`] = representation;
|
const resourceID = { path: `${root}resource` };
|
||||||
await expect(store.deleteResource({ path: `${root}resource` })).resolves.toEqual([
|
accessor.data[resourceID.path] = representation;
|
||||||
{ path: `${root}resource` },
|
await expect(store.deleteResource(resourceID)).resolves.toEqual({
|
||||||
{ path: root },
|
[root]: expect.any(RepresentationMetadata),
|
||||||
]);
|
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||||
expect(accessor.data[`${root}resource`]).toBeUndefined();
|
});
|
||||||
|
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(DC.terms.modified)?.value).toBe(now.toISOString());
|
||||||
expect(accessor.data[root].metadata.get(GENERATED_PREDICATE)).toBeUndefined();
|
expect(accessor.data[root].metadata.get(GENERATED_PREDICATE)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will delete root non-storage containers.', async(): Promise<void> => {
|
it('will delete root non-storage containers.', async(): Promise<void> => {
|
||||||
accessor.data[root] = new BasicRepresentation(representation.data, containerMetadata);
|
accessor.data[root] = new BasicRepresentation(representation.data, containerMetadata);
|
||||||
await expect(store.deleteResource({ path: root })).resolves.toEqual([
|
await expect(store.deleteResource({ path: root })).resolves.toEqual(
|
||||||
{ path: root },
|
{ [root]: expect.any(RepresentationMetadata) },
|
||||||
]);
|
);
|
||||||
expect(accessor.data[root]).toBeUndefined();
|
expect(accessor.data[root]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will delete a root storage auxiliary resource of a non-root container.', async(): Promise<void> => {
|
it('will delete a root storage auxiliary resource of a non-root container.', async(): Promise<void> => {
|
||||||
|
const resourceID = { path: `${root}container/` };
|
||||||
|
const auxResourceID = { path: `${root}container/.dummy` };
|
||||||
const storageMetadata = new RepresentationMetadata(representation.metadata);
|
const storageMetadata = new RepresentationMetadata(representation.metadata);
|
||||||
accessor.data[`${root}container/`] = new BasicRepresentation(representation.data, storageMetadata);
|
accessor.data[resourceID.path] = new BasicRepresentation(representation.data, storageMetadata);
|
||||||
accessor.data[`${root}container/.dummy`] = representation;
|
accessor.data[auxResourceID.path] = representation;
|
||||||
auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
|
auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
|
||||||
await expect(store.deleteResource({ path: `${root}container/.dummy` })).resolves.toEqual([
|
const result = store.deleteResource(auxResourceID);
|
||||||
{ path: `${root}container/.dummy` },
|
await expect(result).resolves.toEqual(
|
||||||
{ path: `${root}container/` },
|
expect.objectContaining({
|
||||||
]);
|
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||||
expect(accessor.data[`${root}container/.dummy`]).toBeUndefined();
|
[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);
|
||||||
|
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will delete related auxiliary resources.', async(): Promise<void> => {
|
it('will delete related auxiliary resources.', async(): Promise<void> => {
|
||||||
accessor.data[`${root}container/`] = representation;
|
const resourceID = { path: `${root}container/` };
|
||||||
accessor.data[`${root}container/.dummy`] = representation;
|
const auxResourceID = { path: `${root}container/.dummy` };
|
||||||
await expect(store.deleteResource({ path: `${root}container/` })).resolves.toEqual([
|
accessor.data[resourceID.path] = representation;
|
||||||
{ path: `${root}container/` },
|
accessor.data[auxResourceID.path] = representation;
|
||||||
{ path: `${root}container/.dummy` },
|
|
||||||
{ path: root },
|
const result = store.deleteResource(resourceID);
|
||||||
]);
|
await expect(result).resolves.toEqual(
|
||||||
expect(accessor.data[`${root}container/`]).toBeUndefined();
|
expect.objectContaining({
|
||||||
expect(accessor.data[`${root}container/.dummy`]).toBeUndefined();
|
[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);
|
||||||
|
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||||
|
expect(accessor.data[auxResourceID.path]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will still delete a resource if deleting auxiliary resources causes errors.', async(): Promise<void> => {
|
it('will still delete a resource if deleting auxiliary resources causes errors.', async(): Promise<void> => {
|
||||||
accessor.data[`${root}resource`] = representation;
|
const resourceID = { path: `${root}resource` };
|
||||||
accessor.data[`${root}resource.dummy`] = representation;
|
const auxResourceID = { path: `${root}resource.dummy` };
|
||||||
|
accessor.data[resourceID.path] = representation;
|
||||||
|
accessor.data[auxResourceID.path] = representation;
|
||||||
const deleteFn = accessor.deleteResource;
|
const deleteFn = accessor.deleteResource;
|
||||||
accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise<void> => {
|
accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise<void> => {
|
||||||
if (auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
if (auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||||
@ -712,12 +796,14 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
});
|
});
|
||||||
const { logger } = store as any;
|
const { logger } = store as any;
|
||||||
logger.error = jest.fn();
|
logger.error = jest.fn();
|
||||||
await expect(store.deleteResource({ path: `${root}resource` })).resolves.toEqual([
|
const result = store.deleteResource(resourceID);
|
||||||
{ path: `${root}resource` },
|
expect(Object.keys(await result)).toHaveLength(2);
|
||||||
{ path: root },
|
await expect(result).resolves.toEqual({
|
||||||
]);
|
[root]: expect.any(RepresentationMetadata),
|
||||||
expect(accessor.data[`${root}resource`]).toBeUndefined();
|
[resourceID.path]: expect.any(RepresentationMetadata),
|
||||||
expect(accessor.data[`${root}resource.dummy`]).toBeDefined();
|
});
|
||||||
|
expect(accessor.data[resourceID.path]).toBeUndefined();
|
||||||
|
expect(accessor.data[auxResourceID.path]).toBeDefined();
|
||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
expect(logger.error).toHaveBeenLastCalledWith(
|
expect(logger.error).toHaveBeenLastCalledWith(
|
||||||
'Error deleting auxiliary resource http://test.com/resource.dummy: auxiliary error!',
|
'Error deleting auxiliary resource http://test.com/resource.dummy: auxiliary error!',
|
||||||
|
@ -1,33 +1,86 @@
|
|||||||
import type { Patch } from '../../../src/http/representation/Patch';
|
import type { Patch } from '../../../src/http/representation/Patch';
|
||||||
import type { Representation } from '../../../src/http/representation/Representation';
|
import type { Representation } from '../../../src/http/representation/Representation';
|
||||||
|
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
||||||
import { MonitoringStore } from '../../../src/storage/MonitoringStore';
|
import { MonitoringStore } from '../../../src/storage/MonitoringStore';
|
||||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||||
|
import { AS, SOLID_AS } from '../../../src/util/Vocabularies';
|
||||||
|
|
||||||
describe('A MonitoringStore', (): void => {
|
describe('A MonitoringStore', (): void => {
|
||||||
let store: MonitoringStore;
|
let store: MonitoringStore;
|
||||||
let source: ResourceStore;
|
let source: ResourceStore;
|
||||||
|
|
||||||
let changedCallback: () => void;
|
let changedCallback: () => void;
|
||||||
const modified = [
|
let createdCallback: () => void;
|
||||||
{ path: 'http://example.org/modified/1' },
|
let updatedCallback: () => void;
|
||||||
{ path: 'http://example.org/modified/2' },
|
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 },
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
source = {
|
source = {
|
||||||
getRepresentation: jest.fn(async(): Promise<any> => ({ success: true })),
|
getRepresentation: jest.fn(async(): Promise<any> => ({ success: true })),
|
||||||
addResource: jest.fn(async(): Promise<any> => ({ path: 'http://example.org/foo/bar/new' })),
|
addResource: jest.fn(async(): Promise<any> => addResourceReturnMock),
|
||||||
setRepresentation: jest.fn(async(): Promise<any> => modified),
|
setRepresentation: jest.fn(async(): Promise<any> => setRepresentationReturnMock),
|
||||||
deleteResource: jest.fn(async(): Promise<any> => modified),
|
deleteResource: jest.fn(async(): Promise<any> => deleteResourceReturnMock),
|
||||||
modifyResource: jest.fn(async(): Promise<any> => modified),
|
modifyResource: jest.fn(async(): Promise<any> => modifyResourceReturnMock),
|
||||||
hasResource: jest.fn(async(): Promise<any> => undefined),
|
hasResource: jest.fn(async(): Promise<any> => undefined),
|
||||||
};
|
};
|
||||||
store = new MonitoringStore(source);
|
store = new MonitoringStore(source);
|
||||||
|
|
||||||
changedCallback = jest.fn();
|
changedCallback = jest.fn();
|
||||||
|
createdCallback = jest.fn();
|
||||||
|
updatedCallback = jest.fn();
|
||||||
|
deletedCallback = jest.fn();
|
||||||
store.on('changed', changedCallback);
|
store.on('changed', changedCallback);
|
||||||
|
store.on(AS.Create, createdCallback);
|
||||||
|
store.on(AS.Update, updatedCallback);
|
||||||
|
store.on(AS.Delete, deletedCallback);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async(): Promise<void> => {
|
afterEach(async(): Promise<void> => {
|
||||||
store.removeListener('changed', changedCallback);
|
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> => {
|
it('calls getRepresentation directly from the source.', async(): Promise<void> => {
|
||||||
@ -43,67 +96,89 @@ describe('A MonitoringStore', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls addResource directly from the source.', async(): Promise<void> => {
|
it('calls addResource directly from the source.', async(): Promise<void> => {
|
||||||
await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation)).resolves
|
await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation)).resolves.toEqual({
|
||||||
.toStrictEqual({ path: 'http://example.org/foo/bar/new' });
|
'http://example.org/foo/bar/new': expect.any(RepresentationMetadata),
|
||||||
|
'http://example.org/foo/bar/': expect.any(RepresentationMetadata),
|
||||||
|
});
|
||||||
expect(source.addResource).toHaveBeenCalledTimes(1);
|
expect(source.addResource).toHaveBeenCalledTimes(1);
|
||||||
expect(source.addResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
expect(source.addResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires container and resource change events after addResource.', async(): Promise<void> => {
|
it('fires appropriate events according to the return value of source.addResource.', async(): Promise<void> => {
|
||||||
const result = store.addResource({ path: 'http://example.org/foo/bar/' }, {} as Representation);
|
const result = store.addResource({ path: 'http://example.org/foo/bar/' }, {} as Representation);
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Create);
|
||||||
|
expect(createdCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||||
|
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||||
|
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls setRepresentation directly from the source.', async(): Promise<void> => {
|
it('calls setRepresentation directly from the source.', async(): Promise<void> => {
|
||||||
await expect(store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation))
|
await expect(store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation))
|
||||||
.resolves.toEqual(modified);
|
.resolves.toEqual(setRepresentationReturnMock);
|
||||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires all modified change events after setRepresentation.', async(): Promise<void> => {
|
it('fires appropriate events according to the return value of source.setRepresentation.', async(): Promise<void> => {
|
||||||
const result = store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation);
|
const result = store.setRepresentation({ path: 'http://example.org/foo/bar' }, {} as Representation);
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
expect(changedCallback).toHaveBeenCalledTimes(1);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/1' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Update);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/2' });
|
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||||
|
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls deleteResource directly from the source.', async(): Promise<void> => {
|
it('calls deleteResource directly from the source.', async(): Promise<void> => {
|
||||||
await expect(store.deleteResource({ path: 'http://example.org/foo/bar' }))
|
await expect(store.deleteResource({ path: 'http://example.org/foo/bar' }))
|
||||||
.resolves.toEqual(modified);
|
.resolves.toEqual(deleteResourceReturnMock);
|
||||||
expect(source.deleteResource).toHaveBeenCalledTimes(1);
|
expect(source.deleteResource).toHaveBeenCalledTimes(1);
|
||||||
expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, undefined);
|
expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires all modified change events after deleteResource.', async(): Promise<void> => {
|
it('fires appropriate events according to the return value of source.deleteResource.', async(): Promise<void> => {
|
||||||
const result = store.deleteResource({ path: 'http://example.org/foo/bar' });
|
const result = store.deleteResource({ path: 'http://example.org/foo/bar' });
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
expect(changedCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/1' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/2' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Delete);
|
||||||
|
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||||
|
expect(deletedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deletedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls modifyResource directly from the source.', async(): Promise<void> => {
|
it('calls modifyResource directly from the source.', async(): Promise<void> => {
|
||||||
await expect(store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch))
|
await expect(store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch))
|
||||||
.resolves.toEqual(modified);
|
.resolves.toEqual(modifyResourceReturnMock);
|
||||||
expect(source.modifyResource).toHaveBeenCalledTimes(1);
|
expect(source.modifyResource).toHaveBeenCalledTimes(1);
|
||||||
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires all modified change events after modifyResource.', async(): Promise<void> => {
|
it('fires appropriate events according to the return value of source.modifyResource.', async(): Promise<void> => {
|
||||||
const result = store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
const result = store.modifyResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||||
await result;
|
await result;
|
||||||
expect(changedCallback).toHaveBeenCalledTimes(2);
|
expect(changedCallback).toHaveBeenCalledTimes(3);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/1' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Create);
|
||||||
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/modified/2' });
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update);
|
||||||
|
expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.Delete);
|
||||||
|
expect(createdCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' });
|
||||||
|
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' });
|
||||||
|
expect(deletedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deletedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls hasResource directly from the source.', async(): Promise<void> => {
|
it('calls hasResource directly from the source.', async(): Promise<void> => {
|
||||||
@ -111,4 +186,20 @@ describe('A MonitoringStore', (): void => {
|
|||||||
expect(source.hasResource).toHaveBeenCalledTimes(1);
|
expect(source.hasResource).toHaveBeenCalledTimes(1);
|
||||||
expect(source.hasResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' });
|
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' },
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await store.addResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
||||||
|
|
||||||
|
expect(changedCallback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||||
|
expect(updatedCallback).toHaveBeenCalledTimes(0);
|
||||||
|
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Patch } from '../../../../src/http/representation/Patch';
|
import type { Patch } from '../../../../src/http/representation/Patch';
|
||||||
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { PatchHandlerInput } from '../../../../src/storage/patch/PatchHandler';
|
import type { PatchHandlerInput } from '../../../../src/storage/patch/PatchHandler';
|
||||||
import type { RepresentationPatcher } from '../../../../src/storage/patch/RepresentationPatcher';
|
import type { RepresentationPatcher } from '../../../../src/storage/patch/RepresentationPatcher';
|
||||||
import { RepresentationPatchHandler } from '../../../../src/storage/patch/RepresentationPatchHandler';
|
import { RepresentationPatchHandler } from '../../../../src/storage/patch/RepresentationPatchHandler';
|
||||||
@ -20,7 +21,9 @@ describe('A RepresentationPatchHandler', (): void => {
|
|||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
source = {
|
source = {
|
||||||
getRepresentation: jest.fn().mockResolvedValue(representation),
|
getRepresentation: jest.fn().mockResolvedValue(representation),
|
||||||
setRepresentation: jest.fn().mockResolvedValue([ identifier ]),
|
setRepresentation: jest.fn().mockResolvedValue({
|
||||||
|
[identifier.path]: new RepresentationMetadata(identifier),
|
||||||
|
}),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
input = { source, identifier, patch };
|
input = { source, identifier, patch };
|
||||||
@ -33,7 +36,9 @@ describe('A RepresentationPatchHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls the patcher with the representation from the store.', async(): Promise<void> => {
|
it('calls the patcher with the representation from the store.', async(): Promise<void> => {
|
||||||
await expect(handler.handle(input)).resolves.toEqual([ identifier ]);
|
await expect(handler.handle(input)).resolves.toEqual({
|
||||||
|
[identifier.path]: new RepresentationMetadata(identifier),
|
||||||
|
});
|
||||||
|
|
||||||
expect(patcher.handleSafe).toHaveBeenCalledTimes(1);
|
expect(patcher.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(patcher.handleSafe).toHaveBeenLastCalledWith({ identifier, patch, representation });
|
expect(patcher.handleSafe).toHaveBeenLastCalledWith({ identifier, patch, representation });
|
||||||
@ -45,7 +50,9 @@ describe('A RepresentationPatchHandler', (): void => {
|
|||||||
it('calls the patcher with no representation if there is none.', async(): Promise<void> => {
|
it('calls the patcher with no representation if there is none.', async(): Promise<void> => {
|
||||||
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
||||||
|
|
||||||
await expect(handler.handle(input)).resolves.toEqual([ identifier ]);
|
await expect(handler.handle(input)).resolves.toEqual({
|
||||||
|
[identifier.path]: new RepresentationMetadata(identifier),
|
||||||
|
});
|
||||||
|
|
||||||
expect(patcher.handleSafe).toHaveBeenCalledTimes(1);
|
expect(patcher.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(patcher.handleSafe).toHaveBeenLastCalledWith({ identifier, patch });
|
expect(patcher.handleSafe).toHaveBeenLastCalledWith({ identifier, patch });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user