From 88d008e36fb573bc7edb29dc565d022be19551e8 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 25 Feb 2021 17:03:26 +0100 Subject: [PATCH] feat: Create pod manager for generating dynamic pods --- src/index.ts | 3 ++ src/pods/ConfigPodManager.ts | 58 +++++++++++++++++++++ src/pods/GeneratedPodManager.ts | 8 +-- src/pods/generate/GenerateUtil.ts | 24 +++++++++ src/pods/generate/PodGenerator.ts | 21 ++++++++ test/unit/pods/ConfigPodManager.test.ts | 67 +++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 src/pods/ConfigPodManager.ts create mode 100644 src/pods/generate/GenerateUtil.ts create mode 100644 src/pods/generate/PodGenerator.ts create mode 100644 test/unit/pods/ConfigPodManager.test.ts diff --git a/src/index.ts b/src/index.ts index 5ac0fdaf3..3c0be75cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,8 +109,10 @@ export * from './logging/VoidLoggerFactory'; export * from './logging/WinstonLoggerFactory'; // Pods/Generate +export * from './pods/generate/GenerateUtil'; export * from './pods/generate/HandlebarsTemplateEngine'; export * from './pods/generate/IdentifierGenerator'; +export * from './pods/generate/PodGenerator'; export * from './pods/generate/ResourcesGenerator'; export * from './pods/generate/SubdomainIdentifierGenerator'; export * from './pods/generate/SuffixIdentifierGenerator'; @@ -123,6 +125,7 @@ export * from './pods/settings/PodSettingsJsonParser'; export * from './pods/settings/PodSettingsParser'; // Pods +export * from './pods/ConfigPodManager'; export * from './pods/GeneratedPodManager'; export * from './pods/PodManager'; export * from './pods/PodManagerHttpHandler'; diff --git a/src/pods/ConfigPodManager.ts b/src/pods/ConfigPodManager.ts new file mode 100644 index 000000000..87a73cc07 --- /dev/null +++ b/src/pods/ConfigPodManager.ts @@ -0,0 +1,58 @@ +import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +import { getLoggerFor } from '../logging/LogUtil'; +import type { KeyValueStorage } from '../storage/keyvalue/KeyValueStorage'; +import type { ResourceStore } from '../storage/ResourceStore'; +import { addGeneratedResources } from './generate/GenerateUtil'; +import type { IdentifierGenerator } from './generate/IdentifierGenerator'; +import type { PodGenerator } from './generate/PodGenerator'; +import type { ResourcesGenerator } from './generate/ResourcesGenerator'; +import type { PodManager } from './PodManager'; +import type { PodSettings } from './settings/PodSettings'; + +/** + * Pod manager that creates a store for the pod with a {@link PodGenerator} + * and fills it with resources from a {@link ResourcesGenerator}. + * + * Part of the dynamic pod creation. + * 1. Calls a PodGenerator to instantiate a new resource store for the pod. + * 2. Generates the pod resources based on the templates as usual. + * 3. Adds the created pod to the routing storage, which is used for linking pod identifiers to their resource stores. + * + * @see {@link TemplatedPodGenerator}, {@link ConfigPodInitializer}, {@link BaseUrlRouterRule} + */ +export class ConfigPodManager implements PodManager { + protected readonly logger = getLoggerFor(this); + private readonly idGenerator: IdentifierGenerator; + private readonly podGenerator: PodGenerator; + private readonly routingStorage: KeyValueStorage; + private readonly resourcesGenerator: ResourcesGenerator; + + /** + * @param idGenerator - Generator for the pod identifiers. + * @param podGenerator - Generator for the pod stores. + * @param resourcesGenerator - Generator for the pod resources. + * @param routingStorage - Where to store the generated pods so they can be routed to. + */ + public constructor(idGenerator: IdentifierGenerator, podGenerator: PodGenerator, + resourcesGenerator: ResourcesGenerator, routingStorage: KeyValueStorage) { + this.idGenerator = idGenerator; + this.podGenerator = podGenerator; + this.routingStorage = routingStorage; + this.resourcesGenerator = resourcesGenerator; + } + + public async createPod(settings: PodSettings): Promise { + const identifier = this.idGenerator.generate(settings.login); + this.logger.info(`Creating pod ${identifier.path}`); + + // Will error in case there already is a store for the given identifier + const store = await this.podGenerator.generate(identifier, settings); + + const count = await addGeneratedResources(identifier, settings, this.resourcesGenerator, store); + this.logger.info(`Added ${count} resources to ${identifier.path}`); + + await this.routingStorage.set(identifier, store); + + return identifier; + } +} diff --git a/src/pods/GeneratedPodManager.ts b/src/pods/GeneratedPodManager.ts index 6207de9c0..85d827fe3 100644 --- a/src/pods/GeneratedPodManager.ts +++ b/src/pods/GeneratedPodManager.ts @@ -2,6 +2,7 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie import { getLoggerFor } from '../logging/LogUtil'; import type { ResourceStore } from '../storage/ResourceStore'; import { ConflictHttpError } from '../util/errors/ConflictHttpError'; +import { addGeneratedResources } from './generate/GenerateUtil'; import type { IdentifierGenerator } from './generate/IdentifierGenerator'; import type { ResourcesGenerator } from './generate/ResourcesGenerator'; import type { PodManager } from './PodManager'; @@ -36,12 +37,7 @@ export class GeneratedPodManager implements PodManager { throw new ConflictHttpError(`There already is a resource at ${podIdentifier.path}`); } - const resources = this.resourcesGenerator.generate(podIdentifier, settings); - let count = 0; - for await (const { identifier, representation } of resources) { - await this.store.setRepresentation(identifier, representation); - count += 1; - } + const count = await addGeneratedResources(podIdentifier, settings, this.resourcesGenerator, this.store); this.logger.info(`Added ${count} resources to ${podIdentifier.path}`); return podIdentifier; } diff --git a/src/pods/generate/GenerateUtil.ts b/src/pods/generate/GenerateUtil.ts new file mode 100644 index 000000000..0889ded12 --- /dev/null +++ b/src/pods/generate/GenerateUtil.ts @@ -0,0 +1,24 @@ +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import type { ResourceStore } from '../../storage/ResourceStore'; +import type { PodSettings } from '../settings/PodSettings'; +import type { ResourcesGenerator } from './ResourcesGenerator'; + +/** + * Generates resources with the given generator and adds them to the given store. + * @param identifier - Identifier of the pod. + * @param settings - Settings from which the pod is being created. + * @param generator - Generator to be used. + * @param store - Store to be updated. + * + * @returns The amount of resources that were added. + */ +export async function addGeneratedResources(identifier: ResourceIdentifier, settings: PodSettings, + generator: ResourcesGenerator, store: ResourceStore): Promise { + const resources = generator.generate(identifier, settings); + let count = 0; + for await (const { identifier: resourceId, representation } of resources) { + await store.setRepresentation(resourceId, representation); + count += 1; + } + return count; +} diff --git a/src/pods/generate/PodGenerator.ts b/src/pods/generate/PodGenerator.ts new file mode 100644 index 000000000..761738cb8 --- /dev/null +++ b/src/pods/generate/PodGenerator.ts @@ -0,0 +1,21 @@ +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import type { ResourceStore } from '../../storage/ResourceStore'; +import type { PodSettings } from '../settings/PodSettings'; + +/** + * Generates an empty resource store to be used as a new pod. + * It is also responsible for storing any relevant variables needed to instantiate this resource store. + * These can then be used when the server is restarted to re-instantiate those stores. + */ +export interface PodGenerator { + /** + * Creates a ResourceStore based on the given input. + * Should error if there already is a store for the given identifier. + * + * @param identifier - Identifier of the new pod. + * @param settings - Parameters to be used for the new pod. + * + * @returns A new ResourceStore to be used for the new pod. + */ + generate: (identifier: ResourceIdentifier, settings: PodSettings) => Promise; +} diff --git a/test/unit/pods/ConfigPodManager.test.ts b/test/unit/pods/ConfigPodManager.test.ts new file mode 100644 index 000000000..23baed28f --- /dev/null +++ b/test/unit/pods/ConfigPodManager.test.ts @@ -0,0 +1,67 @@ +import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; +import { ConfigPodManager } from '../../../src/pods/ConfigPodManager'; +import type { IdentifierGenerator } from '../../../src/pods/generate/IdentifierGenerator'; +import type { PodGenerator } from '../../../src/pods/generate/PodGenerator'; +import type { Resource, ResourcesGenerator } from '../../../src/pods/generate/ResourcesGenerator'; +import type { PodSettings } from '../../../src/pods/settings/PodSettings'; +import type { KeyValueStorage } from '../../../src/storage/keyvalue/KeyValueStorage'; +import type { ResourceStore } from '../../../src/storage/ResourceStore'; +describe('A ConfigPodManager', (): void => { + let settings: PodSettings; + const base = 'http://test.com/'; + const idGenerator: IdentifierGenerator = { + generate: (slug: string): ResourceIdentifier => ({ path: `${base}${slug}/` }), + }; + let store: ResourceStore; + let podGenerator: PodGenerator; + let routingStorage: KeyValueStorage; + let generatorData: Resource[]; + let resourcesGenerator: ResourcesGenerator; + let manager: ConfigPodManager; + + beforeEach(async(): Promise => { + settings = { + login: 'alice', + template: 'config-template.json', + webId: 'webId', + }; + + store = { + setRepresentation: jest.fn(), + } as any; + podGenerator = { + generate: jest.fn().mockResolvedValue(store), + }; + + generatorData = [ + { identifier: { path: '/path/' }, representation: '/' as any }, + { identifier: { path: '/path/foo' }, representation: '/foo' as any }, + ]; + resourcesGenerator = { + generate: jest.fn(async function* (): any { + yield* generatorData; + }), + }; + + const map = new Map(); + routingStorage = { + get: async(identifier: ResourceIdentifier): Promise => map.get(identifier.path), + set: async(identifier: ResourceIdentifier, value: ResourceStore): Promise => map.set(identifier.path, value), + } as any; + + manager = new ConfigPodManager(idGenerator, podGenerator, resourcesGenerator, routingStorage); + }); + + it('creates a pod and returns the newly generated identifier.', async(): Promise => { + const identifier = { path: `${base}alice/` }; + await expect(manager.createPod(settings)).resolves.toEqual(identifier); + expect(podGenerator.generate).toHaveBeenCalledTimes(1); + expect(podGenerator.generate).toHaveBeenLastCalledWith(identifier, settings); + expect(resourcesGenerator.generate).toHaveBeenCalledTimes(1); + expect(resourcesGenerator.generate).toHaveBeenLastCalledWith(identifier, settings); + expect(store.setRepresentation).toHaveBeenCalledTimes(2); + expect(store.setRepresentation).toHaveBeenCalledWith({ path: '/path/' }, '/'); + expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: '/path/foo' }, '/foo'); + await expect(routingStorage.get(identifier)).resolves.toBe(store); + }); +});