@solid/community-server
Version:
Community Solid Server: an open and modular implementation of the Solid specifications
618 lines • 33.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataAccessorBasedStore = void 0;
const node_crypto_1 = require("node:crypto");
const n3_1 = require("n3");
const global_logger_factory_1 = require("global-logger-factory");
const BasicRepresentation_1 = require("../http/representation/BasicRepresentation");
const RepresentationMetadata_1 = require("../http/representation/RepresentationMetadata");
const ContentTypes_1 = require("../util/ContentTypes");
const BadRequestHttpError_1 = require("../util/errors/BadRequestHttpError");
const ConflictHttpError_1 = require("../util/errors/ConflictHttpError");
const ErrorUtil_1 = require("../util/errors/ErrorUtil");
const ForbiddenHttpError_1 = require("../util/errors/ForbiddenHttpError");
const MethodNotAllowedHttpError_1 = require("../util/errors/MethodNotAllowedHttpError");
const NotFoundHttpError_1 = require("../util/errors/NotFoundHttpError");
const NotImplementedHttpError_1 = require("../util/errors/NotImplementedHttpError");
const PreconditionFailedHttpError_1 = require("../util/errors/PreconditionFailedHttpError");
const IterableUtil_1 = require("../util/IterableUtil");
const IdentifierMap_1 = require("../util/map/IdentifierMap");
const PathUtil_1 = require("../util/PathUtil");
const ResourceUtil_1 = require("../util/ResourceUtil");
const StreamUtil_1 = require("../util/StreamUtil");
const Vocabularies_1 = require("../util/Vocabularies");
/**
* ResourceStore which uses a DataAccessor for backend access.
*
* The DataAccessor interface provides elementary store operations such as read and write.
* This DataAccessorBasedStore uses those elementary store operations
* to implement the more high-level ResourceStore contact, abstracting all common functionality
* such that new stores can be added by implementing the more simple DataAccessor contract.
* DataAccessorBasedStore thereby provides behaviours for reuse across different stores, such as:
* * Converting container metadata to data
* * Converting slug to URI
* * Checking if addResource target is a container
* * Checking if no containment triples are written to a container
* * etc.
*
* Currently "metadata" is seen as something that is not directly accessible.
* That means that a consumer can't write directly to the metadata of a resource, only indirectly through headers.
* (Except for containers where data and metadata overlap).
*
* The one thing this store does not take care of (yet?) are containment triples for containers
*
* Work has been done to minimize the number of required calls to the DataAccessor,
* but the main disadvantage is that sometimes multiple calls are required where a specific store might only need one.
*/
class DataAccessorBasedStore {
logger = (0, global_logger_factory_1.getLoggerFor)(this);
accessor;
identifierStrategy;
auxiliaryStrategy;
metadataStrategy;
constructor(accessor, identifierStrategy, auxiliaryStrategy, metadataStrategy) {
this.accessor = accessor;
this.identifierStrategy = identifierStrategy;
this.auxiliaryStrategy = auxiliaryStrategy;
this.metadataStrategy = metadataStrategy;
}
async hasResource(identifier) {
try {
this.validateIdentifier(identifier);
if (this.metadataStrategy.isAuxiliaryIdentifier(identifier)) {
identifier = this.metadataStrategy.getSubjectIdentifier(identifier);
}
await this.accessor.getMetadata(identifier);
return true;
}
catch (error) {
if (NotFoundHttpError_1.NotFoundHttpError.isInstance(error)) {
return false;
}
throw error;
}
}
async getRepresentation(identifier) {
this.validateIdentifier(identifier);
let isMetadata = false;
if (this.metadataStrategy.isAuxiliaryIdentifier(identifier)) {
identifier = this.metadataStrategy.getSubjectIdentifier(identifier);
isMetadata = true;
}
// In the future we want to use getNormalizedMetadata and redirect in case the identifier differs
let metadata = await this.accessor.getMetadata(identifier);
let representation;
// Potentially add auxiliary related metadata
// Solid, §4.3: "Clients can discover auxiliary resources associated with a subject resource by making an HTTP HEAD
// or GET request on the target URL, and checking the HTTP Link header with the rel parameter"
// https://solid.github.io/specification/protocol#auxiliary-resources
await this.auxiliaryStrategy.addMetadata(metadata);
const isContainer = (0, PathUtil_1.isContainerPath)(metadata.identifier.value);
let data = metadata.quads();
if (isContainer || isMetadata) {
if (isContainer) {
// Add containment triples of non-auxiliary resources
for await (const child of this.accessor.getChildren(identifier)) {
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
if (!isMetadata) {
metadata.addQuads(child.quads());
}
metadata.add(Vocabularies_1.LDP.terms.contains, child.identifier, Vocabularies_1.SOLID_META.terms.ResponseMetadata);
}
}
data = metadata.quads();
if (isMetadata) {
metadata = new RepresentationMetadata_1.RepresentationMetadata(this.metadataStrategy.getAuxiliaryIdentifier(identifier));
}
}
metadata.addQuad(Vocabularies_1.DC.terms.namespace, Vocabularies_1.PREFERRED_PREFIX_TERM, 'dc', Vocabularies_1.SOLID_META.terms.ResponseMetadata);
metadata.addQuad(Vocabularies_1.LDP.terms.namespace, Vocabularies_1.PREFERRED_PREFIX_TERM, 'ldp', Vocabularies_1.SOLID_META.terms.ResponseMetadata);
metadata.addQuad(Vocabularies_1.POSIX.terms.namespace, Vocabularies_1.PREFERRED_PREFIX_TERM, 'posix', Vocabularies_1.SOLID_META.terms.ResponseMetadata);
metadata.addQuad(Vocabularies_1.XSD.terms.namespace, Vocabularies_1.PREFERRED_PREFIX_TERM, 'xsd', Vocabularies_1.SOLID_META.terms.ResponseMetadata);
}
if (isContainer) {
representation = new BasicRepresentation_1.BasicRepresentation(data, metadata, ContentTypes_1.INTERNAL_QUADS);
}
else if (isMetadata) {
representation = new BasicRepresentation_1.BasicRepresentation(metadata.quads(), this.metadataStrategy.getAuxiliaryIdentifier(identifier), ContentTypes_1.INTERNAL_QUADS);
}
else {
representation = new BasicRepresentation_1.BasicRepresentation(await this.accessor.getData(identifier), metadata);
}
return representation;
}
async addResource(container, representation, conditions) {
this.validateIdentifier(container);
const parentMetadata = await this.getSafeNormalizedMetadata(container);
// Solid, §5.3: "When a POST method request targets a resource without an existing representation,
// the server MUST respond with the 404 status code."
// https://solid.github.io/specification/protocol#writing-resources
if (!parentMetadata) {
throw new NotFoundHttpError_1.NotFoundHttpError();
}
// Not using `container` since `getSafeNormalizedMetadata` might return metadata for a different identifier.
// Solid, §5: "Servers MUST respond with the 405 status code to requests using HTTP methods
// that are not supported by the target resource."
// https://solid.github.io/specification/protocol#reading-writing-resources
if (!(0, PathUtil_1.isContainerPath)(parentMetadata.identifier.value)) {
throw new MethodNotAllowedHttpError_1.MethodNotAllowedHttpError(['POST'], 'The given path is not a container.');
}
this.validateConditions(conditions, parentMetadata);
// Solid, §5.1: "Servers MAY allow clients to suggest the URI of a resource created through POST,
// using the HTTP Slug header as defined in [RFC5023].
// Clients who want the server to assign a URI of a resource, MUST use the POST request."
// https://solid.github.io/specification/protocol#resource-type-heuristics
const newID = await this.createSafeUri(container, representation.metadata);
const isContainer = (0, PathUtil_1.isContainerIdentifier)(newID);
// Ensure the representation is supported by the accessor
// Containers are not checked because uploaded representations are treated as metadata
if (!isContainer) {
await this.accessor.canHandle(representation);
}
// Write the data. New containers should never be made for a POST request.
return this.writeData(newID, representation, isContainer, false, false);
}
async setRepresentation(identifier, representation, conditions) {
this.validateIdentifier(identifier);
// Check if the resource already exists
const oldMetadata = await this.getSafeNormalizedMetadata(identifier);
// We do not allow PUT on an already existing Container
// See https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1027#issuecomment-1023371546
if (oldMetadata && (0, PathUtil_1.isContainerIdentifier)(identifier)) {
throw new ConflictHttpError_1.ConflictHttpError('Existing containers cannot be updated via PUT.');
}
// Preserve the old metadata
if (oldMetadata && representation.metadata.has(Vocabularies_1.SOLID_META.terms.preserve, n3_1.DataFactory.namedNode(this.metadataStrategy.getAuxiliaryIdentifier(identifier).path))) {
// Preserve all the quads from the old metadata apart from the ContentType
oldMetadata.contentType = undefined;
const quads = oldMetadata.quads();
representation.metadata.addQuads(quads);
}
// Might want to redirect in the future.
// See #480
// Solid, §3.1: "If two URIs differ only in the trailing slash, and the server has associated a resource with
// one of them, then the other URI MUST NOT correspond to another resource. Instead, the server MAY respond to
// requests for the latter URI with a 301 redirect to the former."
// https://solid.github.io/specification/protocol#uri-slash-semantics
if (oldMetadata && oldMetadata.identifier.value !== identifier.path) {
throw new ConflictHttpError_1.ConflictHttpError(`${identifier.path} conflicts with existing path ${oldMetadata.identifier.value}`);
}
// Solid, §3.1: "Paths ending with a slash denote a container resource."
// https://solid.github.io/specification/protocol#uri-slash-semantics
const isContainer = (0, PathUtil_1.isContainerIdentifier)(identifier);
if (!isContainer && this.isContainerType(representation.metadata)) {
throw new BadRequestHttpError_1.BadRequestHttpError('Containers should have a `/` at the end of their path, resources should not.');
}
// Ensure the representation is supported by the accessor
// Metadata and containers are not checked since they get converted to RepresentationMetadata objects.
if (!isContainer && !this.metadataStrategy.isAuxiliaryIdentifier(identifier)) {
await this.accessor.canHandle(representation);
}
this.validateConditions(conditions, oldMetadata);
if (this.metadataStrategy.isAuxiliaryIdentifier(identifier)) {
return this.writeMetadata(identifier, representation);
}
// Potentially have to create containers if it didn't exist yet
return this.writeData(identifier, representation, isContainer, !oldMetadata, Boolean(oldMetadata));
}
async modifyResource(identifier, patch, conditions) {
if (conditions) {
let metadata;
try {
metadata = await this.accessor.getMetadata(identifier);
}
catch (error) {
if (!NotFoundHttpError_1.NotFoundHttpError.isInstance(error)) {
throw error;
}
}
this.validateConditions(conditions, metadata);
}
throw new NotImplementedHttpError_1.NotImplementedHttpError('Patches are not supported by the default store.');
}
async deleteResource(identifier, conditions) {
this.validateIdentifier(identifier);
// https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1027#issuecomment-988664970
// DELETE is not allowed on metadata
if (this.metadataStrategy.isAuxiliaryIdentifier(identifier)) {
throw new ConflictHttpError_1.ConflictHttpError('Not allowed to delete metadata resources directly.');
}
const metadata = await this.accessor.getMetadata(identifier);
// Solid, §5.4: "When a DELETE request targets storage’s root container or its associated ACL resource,
// the server MUST respond with the 405 status code."
// https://solid.github.io/specification/protocol#deleting-resources
if (this.isRootStorage(metadata)) {
throw new MethodNotAllowedHttpError_1.MethodNotAllowedHttpError(['DELETE'], 'Cannot delete a root storage container.');
}
if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) &&
this.auxiliaryStrategy.isRequiredInRoot(identifier)) {
const subjectIdentifier = this.auxiliaryStrategy.getSubjectIdentifier(identifier);
const parentMetadata = await this.accessor.getMetadata(subjectIdentifier);
if (this.isRootStorage(parentMetadata)) {
throw new MethodNotAllowedHttpError_1.MethodNotAllowedHttpError(['DELETE'], `Cannot delete ${identifier.path} from a root storage container.`);
}
}
// Solid, §5.4: "When a DELETE request is made to a container, the server MUST delete the container
// if it contains no resources. If the container contains resources,
// the server MUST respond with the 409 status code and response body describing the error."
// https://solid.github.io/specification/protocol#deleting-resources
// Auxiliary resources are not counted when deleting a container since they will also be deleted.
if ((0, PathUtil_1.isContainerIdentifier)(identifier) && await this.hasProperChildren(identifier)) {
throw new ConflictHttpError_1.ConflictHttpError('Can only delete empty containers.');
}
this.validateConditions(conditions, metadata);
// Solid, §5.4: "When a contained resource is deleted,
// the server MUST also delete the associated auxiliary resources"
// https://solid.github.io/specification/protocol#deleting-resources
const changes = new IdentifierMap_1.IdentifierMap();
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
const auxiliaries = this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier);
for (const deletedId of await this.safelyDeleteAuxiliaryResources(auxiliaries)) {
this.addActivityMetadata(changes, deletedId, Vocabularies_1.AS.terms.Delete);
}
}
if (!this.identifierStrategy.isRootContainer(identifier)) {
const container = this.identifierStrategy.getParentContainer(identifier);
this.addContainerActivity(changes, container, false, identifier);
// Update modified date of parent
await this.updateContainerModifiedDate(container);
}
await this.accessor.deleteResource(identifier);
this.addActivityMetadata(changes, identifier, Vocabularies_1.AS.terms.Delete);
return changes;
}
/**
* Verify if the given identifier matches the stored base.
*/
validateIdentifier(identifier) {
if (!this.identifierStrategy.supportsIdentifier(identifier)) {
throw new NotFoundHttpError_1.NotFoundHttpError();
}
}
/**
* Verify if the given metadata matches the conditions.
*/
validateConditions(conditions, metadata) {
// The 412 (Precondition Failed) status code indicates
// that one or more conditions given in the request header fields evaluated to false when tested on the server.
if (conditions && !conditions.matchesMetadata(metadata)) {
throw new PreconditionFailedHttpError_1.PreconditionFailedHttpError();
}
}
/**
* Returns the metadata matching the identifier, ignoring the presence of a trailing slash or not.
*
* Solid, §3.1: "If two URIs differ only in the trailing slash,
* and the server has associated a resource with one of them,
* then the other URI MUST NOT correspond to another resource."
* https://solid.github.io/specification/protocol#uri-slash-semantics
*
* First the identifier gets requested. If no result is found,
* the identifier with differing trailing slash is requested.
*
* @param identifier - Identifier that needs to be checked.
*/
async getNormalizedMetadata(identifier) {
const hasSlash = (0, PathUtil_1.isContainerIdentifier)(identifier);
try {
return await this.accessor.getMetadata(identifier);
}
catch (error) {
if (NotFoundHttpError_1.NotFoundHttpError.isInstance(error)) {
const otherIdentifier = { path: hasSlash ? (0, PathUtil_1.trimTrailingSlashes)(identifier.path) : (0, PathUtil_1.ensureTrailingSlash)(identifier.path) };
// Only try to access other identifier if it is valid in the scope of the DataAccessor
this.validateIdentifier(otherIdentifier);
return this.accessor.getMetadata(otherIdentifier);
}
throw error;
}
}
/**
* Returns the result of `getNormalizedMetadata` or undefined if a 404 error is thrown.
*/
async getSafeNormalizedMetadata(identifier) {
try {
return await this.getNormalizedMetadata(identifier);
}
catch (error) {
if (!NotFoundHttpError_1.NotFoundHttpError.isInstance(error)) {
throw error;
}
}
}
/**
* Write the given metadata resource to the DataAccessor.
*
* @param identifier - Identifier of the metadata.
* @param representation - Corresponding Representation.
*
* @returns Identifiers of resources that were possibly modified.
*/
async writeMetadata(identifier, representation) {
const subjectIdentifier = this.metadataStrategy.getSubjectIdentifier(identifier);
// Cannot create metadata without a corresponding resource
if (!await this.hasResource(subjectIdentifier)) {
throw new ConflictHttpError_1.ConflictHttpError('Metadata resources can not be created directly.');
}
// https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1027#issuecomment-988664970
// It must not be possible to create .meta.meta resources
if (this.metadataStrategy.isAuxiliaryIdentifier(subjectIdentifier)) {
throw new ConflictHttpError_1.ConflictHttpError('Not allowed to create metadata resources on a metadata resource.');
}
const changes = new IdentifierMap_1.IdentifierMap();
// Transform representation data to quads and add them to the metadata object
const metadata = new RepresentationMetadata_1.RepresentationMetadata(subjectIdentifier);
const quads = await (0, StreamUtil_1.arrayifyStream)(representation.data);
metadata.addQuads(quads);
// Add date modified metadata
(0, ResourceUtil_1.updateModifiedDate)(metadata);
// Remove the response metadata as this must not be stored
this.removeResponseMetadata(metadata);
await this.accessor.writeMetadata(subjectIdentifier, metadata);
this.addActivityMetadata(changes, subjectIdentifier, Vocabularies_1.AS.terms.Update);
return changes;
}
/**
* Write the given resource to the DataAccessor. Metadata will be updated with necessary triples.
* For containers, `handleContainerData` will be used to verify the data.
*
* @param identifier - Identifier of the resource.
* @param representation - Corresponding Representation.
* @param isContainer - Is the incoming resource a container?
* @param createContainers - Should parent containers (potentially) be created?
* @param exists - If the resource already exists.
*
* @returns Identifiers of resources that were possibly modified.
*/
async writeData(identifier, representation, isContainer, createContainers, exists) {
// 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
representation.metadata.identifier = n3_1.DataFactory.namedNode(identifier.path);
(0, ResourceUtil_1.addResourceMetadata)(representation.metadata, isContainer);
// Validate container data
if (isContainer) {
await this.handleContainerData(representation);
}
// Validate auxiliary data
if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
await this.auxiliaryStrategy.validate(representation);
}
// Add date modified metadata
(0, ResourceUtil_1.updateModifiedDate)(representation.metadata);
// Root container should not have a parent container
// Solid, §5.3: "Servers MUST create intermediate containers and include corresponding containment triples
// in container representations derived from the URI path component of PUT and PATCH requests."
// https://solid.github.io/specification/protocol#writing-resources
let changes = new IdentifierMap_1.IdentifierMap();
if (!this.identifierStrategy.isRootContainer(identifier) && !exists) {
const parent = this.identifierStrategy.getParentContainer(identifier);
if (createContainers) {
changes = await this.createRecursiveContainers(parent);
}
// No changes means the parent container exists and will be updated
if (changes.size === 0) {
this.addContainerActivity(changes, parent, true, identifier);
}
// Parent container is also modified
await this.updateContainerModifiedDate(parent);
}
// Remove all generated metadata to prevent it from being stored permanently
this.removeResponseMetadata(representation.metadata);
await (isContainer ?
this.accessor.writeContainer(identifier, representation.metadata) :
this.accessor.writeDocument(identifier, representation.data, representation.metadata));
this.addActivityMetadata(changes, identifier, exists ? Vocabularies_1.AS.terms.Update : Vocabularies_1.AS.terms.Create);
return changes;
}
/**
* Warns when the representation has data and removes the content-type from the metadata.
*
* @param representation - Container representation.
*/
async handleContainerData(representation) {
// https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1027#issuecomment-1022214820
// Make it not possible via PUT to add metadata during the creation of a container
// Thus the contents are ignored and a warning is sent
if (!representation.isEmpty) {
this.logger.warn('The contents of the body are ignored when creating a container.');
}
// Input content type doesn't matter anymore
representation.metadata.removeAll(Vocabularies_1.CONTENT_TYPE_TERM);
}
/**
* Removes all generated data from metadata to prevent it from being stored permanently.
*/
removeResponseMetadata(metadata) {
metadata.removeQuads(metadata.quads(null, null, null, Vocabularies_1.SOLID_META.terms.ResponseMetadata));
}
/**
* Updates the last modified date of the given container
*/
async updateContainerModifiedDate(container) {
const parentMetadata = await this.accessor.getMetadata(container);
(0, ResourceUtil_1.updateModifiedDate)(parentMetadata);
this.removeResponseMetadata(parentMetadata);
await this.accessor.writeContainer(container, parentMetadata);
}
/**
* Generates a new URI for a resource in the given container, potentially using the given slug.
*
* Solid, §5.3: "Servers MUST allow creating new resources with a POST request to URI path ending `/`.
* Servers MUST create a resource with URI path ending `/{id}` in container `/`.
* Servers MUST create a container with URI path ending `/{id}/` in container `/` for requests
* including the HTTP Link header with rel="type" targeting a valid LDP container type."
* https://solid.github.io/specification/protocol#writing-resources
*
* @param container - Parent container of the new URI.
* @param isContainer - Does the new URI represent a container?
* @param slug - Slug to use for the new URI.
*/
createURI(container, isContainer, slug) {
this.validateSlug(isContainer, slug);
const base = (0, PathUtil_1.ensureTrailingSlash)(container.path);
const name = (slug && this.cleanSlug(slug)) ?? (0, node_crypto_1.randomUUID)();
const suffix = isContainer ? '/' : '';
return { path: `${base}${name}${suffix}` };
}
/**
* Validates if the slug and headers are valid.
* Errors if slug exists, ends on slash, but ContainerType Link header is NOT present
*
* @param isContainer - Is the slug supposed to represent a container?
* @param slug - Is the requested slug (if any).
*/
validateSlug(isContainer, slug) {
if (slug && (0, PathUtil_1.isContainerPath)(slug) && !isContainer) {
throw new BadRequestHttpError_1.BadRequestHttpError('Only slugs used to create containers can end with a `/`.');
}
}
/**
* Clean http Slug to be compatible with the server. Makes sure there are no unwanted characters,
* e.g., cleanslug('&%26') returns '%26%26'
*
* @param slug - the slug to clean
*/
cleanSlug(slug) {
if (/\/[^/]/u.test(slug)) {
throw new BadRequestHttpError_1.BadRequestHttpError('Slugs should not contain slashes');
}
return (0, PathUtil_1.toCanonicalUriPath)((0, PathUtil_1.trimTrailingSlashes)(slug));
}
/**
* Generate a valid URI to store a new Resource in the given container.
* URI will be based on the slug header if there is one and is guaranteed to not exist yet.
*
* @param container - Identifier of the target container.
* @param metadata - Metadata of the new resource.
*/
async createSafeUri(container, metadata) {
// Get all values needed for naming the resource
const isContainer = this.isContainerType(metadata);
const slug = metadata.get(Vocabularies_1.SOLID_HTTP.terms.slug)?.value;
metadata.removeAll(Vocabularies_1.SOLID_HTTP.terms.slug);
let newID = this.createURI(container, isContainer, slug);
// Solid, §5.3: "When a POST method request with the Slug header targets an auxiliary resource,
// the server MUST respond with the 403 status code and response body describing the error."
// https://solid.github.io/specification/protocol#writing-resources
if (this.auxiliaryStrategy.isAuxiliaryIdentifier(newID)) {
throw new ForbiddenHttpError_1.ForbiddenHttpError('Slug bodies that would result in an auxiliary resource are forbidden');
}
// Make sure we don't already have a resource with this exact name (or with differing trailing slash)
const withSlash = { path: (0, PathUtil_1.ensureTrailingSlash)(newID.path) };
const withoutSlash = { path: (0, PathUtil_1.trimTrailingSlashes)(newID.path) };
if (await this.hasResource(withSlash) || await this.hasResource(withoutSlash)) {
newID = this.createURI(container, isContainer);
}
return newID;
}
/**
* Checks whether the given metadata represents a (potential) container,
* based on the metadata.
*
* @param metadata - Metadata of the (new) resource.
*/
isContainerType(metadata) {
return this.hasContainerType(metadata.getAll(Vocabularies_1.RDF.terms.type));
}
/**
* Checks in a list of types if any of them match a Container type.
*/
hasContainerType(rdfTypes) {
return rdfTypes.some((type) => type.value === Vocabularies_1.LDP.Container || type.value === Vocabularies_1.LDP.BasicContainer);
}
/**
* Verifies if this is the metadata of a root storage container.
*/
isRootStorage(metadata) {
return metadata.getAll(Vocabularies_1.RDF.terms.type).some((term) => term.value === Vocabularies_1.PIM.Storage);
}
/**
* Checks if the given container has any non-auxiliary resources.
*/
async hasProperChildren(container) {
for await (const child of this.accessor.getChildren(container)) {
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
return true;
}
}
return false;
}
/**
* Deletes the given array of auxiliary identifiers.
* Does not throw an error if something goes wrong.
*/
async safelyDeleteAuxiliaryResources(identifiers) {
const deleted = [];
await Promise.all(identifiers.map(async (identifier) => {
try {
await this.accessor.deleteResource(identifier);
deleted.push(identifier);
}
catch (error) {
if (!NotFoundHttpError_1.NotFoundHttpError.isInstance(error)) {
this.logger.error(`Error deleting auxiliary resource ${identifier.path}: ${(0, ErrorUtil_1.createErrorMessage)(error)}`);
}
}
}));
return deleted;
}
/**
* Create containers starting from the root until the given identifier corresponds to an existing container.
* 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.
*/
async createRecursiveContainers(container) {
// Verify whether the container already exists
try {
const metadata = await this.getNormalizedMetadata(container);
// See https://github.com/CommunitySolidServer/CommunitySolidServer/issues/480
// Solid, §3.1: "If two URIs differ only in the trailing slash, and the server has associated a resource with
// one of them, then the other URI MUST NOT correspond to another resource. Instead, the server MAY respond to
// requests for the latter URI with a 301 redirect to the former."
// https://solid.github.io/specification/protocol#uri-slash-semantics
if (!(0, PathUtil_1.isContainerPath)(metadata.identifier.value)) {
throw new ForbiddenHttpError_1.ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`);
}
return new IdentifierMap_1.IdentifierMap();
}
catch (error) {
if (!NotFoundHttpError_1.NotFoundHttpError.isInstance(error)) {
throw error;
}
}
// Create the container, starting with its parent
const ancestors = this.identifierStrategy.isRootContainer(container) ?
new IdentifierMap_1.IdentifierMap() :
await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
const changes = await this.writeData(container, new BasicRepresentation_1.BasicRepresentation([], container), true, false, false);
return new IdentifierMap_1.IdentifierMap((0, IterableUtil_1.concat)([changes, ancestors]));
}
/**
* Generates activity metadata for a resource and adds it to the {@link ChangeMap}
*
* @param map - ChangeMap to update.
* @param id - Identifier of the resource being changed.
* @param activity - Which activity is taking place.
*/
addActivityMetadata(map, id, activity) {
map.set(id, new RepresentationMetadata_1.RepresentationMetadata(id, { [Vocabularies_1.SOLID_AS.activity]: activity }));
}
/**
* Generates activity metadata specifically for Add/Remove events on a container.
*
* @param map - ChangeMap to update.
* @param id - Identifier of the container.
* @param add - If there is a resource being added (`true`) or removed (`false`).
* @param object - The object that is being added/removed.
*/
addContainerActivity(map, id, add, object) {
const metadata = new RepresentationMetadata_1.RepresentationMetadata({
[Vocabularies_1.SOLID_AS.activity]: add ? Vocabularies_1.AS.terms.Add : Vocabularies_1.AS.terms.Remove,
[Vocabularies_1.AS.object]: n3_1.DataFactory.namedNode(object.path),
});
map.set(id, metadata);
}
}
exports.DataAccessorBasedStore = DataAccessorBasedStore;
//# sourceMappingURL=DataAccessorBasedStore.js.map