UNPKG

@solid/community-server

Version:

Community Solid Server: an open and modular implementation of the Solid specifications

618 lines 33.8 kB
"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