-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Initialize root containers with RootContainerInitializer
- Loading branch information
Showing
5 changed files
with
128 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { DataFactory } from 'n3'; | ||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; | ||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
import { getLoggerFor } from '../logging/LogUtil'; | ||
import type { ResourceStore } from '../storage/ResourceStore'; | ||
import { TEXT_TURTLE } from '../util/ContentTypes'; | ||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; | ||
import { ensureTrailingSlash } from '../util/PathUtil'; | ||
import { generateResourceQuads } from '../util/ResourceUtil'; | ||
import { guardedStreamFrom } from '../util/StreamUtil'; | ||
import { PIM, RDF } from '../util/UriConstants'; | ||
import { toNamedNode } from '../util/UriUtil'; | ||
import { Initializer } from './Initializer'; | ||
import namedNode = DataFactory.namedNode; | ||
|
||
/** | ||
* Initializes ResourceStores by creating a root container if it didn't exist yet. | ||
*/ | ||
export class RootContainerInitializer extends Initializer { | ||
protected readonly logger = getLoggerFor(this); | ||
private readonly baseId: ResourceIdentifier; | ||
private readonly store: ResourceStore; | ||
|
||
public constructor(baseUrl: string, store: ResourceStore) { | ||
super(); | ||
this.baseId = { path: ensureTrailingSlash(baseUrl) }; | ||
this.store = store; | ||
} | ||
|
||
public async handle(): Promise<void> { | ||
if (!await this.hasRootContainer()) { | ||
await this.createRootContainer(); | ||
} | ||
} | ||
|
||
/** | ||
* Verify if a root container already exists in a ResourceStore. | ||
*/ | ||
protected async hasRootContainer(): Promise<boolean> { | ||
try { | ||
const result = await this.store.getRepresentation(this.baseId, {}); | ||
this.logger.debug(`Existing root container found at ${this.baseId.path}`); | ||
result.data.destroy(); | ||
return true; | ||
} catch (error: unknown) { | ||
if (!(error instanceof NotFoundHttpError)) { | ||
throw error; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Create a root container in a ResourceStore. | ||
*/ | ||
protected async createRootContainer(): Promise<void> { | ||
const metadata = new RepresentationMetadata(this.baseId); | ||
metadata.addQuads(generateResourceQuads(namedNode(this.baseId.path), true)); | ||
|
||
// Make sure the root container is a pim:Storage | ||
// This prevents deletion of the root container as storage root containers can not be deleted | ||
metadata.add(RDF.type, toNamedNode(PIM.Storage)); | ||
|
||
metadata.contentType = TEXT_TURTLE; | ||
|
||
await this.store.setRepresentation(this.baseId, { | ||
binary: true, | ||
data: guardedStreamFrom([]), | ||
metadata, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { RootContainerInitializer } from '../../../src/init/RootContainerInitializer'; | ||
import type { ResourceStore } from '../../../src/storage/ResourceStore'; | ||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; | ||
|
||
describe('A RootContainerInitializer', (): void => { | ||
const baseUrl = 'http://test.com/'; | ||
const store: jest.Mocked<ResourceStore> = { | ||
getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), | ||
setRepresentation: jest.fn(), | ||
} as any; | ||
const initializer = new RootContainerInitializer(baseUrl, store); | ||
|
||
afterEach((): void => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('invokes ResourceStore initialization.', async(): Promise<void> => { | ||
await initializer.handle(); | ||
|
||
expect(store.getRepresentation).toHaveBeenCalledTimes(1); | ||
expect(store.getRepresentation).toHaveBeenCalledWith({ path: baseUrl }, {}); | ||
expect(store.setRepresentation).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('does not invoke ResourceStore initialization when a root container already exists.', async(): Promise<void> => { | ||
store.getRepresentation.mockReturnValueOnce(Promise.resolve({ | ||
data: { destroy: jest.fn() }, | ||
} as any)); | ||
|
||
await initializer.handle(); | ||
|
||
expect(store.getRepresentation).toHaveBeenCalledTimes(1); | ||
expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/' }, {}); | ||
expect(store.setRepresentation).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('errors when the store errors writing the root container.', async(): Promise<void> => { | ||
store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); | ||
await expect(initializer.handle()).rejects.toThrow('Fatal'); | ||
}); | ||
}); |