mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Update templates and generators to support ACP
This commit is contained in:
@@ -258,12 +258,15 @@ export * from './pods/generate/variables/VariableSetter';
|
||||
|
||||
// Pods/Generate
|
||||
export * from './pods/generate/BaseComponentsJsFactory';
|
||||
export * from './pods/generate/BaseResourcesGenerator';
|
||||
export * from './pods/generate/ComponentsJsFactory';
|
||||
export * from './pods/generate/GenerateUtil';
|
||||
export * from './pods/generate/IdentifierGenerator';
|
||||
export * from './pods/generate/PodGenerator';
|
||||
export * from './pods/generate/ResourcesGenerator';
|
||||
export * from './pods/generate/StaticFolderGenerator';
|
||||
export * from './pods/generate/SubdomainIdentifierGenerator';
|
||||
export * from './pods/generate/SubfolderResourcesGenerator';
|
||||
export * from './pods/generate/SuffixIdentifierGenerator';
|
||||
export * from './pods/generate/TemplatedPodGenerator';
|
||||
export * from './pods/generate/TemplatedResourcesGenerator';
|
||||
|
||||
237
src/pods/generate/BaseResourcesGenerator.ts
Normal file
237
src/pods/generate/BaseResourcesGenerator.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { createReadStream, promises as fsPromises } from 'fs';
|
||||
import type { Readable } from 'stream';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { Parser } from 'n3';
|
||||
import type { AuxiliaryStrategy } from '../../http/auxiliary/AuxiliaryStrategy';
|
||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type {
|
||||
FileIdentifierMapper,
|
||||
FileIdentifierMapperFactory,
|
||||
ResourceLink,
|
||||
} from '../../storage/mapping/FileIdentifierMapper';
|
||||
import type { ResourceSet } from '../../storage/ResourceSet';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { guardStream } from '../../util/GuardedStream';
|
||||
import type { Guarded } from '../../util/GuardedStream';
|
||||
import { joinFilePath, isContainerIdentifier, resolveAssetPath } from '../../util/PathUtil';
|
||||
import { addResourceMetadata } from '../../util/ResourceUtil';
|
||||
import { guardedStreamFrom, readableToString } from '../../util/StreamUtil';
|
||||
import type { TemplateEngine } from '../../util/templates/TemplateEngine';
|
||||
import type { Resource } from './ResourcesGenerator';
|
||||
import type { TemplatedResourcesGenerator } from './TemplatedResourcesGenerator';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
interface TemplateResourceLink extends ResourceLink {
|
||||
isTemplate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input arguments required for {@link BaseResourcesGenerator}
|
||||
*/
|
||||
export interface SubfolderResourcesGeneratorArgs {
|
||||
/**
|
||||
* Factory used to generate mapper relative to the base identifier.
|
||||
*/
|
||||
factory: FileIdentifierMapperFactory;
|
||||
/**
|
||||
* Template engine for generating the resources.
|
||||
*/
|
||||
templateEngine: TemplateEngine;
|
||||
/**
|
||||
* The extension of files that need to be interpreted as templates.
|
||||
* Will be removed to generate the identifier.
|
||||
*/
|
||||
templateExtension?: string;
|
||||
/**
|
||||
* The metadataStrategy
|
||||
*/
|
||||
metadataStrategy: AuxiliaryStrategy;
|
||||
/**
|
||||
* The default ResourceStore
|
||||
*/
|
||||
store: ResourceSet;
|
||||
}
|
||||
|
||||
// Comparator for the results of the `groupLinks` call
|
||||
function comparator(left: { link: TemplateResourceLink }, right: { link: TemplateResourceLink }): number {
|
||||
return left.link.identifier.path.localeCompare(right.link.identifier.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates resources by making use of a template engine.
|
||||
* The template folder structure will be kept.
|
||||
* Folders will be interpreted as containers and files as documents.
|
||||
* A FileIdentifierMapper will be used to generate identifiers that correspond to the relative structure.
|
||||
*
|
||||
* Metadata resources will be yielded separately from their subject resource.
|
||||
*
|
||||
* A relative `templateFolder` is resolved relative to cwd,
|
||||
* unless it's preceded by `@css:`, e.g. `@css:foo/bar`.
|
||||
*/
|
||||
export class BaseResourcesGenerator implements TemplatedResourcesGenerator {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly factory: FileIdentifierMapperFactory;
|
||||
private readonly templateEngine: TemplateEngine;
|
||||
private readonly templateExtension: string;
|
||||
private readonly metadataStrategy: AuxiliaryStrategy;
|
||||
private readonly store: ResourceSet;
|
||||
|
||||
/**
|
||||
* A mapper is needed to convert the template file paths to identifiers relative to the given base identifier.
|
||||
*
|
||||
* @param args - TemplatedResourcesGeneratorArgs
|
||||
*/
|
||||
public constructor(args: SubfolderResourcesGeneratorArgs) {
|
||||
this.factory = args.factory;
|
||||
this.templateEngine = args.templateEngine;
|
||||
this.templateExtension = args.templateExtension ?? '.hbs';
|
||||
this.metadataStrategy = args.metadataStrategy;
|
||||
this.store = args.store;
|
||||
}
|
||||
|
||||
public async* generate(templateFolder: string, location: ResourceIdentifier, options: Dict<string>):
|
||||
AsyncIterable<Resource> {
|
||||
templateFolder = resolveAssetPath(templateFolder);
|
||||
|
||||
// Ignore folders that don't exist
|
||||
if (!await pathExists(templateFolder)) {
|
||||
this.logger.warn(`Ignoring non-existing template folder ${templateFolder}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mapper = await this.factory.create(location.path, templateFolder);
|
||||
const folderLink = await this.toTemplateLink(templateFolder, mapper);
|
||||
yield* this.processFolder(folderLink, mapper, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates results for all entries in the given folder, including the folder itself.
|
||||
*/
|
||||
private async* processFolder(folderLink: TemplateResourceLink, mapper: FileIdentifierMapper, options: Dict<string>):
|
||||
AsyncIterable<Resource> {
|
||||
// Group resource links with their corresponding metadata links
|
||||
const links = await this.groupLinks(folderLink.filePath, mapper);
|
||||
|
||||
// Remove root metadata if it exists
|
||||
const metaLink = links[folderLink.identifier.path]?.meta;
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete links[folderLink.identifier.path];
|
||||
|
||||
yield* this.generateResource(folderLink, options, metaLink);
|
||||
|
||||
// Make sure the results are sorted
|
||||
for (const { link, meta } of Object.values(links).sort(comparator)) {
|
||||
if (isContainerIdentifier(link.identifier)) {
|
||||
yield* this.processFolder(link, mapper, options);
|
||||
} else {
|
||||
yield* this.generateResource(link, options, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TemplateResourceLink for the given filePath,
|
||||
* which connects a resource URL to its template file.
|
||||
* The identifier will be based on the file path stripped from the template extension,
|
||||
* but the filePath parameter will still point to the original file.
|
||||
*/
|
||||
private async toTemplateLink(filePath: string, mapper: FileIdentifierMapper): Promise<TemplateResourceLink> {
|
||||
const stats = await fsPromises.lstat(filePath);
|
||||
|
||||
// Slice the template extension from the filepath for correct identifier generation
|
||||
const isTemplate = filePath.endsWith(this.templateExtension);
|
||||
const slicedPath = isTemplate ? filePath.slice(0, -this.templateExtension.length) : filePath;
|
||||
const link = await mapper.mapFilePathToUrl(slicedPath, stats.isDirectory());
|
||||
// We still need the original file path for disk reading though
|
||||
return {
|
||||
...link,
|
||||
filePath,
|
||||
isTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates TemplateResourceLinks for each entry in the given folder
|
||||
* and combines the results so resources and their metadata are grouped together.
|
||||
*/
|
||||
private async groupLinks(folderPath: string, mapper: FileIdentifierMapper):
|
||||
Promise<Record<string, { link: TemplateResourceLink; meta?: TemplateResourceLink }>> {
|
||||
const files = await fsPromises.readdir(folderPath);
|
||||
const links: Record<string, { link: TemplateResourceLink; meta?: TemplateResourceLink }> = { };
|
||||
for (const name of files) {
|
||||
const link = await this.toTemplateLink(joinFilePath(folderPath, name), mapper);
|
||||
const { path } = link.identifier;
|
||||
links[path] = Object.assign(links[path] || {}, link.isMetadata ? { meta: link } : { link });
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Resource object for the given ResourceLink.
|
||||
* In the case of documents the corresponding template will be used.
|
||||
* If a ResourceLink of metadata is provided the corresponding metadata resource
|
||||
* will be yielded as a separate resource.
|
||||
*/
|
||||
private async* generateResource(link: TemplateResourceLink, options: Dict<string>, metaLink?: TemplateResourceLink):
|
||||
AsyncIterable<Resource> {
|
||||
let data: Guarded<Readable> | undefined;
|
||||
const metadata = new RepresentationMetadata(link.identifier);
|
||||
|
||||
// Read file if it is not a container
|
||||
if (!isContainerIdentifier(link.identifier)) {
|
||||
data = await this.processFile(link, options);
|
||||
metadata.contentType = link.contentType;
|
||||
}
|
||||
// Do not yield a container resource if it already exists
|
||||
if (!isContainerIdentifier(link.identifier) || !await this.store.hasResource(link.identifier)) {
|
||||
this.logger.debug(`Generating resource ${link.identifier.path}`);
|
||||
yield {
|
||||
identifier: link.identifier,
|
||||
representation: new BasicRepresentation(data ?? [], metadata),
|
||||
};
|
||||
}
|
||||
|
||||
// Add metadata from .meta file if there is one
|
||||
if (metaLink) {
|
||||
const rawMetadata = await this.generateMetadata(metaLink, options);
|
||||
const metaIdentifier = this.metadataStrategy.getAuxiliaryIdentifier(link.identifier);
|
||||
const descriptionMeta = new RepresentationMetadata(metaIdentifier);
|
||||
addResourceMetadata(rawMetadata, isContainerIdentifier(link.identifier));
|
||||
this.logger.debug(`Generating resource ${metaIdentifier.path}`);
|
||||
yield {
|
||||
identifier: metaIdentifier,
|
||||
representation: new BasicRepresentation(rawMetadata.quads(), descriptionMeta, INTERNAL_QUADS),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a RepresentationMetadata using the given template.
|
||||
*/
|
||||
private async generateMetadata(metaLink: TemplateResourceLink, options: Dict<string>):
|
||||
Promise<RepresentationMetadata> {
|
||||
const metadata = new RepresentationMetadata(metaLink.identifier);
|
||||
|
||||
const data = await this.processFile(metaLink, options);
|
||||
const parser = new Parser({ format: metaLink.contentType, baseIRI: metaLink.identifier.path });
|
||||
const quads = parser.parse(await readableToString(data));
|
||||
metadata.addQuads(quads);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read stream from the file and applies the template if necessary.
|
||||
*/
|
||||
private async processFile(link: TemplateResourceLink, contents: Dict<string>): Promise<Guarded<Readable>> {
|
||||
if (link.isTemplate) {
|
||||
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: link.filePath }});
|
||||
return guardedStreamFrom(rendered);
|
||||
}
|
||||
return guardStream(createReadStream(link.filePath));
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export interface Resource {
|
||||
export interface ResourcesGenerator {
|
||||
/**
|
||||
* Generates resources with the given options.
|
||||
* The output Map should be sorted so that containers always appear before their contents.
|
||||
* The output Iterable should be sorted so that containers always appear before their contents.
|
||||
* @param location - Base identifier.
|
||||
* @param options - Options that can be used when generating resources.
|
||||
*
|
||||
|
||||
21
src/pods/generate/StaticFolderGenerator.ts
Normal file
21
src/pods/generate/StaticFolderGenerator.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
|
||||
import type { TemplatedResourcesGenerator } from './TemplatedResourcesGenerator';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* Stores a static template folder that will be used to call the wrapped {@link TemplatedResourcesGenerator}.
|
||||
*/
|
||||
export class StaticFolderGenerator implements ResourcesGenerator {
|
||||
private readonly resourcesGenerator: TemplatedResourcesGenerator;
|
||||
private readonly templateFolder: string;
|
||||
|
||||
public constructor(resourcesGenerator: TemplatedResourcesGenerator, templateFolder: string) {
|
||||
this.resourcesGenerator = resourcesGenerator;
|
||||
this.templateFolder = templateFolder;
|
||||
}
|
||||
|
||||
public generate(location: ResourceIdentifier, options: Dict<string>): AsyncIterable<Resource> {
|
||||
return this.resourcesGenerator.generate(this.templateFolder, location, options);
|
||||
}
|
||||
}
|
||||
56
src/pods/generate/SubfolderResourcesGenerator.ts
Normal file
56
src/pods/generate/SubfolderResourcesGenerator.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { sortedAsyncMerge } from '../../util/IterableUtil';
|
||||
import { joinFilePath, resolveAssetPath } from '../../util/PathUtil';
|
||||
import type { Resource } from './ResourcesGenerator';
|
||||
import type { TemplatedResourcesGenerator } from './TemplatedResourcesGenerator';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
// Sorts Resources based on their identifiers
|
||||
function comparator(left: Resource, right: Resource): number {
|
||||
return left.identifier.path.localeCompare(right.identifier.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates all resources found in specific subfolders of the given template folder.
|
||||
* In case the same resource is defined in several subfolders,
|
||||
* the data of the last subfolder in the list will be used.
|
||||
*
|
||||
* The results of all the subfolders will be merged so the end result is still a sorted stream of identifiers.
|
||||
*
|
||||
* One of the main use cases for this class is so template resources can be in a separate folder
|
||||
* than their corresponding authorization resources,
|
||||
* allowing for authorization-independent templates.
|
||||
*/
|
||||
export class SubfolderResourcesGenerator implements TemplatedResourcesGenerator {
|
||||
private readonly resourcesGenerator: TemplatedResourcesGenerator;
|
||||
private readonly subfolders: string[];
|
||||
|
||||
public constructor(resourcesGenerator: TemplatedResourcesGenerator, subfolders: string[]) {
|
||||
this.resourcesGenerator = resourcesGenerator;
|
||||
this.subfolders = subfolders;
|
||||
}
|
||||
|
||||
public async* generate(templateFolder: string, location: ResourceIdentifier, options: Dict<string>):
|
||||
AsyncIterable<Resource> {
|
||||
const root = resolveAssetPath(templateFolder);
|
||||
const templateSubfolders = this.subfolders.map((subfolder): string => joinFilePath(root, subfolder));
|
||||
|
||||
// Build all generators
|
||||
const generators: AsyncIterator<Resource>[] = [];
|
||||
for (const templateSubfolder of templateSubfolders) {
|
||||
generators.push(this.resourcesGenerator.generate(templateSubfolder, location, options)[Symbol.asyncIterator]());
|
||||
}
|
||||
|
||||
let previous: ResourceIdentifier = { path: '' };
|
||||
for await (const result of sortedAsyncMerge(generators, comparator)) {
|
||||
// Skip duplicate results.
|
||||
// In practice these are just going to be the same empty containers.
|
||||
if (result.identifier.path === previous.path) {
|
||||
result.representation.data.destroy();
|
||||
} else {
|
||||
previous = result.identifier;
|
||||
yield result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,216 +1,21 @@
|
||||
import { createReadStream, promises as fsPromises } from 'fs';
|
||||
import type { Readable } from 'stream';
|
||||
import { Parser } from 'n3';
|
||||
import type { AuxiliaryStrategy } from '../../http/auxiliary/AuxiliaryStrategy';
|
||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import type {
|
||||
FileIdentifierMapper,
|
||||
FileIdentifierMapperFactory,
|
||||
ResourceLink,
|
||||
} from '../../storage/mapping/FileIdentifierMapper';
|
||||
import type { ResourceSet } from '../../storage/ResourceSet';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { guardStream } from '../../util/GuardedStream';
|
||||
import type { Guarded } from '../../util/GuardedStream';
|
||||
import { joinFilePath, isContainerIdentifier, resolveAssetPath } from '../../util/PathUtil';
|
||||
import { addResourceMetadata } from '../../util/ResourceUtil';
|
||||
import { guardedStreamFrom, readableToString } from '../../util/StreamUtil';
|
||||
import type { TemplateEngine } from '../../util/templates/TemplateEngine';
|
||||
import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
|
||||
import type { Resource } from './ResourcesGenerator';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
interface TemplateResourceLink extends ResourceLink {
|
||||
isTemplate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input arguments required for {@link TemplatedResourcesGenerator}
|
||||
* Generator used to create resources relative to a given base identifier.
|
||||
* Similar to {@link ResourcesGenerator}, but takes as input a string
|
||||
* indicating where the templates are stored that need to be used for resource generation.
|
||||
*/
|
||||
export interface TemplatedResourcesGeneratorArgs {
|
||||
export interface TemplatedResourcesGenerator {
|
||||
/**
|
||||
* Folder where the templates are located.
|
||||
*/
|
||||
templateFolder: string;
|
||||
/**
|
||||
* Factory used to generate mapper relative to the base identifier.
|
||||
*/
|
||||
factory: FileIdentifierMapperFactory;
|
||||
/**
|
||||
* Template engine for generating the resources.
|
||||
*/
|
||||
templateEngine: TemplateEngine;
|
||||
/**
|
||||
* The extension of files that need to be interpreted as templates.
|
||||
* Will be removed to generate the identifier.
|
||||
*/
|
||||
templateExtension?: string;
|
||||
/**
|
||||
* The metadataStrategy
|
||||
*/
|
||||
metadataStrategy: AuxiliaryStrategy;
|
||||
/**
|
||||
* The default ResourceStore
|
||||
*/
|
||||
store: ResourceSet;
|
||||
}
|
||||
/**
|
||||
* Generates resources by making use of a template engine.
|
||||
* The template folder structure will be kept.
|
||||
* Folders will be interpreted as containers and files as documents.
|
||||
* A FileIdentifierMapper will be used to generate identifiers that correspond to the relative structure.
|
||||
*
|
||||
* A relative `templateFolder` is resolved relative to cwd,
|
||||
* unless it's preceded by `@css:`, e.g. `@css:foo/bar`.
|
||||
*/
|
||||
export class TemplatedResourcesGenerator implements ResourcesGenerator {
|
||||
private readonly templateFolder: string;
|
||||
private readonly factory: FileIdentifierMapperFactory;
|
||||
private readonly templateEngine: TemplateEngine;
|
||||
private readonly templateExtension: string;
|
||||
private readonly metadataStrategy: AuxiliaryStrategy;
|
||||
private readonly store: ResourceSet;
|
||||
|
||||
/**
|
||||
* A mapper is needed to convert the template file paths to identifiers relative to the given base identifier.
|
||||
* Generates resources with the given options, based on the given template folder.
|
||||
* The output Iterable should be sorted so that containers always appear before their contents.
|
||||
* @param templateFolder - Folder where the templates are located.
|
||||
* @param location - Base identifier.
|
||||
* @param options - Options that can be used when generating resources.
|
||||
*
|
||||
* @param args - TemplatedResourcesGeneratorArgs
|
||||
* @returns A map where the keys are the identifiers and the values the corresponding representations to store.
|
||||
*/
|
||||
public constructor(args: TemplatedResourcesGeneratorArgs) {
|
||||
this.templateFolder = resolveAssetPath(args.templateFolder);
|
||||
this.factory = args.factory;
|
||||
this.templateEngine = args.templateEngine;
|
||||
this.templateExtension = args.templateExtension ?? '.hbs';
|
||||
this.metadataStrategy = args.metadataStrategy;
|
||||
this.store = args.store;
|
||||
}
|
||||
|
||||
public async* generate(location: ResourceIdentifier, options: Dict<string>): AsyncIterable<Resource> {
|
||||
const mapper = await this.factory.create(location.path, this.templateFolder);
|
||||
const folderLink = await this.toTemplateLink(this.templateFolder, mapper);
|
||||
yield* this.processFolder(folderLink, mapper, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates results for all entries in the given folder, including the folder itself.
|
||||
*/
|
||||
private async* processFolder(folderLink: TemplateResourceLink, mapper: FileIdentifierMapper, options: Dict<string>):
|
||||
AsyncIterable<Resource> {
|
||||
// Group resource links with their corresponding metadata links
|
||||
const links = await this.groupLinks(folderLink.filePath, mapper);
|
||||
|
||||
// Remove root metadata if it exists
|
||||
const metaLink = links[folderLink.identifier.path]?.meta;
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete links[folderLink.identifier.path];
|
||||
|
||||
yield* this.generateResource(folderLink, options, metaLink);
|
||||
|
||||
for (const { link, meta } of Object.values(links)) {
|
||||
if (isContainerIdentifier(link.identifier)) {
|
||||
yield* this.processFolder(link, mapper, options);
|
||||
} else {
|
||||
yield* this.generateResource(link, options, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TemplateResourceLink for the given filePath.
|
||||
* The identifier will be based on the file path stripped from the template extension,
|
||||
* but the filePath parameter will still point to the original file.
|
||||
*/
|
||||
private async toTemplateLink(filePath: string, mapper: FileIdentifierMapper): Promise<TemplateResourceLink> {
|
||||
const stats = await fsPromises.lstat(filePath);
|
||||
|
||||
// Slice the template extension from the filepath for correct identifier generation
|
||||
const isTemplate = filePath.endsWith(this.templateExtension);
|
||||
const slicedPath = isTemplate ? filePath.slice(0, -this.templateExtension.length) : filePath;
|
||||
const link = await mapper.mapFilePathToUrl(slicedPath, stats.isDirectory());
|
||||
// We still need the original file path for disk reading though
|
||||
return {
|
||||
...link,
|
||||
filePath,
|
||||
isTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates TemplateResourceLinks for each entry in the given folder
|
||||
* and combines the results so resources and their metadata are grouped together.
|
||||
*/
|
||||
private async groupLinks(folderPath: string, mapper: FileIdentifierMapper):
|
||||
Promise<Record<string, { link: TemplateResourceLink; meta?: TemplateResourceLink }>> {
|
||||
const files = await fsPromises.readdir(folderPath);
|
||||
const links: Record<string, { link: TemplateResourceLink; meta?: TemplateResourceLink }> = { };
|
||||
for (const name of files) {
|
||||
const link = await this.toTemplateLink(joinFilePath(folderPath, name), mapper);
|
||||
const { path } = link.identifier;
|
||||
links[path] = Object.assign(links[path] || {}, link.isMetadata ? { meta: link } : { link });
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Resource object for the given ResourceLink.
|
||||
* In the case of documents the corresponding template will be used.
|
||||
* If a ResourceLink of metadata is provided the corresponding data will be added as metadata.
|
||||
*/
|
||||
private async* generateResource(link: TemplateResourceLink, options: Dict<string>, metaLink?: TemplateResourceLink):
|
||||
AsyncIterable<Resource> {
|
||||
let data: Guarded<Readable> | undefined;
|
||||
const metadata = new RepresentationMetadata(link.identifier);
|
||||
|
||||
// Read file if it is not a container
|
||||
if (!isContainerIdentifier(link.identifier)) {
|
||||
data = await this.processFile(link, options);
|
||||
metadata.contentType = link.contentType;
|
||||
}
|
||||
// Do not yield a container resource if it already exists
|
||||
if (!isContainerIdentifier(link.identifier) || !await this.store.hasResource(link.identifier)) {
|
||||
yield {
|
||||
identifier: link.identifier,
|
||||
representation: new BasicRepresentation(data ?? [], metadata),
|
||||
};
|
||||
}
|
||||
|
||||
// Add metadata from .meta file if there is one
|
||||
if (metaLink) {
|
||||
const rawMetadata = await this.generateMetadata(metaLink, options);
|
||||
const metaIdentifier = this.metadataStrategy.getAuxiliaryIdentifier(link.identifier);
|
||||
const descriptionMeta = new RepresentationMetadata(metaIdentifier);
|
||||
addResourceMetadata(rawMetadata, isContainerIdentifier(link.identifier));
|
||||
yield {
|
||||
identifier: metaIdentifier,
|
||||
representation: new BasicRepresentation(rawMetadata.quads(), descriptionMeta, INTERNAL_QUADS),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a RepresentationMetadata using the given template.
|
||||
*/
|
||||
private async generateMetadata(metaLink: TemplateResourceLink, options: Dict<string>):
|
||||
Promise<RepresentationMetadata> {
|
||||
const metadata = new RepresentationMetadata(metaLink.identifier);
|
||||
|
||||
const data = await this.processFile(metaLink, options);
|
||||
const parser = new Parser({ format: metaLink.contentType, baseIRI: metaLink.identifier.path });
|
||||
const quads = parser.parse(await readableToString(data));
|
||||
metadata.addQuads(quads);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read stream from the file and applies the template if necessary.
|
||||
*/
|
||||
private async processFile(link: TemplateResourceLink, contents: Dict<string>): Promise<Guarded<Readable>> {
|
||||
if (link.isTemplate) {
|
||||
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: link.filePath }});
|
||||
return guardedStreamFrom(rendered);
|
||||
}
|
||||
return guardStream(createReadStream(link.filePath));
|
||||
}
|
||||
generate: (templateFolder: string, location: ResourceIdentifier, options: Dict<string>) => AsyncIterable<Resource>;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@ export const INTERNAL_ERROR = 'internal/error';
|
||||
|
||||
export const DEFAULT_CUSTOM_TYPES = {
|
||||
acl: TEXT_TURTLE,
|
||||
acr: TEXT_TURTLE,
|
||||
meta: TEXT_TURTLE,
|
||||
};
|
||||
|
||||
@@ -113,3 +113,93 @@ export function reduce<TIn, TOut>(iterable: Iterable<TIn>,
|
||||
}
|
||||
return previousValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for {@link sortedAsyncMerge}.
|
||||
*
|
||||
* Returns the next result of an AsyncIterator, or undefined if the iterator is finished.
|
||||
*/
|
||||
async function nextAsyncEntry<T>(iterator: AsyncIterator<T>): Promise<T | undefined> {
|
||||
const result = await iterator.next();
|
||||
if (result.done) {
|
||||
return;
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for {@link sortedAsyncMerge}.
|
||||
*
|
||||
* Compares the next results of all `iterators` and returns the first one,
|
||||
* determined by the provided `comparator`.
|
||||
*
|
||||
* `results` should contain the first result of all these iterators.
|
||||
* This array will also be updated, replacing the result of the iterator whose result was chosen by the next one.
|
||||
*/
|
||||
async function findNextSorted<T>(iterators: AsyncIterator<T>[], results: (T | undefined)[],
|
||||
comparator: (left: T, right: T) => number): Promise<T | undefined> {
|
||||
let best: { idx: number; value: T } | undefined;
|
||||
// For every iterator: see if their next result is the best one so far
|
||||
for (let i = 0; i < iterators.length; ++i) {
|
||||
const value = results[i];
|
||||
if (typeof value !== 'undefined') {
|
||||
let compare = 1;
|
||||
if (best) {
|
||||
compare = comparator(best.value, value);
|
||||
}
|
||||
|
||||
if (compare > 0) {
|
||||
best = { idx: i, value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best) {
|
||||
// Advance the iterator that returned the new result
|
||||
results[best.idx] = await nextAsyncEntry(iterators[best.idx]);
|
||||
}
|
||||
|
||||
// Will return undefined if `best` was never initialized above
|
||||
return best?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the results of several sorted iterators.
|
||||
* In case the results of the individual iterators are not sorted the outcome results will also not be sorted.
|
||||
*
|
||||
* @param iterators - The iterators whose results need to be merged.
|
||||
* @param comparator - The comparator to use to compare the results.
|
||||
*/
|
||||
export async function* sortedAsyncMerge<T>(iterators: AsyncIterator<T>[], comparator?: (left: T, right: T) => number):
|
||||
AsyncIterable<T> {
|
||||
if (!comparator) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extra-parens
|
||||
comparator = (left, right): number => left < right ? -1 : (left > right ? 1 : 0);
|
||||
}
|
||||
|
||||
// Initialize the array to the first result of every iterator
|
||||
const results: (T | undefined)[] = [];
|
||||
for (const iterator of iterators) {
|
||||
results.push(await nextAsyncEntry(iterator));
|
||||
}
|
||||
|
||||
// Keep returning results as long as we find them
|
||||
while (true) {
|
||||
const next = await findNextSorted(iterators, results, comparator);
|
||||
if (typeof next === 'undefined') {
|
||||
return;
|
||||
}
|
||||
yield next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an `AsyncIterator` to an array.
|
||||
*/
|
||||
export async function asyncToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
||||
const arr: T[] = [];
|
||||
for await (const result of iterable) {
|
||||
arr.push(result);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user