diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts index 18199ee18..69cc24da6 100644 --- a/src/init/AclInitializer.ts +++ b/src/init/AclInitializer.ts @@ -66,7 +66,7 @@ export class AclInitializer extends Initializer { acl:mode acl:Control; acl:accessTo <${this.baseUrl}>; acl:default <${this.baseUrl}>.`; - const metadata = new RepresentationMetadata(rootAcl.path, { [CONTENT_TYPE]: TEXT_TURTLE }); + const metadata = new RepresentationMetadata(rootAcl, { [CONTENT_TYPE]: TEXT_TURTLE }); this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); await this.store.setRepresentation( rootAcl, diff --git a/src/ldp/representation/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts index 058c138ff..bb6d8a529 100644 --- a/src/ldp/representation/RepresentationMetadata.ts +++ b/src/ldp/representation/RepresentationMetadata.ts @@ -2,7 +2,10 @@ import { DataFactory, Store } from 'n3'; import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js'; import { getLoggerFor } from '../../logging/LogUtil'; import { toObjectTerm, toNamedNode, isTerm } from '../../util/UriUtil'; +import type { ResourceIdentifier } from './ResourceIdentifier'; +import { isResourceIdentifier } from './ResourceIdentifier'; +export type MetadataIdentifier = ResourceIdentifier | NamedNode | BlankNode; export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode | Literal | string)[]; /** @@ -23,7 +26,7 @@ export class RepresentationMetadata { * * `@ignored` tag is necessary for Components-Generator.js */ - public constructor(identifier?: NamedNode | BlankNode | string, overrides?: Record); + public constructor(identifier?: MetadataIdentifier, overrides?: Record); /** * @param metadata - Starts as a copy of the input metadata. @@ -38,12 +41,12 @@ export class RepresentationMetadata { public constructor(overrides?: Record); public constructor( - input?: NamedNode | BlankNode | string | RepresentationMetadata | Record, + input?: MetadataIdentifier | RepresentationMetadata | Record, overrides?: Record, ) { this.store = new Store(); - if (typeof input === 'string') { - this.id = DataFactory.namedNode(input); + if (isResourceIdentifier(input)) { + this.id = DataFactory.namedNode(input.path); } else if (isTerm(input)) { this.id = input; } else if (input instanceof RepresentationMetadata) { diff --git a/src/ldp/representation/ResourceIdentifier.ts b/src/ldp/representation/ResourceIdentifier.ts index cb8cf16ac..ff0c7b9a4 100644 --- a/src/ldp/representation/ResourceIdentifier.ts +++ b/src/ldp/representation/ResourceIdentifier.ts @@ -7,3 +7,10 @@ export interface ResourceIdentifier { */ path: string; } + +/** + * Determines whether the object is a `ResourceIdentifier`. + */ +export const isResourceIdentifier = function(object: any): object is ResourceIdentifier { + return object && (typeof object.path === 'string'); +}; diff --git a/src/pods/generate/TemplatedResourcesGenerator.ts b/src/pods/generate/TemplatedResourcesGenerator.ts index c3c0ae8d0..ae16fd218 100644 --- a/src/pods/generate/TemplatedResourcesGenerator.ts +++ b/src/pods/generate/TemplatedResourcesGenerator.ts @@ -51,7 +51,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator { representation: { binary: true, data: guardedStreamFrom([]), - metadata: new RepresentationMetadata(link.identifier.path), + metadata: new RepresentationMetadata(link.identifier), }, }; @@ -74,7 +74,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator { private async generateDocument(filePath: string, mapper: FileIdentifierMapper, options: Dict): Promise { const link = await mapper.mapFilePathToUrl(filePath, false); - const metadata = new RepresentationMetadata(link.identifier.path); + const metadata = new RepresentationMetadata(link.identifier); metadata.contentType = link.contentType; const raw = await fsPromises.readFile(filePath, 'utf8'); diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 04a3a7382..48611d4ce 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -370,7 +370,7 @@ export class DataAccessorBasedStore implements ResourceStore { return { binary: true, data: guardedStreamFrom([]), - metadata: new RepresentationMetadata(container.path), + metadata: new RepresentationMetadata(container), }; } } diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts index ea994347d..9bd71978d 100644 --- a/src/storage/accessors/FileDataAccessor.ts +++ b/src/storage/accessors/FileDataAccessor.ts @@ -246,7 +246,7 @@ export class FileDataAccessor implements DataAccessor { */ private async getBaseMetadata(link: ResourceLink, stats: Stats, isContainer: boolean): Promise { - const metadata = new RepresentationMetadata(link.identifier.path) + const metadata = new RepresentationMetadata(link.identifier) .addQuads(await this.getRawMetadata(link.identifier)); metadata.addQuads(generateResourceQuads(metadata.identifier as NamedNode, isContainer)); metadata.addQuads(this.generatePosixQuads(metadata.identifier as NamedNode, stats)); diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts index bf7a9998f..33c38aba6 100644 --- a/src/storage/accessors/InMemoryDataAccessor.ts +++ b/src/storage/accessors/InMemoryDataAccessor.ts @@ -28,7 +28,7 @@ export class InMemoryDataAccessor implements DataAccessor { public constructor(base: string) { this.base = ensureTrailingSlash(base); - const metadata = new RepresentationMetadata(this.base); + const metadata = new RepresentationMetadata({ path: this.base }); metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true)); this.store = { entries: {}, metadata }; } diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts index dc6a7904a..1819e4190 100644 --- a/src/storage/accessors/SparqlDataAccessor.ts +++ b/src/storage/accessors/SparqlDataAccessor.ts @@ -94,7 +94,7 @@ export class SparqlDataAccessor implements DataAccessor { throw new NotFoundHttpError(); } - const metadata = new RepresentationMetadata(identifier.path).addQuads(quads); + const metadata = new RepresentationMetadata(identifier).addQuads(quads); if (!isContainerIdentifier(identifier)) { metadata.contentType = INTERNAL_QUADS; } diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index 3fecf133a..39df46886 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -108,7 +108,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler { this.logger.debug(`${store.size} quads will be stored to ${identifier.path}.`); // Write the result - const metadata = new RepresentationMetadata(identifier.path, { [CONTENT_TYPE]: INTERNAL_QUADS }); + const metadata = new RepresentationMetadata(identifier, { [CONTENT_TYPE]: INTERNAL_QUADS }); const representation: Representation = { binary: false, data: guardStream(store.match() as Readable), diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts index 83d82f297..03d53a6b6 100644 --- a/test/unit/ldp/representation/RepresentationMetadata.test.ts +++ b/test/unit/ldp/representation/RepresentationMetadata.test.ts @@ -28,12 +28,12 @@ describe('A RepresentationMetadata', (): void => { }); it('converts identifier strings to named nodes.', async(): Promise => { - metadata = new RepresentationMetadata('identifier'); + metadata = new RepresentationMetadata({ path: 'identifier' }); expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier')); }); it('copies an other metadata object.', async(): Promise => { - const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' }); + const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' }); metadata = new RepresentationMetadata(other); expect(metadata.identifier).toEqualRdfTerm(namedNode('otherId')); expect(metadata.quads()).toBeRdfIsomorphic([ @@ -59,7 +59,7 @@ describe('A RepresentationMetadata', (): void => { }); it('can combine overrides with other metadata.', async(): Promise => { - const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' }); + const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' }); metadata = new RepresentationMetadata(other, { 'test:pred': 'objVal2' }); expect(metadata.quads()).toBeRdfIsomorphic([ quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal2')) ]); @@ -101,7 +101,7 @@ describe('A RepresentationMetadata', (): void => { }); it('updates its identifier when copying metadata.', async(): Promise => { - const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' }); + const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' }); metadata.setMetadata(other); // `setMetadata` should have the same result as the following diff --git a/test/unit/storage/accessors/FileDataAccessor.test.ts b/test/unit/storage/accessors/FileDataAccessor.test.ts index 46c609871..4e87a6aaa 100644 --- a/test/unit/storage/accessors/FileDataAccessor.test.ts +++ b/test/unit/storage/accessors/FileDataAccessor.test.ts @@ -119,7 +119,7 @@ describe('A FileDataAccessor', (): void => { const childQuads = metadata.quads().filter((quad): boolean => quad.subject.value === `${base}container/resource`); - const childMetadata = new RepresentationMetadata(`${base}container/resource`).addQuads(childQuads); + const childMetadata = new RepresentationMetadata({ path: `${base}container/resource` }).addQuads(childQuads); expect(childMetadata.get(RDF.type)?.value).toBe(LDP.Resource); expect(childMetadata.get(POSIX.size)).toEqualRdfTerm(toTypedLiteral('data'.length, XSD.integer)); expect(childMetadata.get(DCTERMS.modified)).toEqualRdfTerm(toTypedLiteral(now.toISOString(), XSD.dateTime)); @@ -160,7 +160,8 @@ describe('A FileDataAccessor', (): void => { }); it('writes metadata to the corresponding metadata file.', async(): Promise => { - metadata = new RepresentationMetadata(`${base}res.ttl`, { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' }); + metadata = new RepresentationMetadata({ path: `${base}res.ttl` }, + { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' }); await expect(accessor.writeDocument({ path: `${base}res.ttl` }, data, metadata)).resolves.toBeUndefined(); expect(cache.data['res.ttl']).toBe('data'); expect(cache.data['res.ttl.meta']).toMatch(`<${base}res.ttl> "apples".`); @@ -266,21 +267,21 @@ describe('A FileDataAccessor', (): void => { }); it('writes metadata to the corresponding metadata file.', async(): Promise => { - metadata = new RepresentationMetadata(`${base}container/`, { likes: 'apples' }); + metadata = new RepresentationMetadata({ path: `${base}container/` }, { likes: 'apples' }); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined(); expect(cache.data.container).toEqual({ '.meta': expect.stringMatching(`<${base}container/> "apples".`) }); }); it('overwrites existing metadata.', async(): Promise => { cache.data.container = { '.meta': `<${base}container/> "pears".` }; - metadata = new RepresentationMetadata(`${base}container/`, { likes: 'apples' }); + metadata = new RepresentationMetadata({ path: `${base}container/` }, { likes: 'apples' }); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined(); expect(cache.data.container).toEqual({ '.meta': expect.stringMatching(`<${base}container/> "apples".`) }); }); it('does not write metadata that is stored by the file system.', async(): Promise => { metadata = new RepresentationMetadata( - `${base}container/`, + { path: `${base}container/` }, { [RDF.type]: [ toNamedNode(LDP.BasicContainer), toNamedNode(LDP.Resource) ]}, ); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined(); diff --git a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts index 588a8fda2..cfecdf0e3 100644 --- a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts +++ b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts @@ -86,9 +86,10 @@ describe('An InMemoryDataAccessor', (): void => { }); it('adds stored metadata when requesting document metadata.', async(): Promise => { - const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) }); - await accessor.writeDocument({ path: `${base}resource` }, data, inputMetadata); - metadata = await accessor.getMetadata({ path: `${base}resource` }); + const identifier = { path: `${base}resource` }; + const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Resource) }); + await accessor.writeDocument(identifier, data, inputMetadata); + metadata = await accessor.getMetadata(identifier); expect(metadata.identifier.value).toBe(`${base}resource`); const quads = metadata.quads(); expect(quads).toHaveLength(1); @@ -96,10 +97,11 @@ describe('An InMemoryDataAccessor', (): void => { }); it('adds stored metadata when requesting container metadata.', async(): Promise => { - const inputMetadata = new RepresentationMetadata(`${base}container/`, { [RDF.type]: toNamedNode(LDP.Container) }); - await accessor.writeContainer({ path: `${base}container/` }, inputMetadata); + const identifier = { path: `${base}container/` }; + const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) }); + await accessor.writeContainer(identifier, inputMetadata); - metadata = await accessor.getMetadata({ path: `${base}container/` }); + metadata = await accessor.getMetadata(identifier); expect(metadata.identifier.value).toBe(`${base}container/`); const quads = metadata.quads(); expect(quads).toHaveLength(1); @@ -107,8 +109,9 @@ describe('An InMemoryDataAccessor', (): void => { }); it('can overwrite the metadata of an existing container without overwriting children.', async(): Promise => { - const inputMetadata = new RepresentationMetadata(`${base}container/`, { [RDF.type]: toNamedNode(LDP.Container) }); - await accessor.writeContainer({ path: `${base}container/` }, inputMetadata); + const identifier = { path: `${base}container/` }; + const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) }); + await accessor.writeContainer(identifier, inputMetadata); const resourceMetadata = new RepresentationMetadata(); await accessor.writeDocument( { path: `${base}container/resource` }, data, resourceMetadata, @@ -116,9 +119,9 @@ describe('An InMemoryDataAccessor', (): void => { const newMetadata = new RepresentationMetadata(inputMetadata); newMetadata.add(RDF.type, toNamedNode(LDP.BasicContainer)); - await accessor.writeContainer({ path: `${base}container/` }, newMetadata); + await accessor.writeContainer(identifier, newMetadata); - metadata = await accessor.getMetadata({ path: `${base}container/` }); + metadata = await accessor.getMetadata(identifier); expect(metadata.identifier.value).toBe(`${base}container/`); const quads = metadata.quads(); expect(quads).toHaveLength(3); diff --git a/test/unit/storage/accessors/SparqlDataAccessor.test.ts b/test/unit/storage/accessors/SparqlDataAccessor.test.ts index 097936775..ceaccda88 100644 --- a/test/unit/storage/accessors/SparqlDataAccessor.test.ts +++ b/test/unit/storage/accessors/SparqlDataAccessor.test.ts @@ -152,7 +152,7 @@ describe('A SparqlDataAccessor', (): void => { }); it('overwrites the metadata when writing a container and updates parent.', async(): Promise => { - metadata = new RepresentationMetadata('http://test.com/container/', + metadata = new RepresentationMetadata({ path: 'http://test.com/container/' }, { [RDF.type]: [ toNamedNode(LDP.Resource), toNamedNode(LDP.Container) ]}); await expect(accessor.writeContainer({ path: 'http://test.com/container/' }, metadata)).resolves.toBeUndefined(); @@ -171,7 +171,7 @@ describe('A SparqlDataAccessor', (): void => { }); it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise => { - metadata = new RepresentationMetadata('http://test.com/container/resource', + metadata = new RepresentationMetadata({ path: 'http://test.com/container/resource' }, { [RDF.type]: [ toNamedNode(LDP.Resource) ]}); await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata)) .resolves.toBeUndefined(); @@ -190,7 +190,7 @@ describe('A SparqlDataAccessor', (): void => { }); it('overwrites the data and metadata when writing an empty resource.', async(): Promise => { - metadata = new RepresentationMetadata('http://test.com/container/resource', + metadata = new RepresentationMetadata({ path: 'http://test.com/container/resource' }, { [RDF.type]: [ toNamedNode(LDP.Resource) ]}); const empty = guardedStreamFrom([]); await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, empty, metadata)) @@ -209,7 +209,7 @@ describe('A SparqlDataAccessor', (): void => { }); it('removes all references when deleting a resource.', async(): Promise => { - metadata = new RepresentationMetadata('http://test.com/container/', + metadata = new RepresentationMetadata({ path: 'http://test.com/container/' }, { [RDF.type]: [ toNamedNode(LDP.Resource), toNamedNode(LDP.Container) ]}); await expect(accessor.deleteResource({ path: 'http://test.com/container/' })).resolves.toBeUndefined(); @@ -246,14 +246,14 @@ describe('A SparqlDataAccessor', (): void => { }); it('errors when the SPARQL endpoint fails during writing.', async(): Promise => { - const path = 'http://test.com/container/'; - metadata = new RepresentationMetadata(path); + const identifier = { path: 'http://test.com/container/' }; + metadata = new RepresentationMetadata(identifier); updateError = 'error'; - await expect(accessor.writeContainer({ path }, metadata)).rejects.toBe(updateError); + await expect(accessor.writeContainer(identifier, metadata)).rejects.toBe(updateError); updateError = new Error(); - await expect(accessor.writeContainer({ path }, metadata)).rejects.toThrow(updateError); + await expect(accessor.writeContainer(identifier, metadata)).rejects.toThrow(updateError); updateError = undefined; });