Standard libraries that lift up your code.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

282 lines
8.7 KiB

/**
* Base type for a canonical definition.
*/
import {Canon} from './Canon'
import {universalPath, UniversalPath, ErrorWithContext} from '../util'
import {Logging} from './Logging'
import {Inject} from '../di'
import * as nodePath from 'path'
import {Unit} from '../lifecycle/Unit'
import {isCanonicalReceiver} from '../support/CanonicalReceiver'
/**
* Interface describing a definition of a single canonical item loaded from the app.
*/
export interface CanonicalDefinition {
canonicalName: string,
originalName: string,
imported: any,
}
/**
* Type alias for a function that resolves a canonical name to a canonical item, if one exists.
*/
export type CanonicalResolverFunction<T> = (key: string) => T | undefined
/**
* Interface for a canonical resolver that provides additional information.
*/
export interface ComplexCanonicalResolver<T> {
get: CanonicalResolverFunction<T>,
all: () => string[],
}
export type CanonicalResolver<T> = CanonicalResolverFunction<T> | ComplexCanonicalResolver<T>
/**
* Base type for a canonical name reference.
*/
export interface CanonicalReference {
resource?: string,
item: string,
particular?: string,
}
/**
* Abstract unit type that loads items recursively from a directory structure, assigning
* them normalized names ("canonical names"), and providing a way to fetch the resources
* by name.
*
* @example
* The Config service is a Canonical derivative that loads files ending with `.config.js`
* from the `app/config` directory.
*
* If, for example, there is a config file `app/config/auth/Forms.config.js` (in the
* generated code), it can be loaded by the canonical name `auth:Forms`.
*
*/
export abstract class Canonical<T> extends Unit {
@Inject()
protected readonly logging!: Logging
@Inject()
protected readonly canon!: Canon
/**
* The base path directory where the canonical definitions reside.
* @type string
*/
protected appPath: string[] = []
/**
* The file suffix of files in the base path that should be loaded.
* @type string
*/
protected suffix = '.js'
/**
* The singular, programmatic name of one of these canonical items.
* @example middleware
* @type string
*/
protected canonicalItem = ''
/**
* Object mapping canonical names to loaded file references.
* @type object
*/
protected loadedItems: { [key: string]: T } = {}
/**
* Object mapping canonical namespaces to resolver functions.
* @protected
*/
protected loadedNamespaces: { [key: string]: CanonicalResolver<T> } = {}
/**
* Resolve a canonical reference from its string form to a CanonicalReference.
* @param {string} reference
* @return CanonicalReference
*/
public static resolve(reference: string): CanonicalReference {
const rscParts = reference.split('::')
const resource = rscParts.length > 1 ? rscParts[0] + 's' : undefined
const rscLess = rscParts.length > 1 ? rscParts[1] : rscParts[0]
const prtParts = rscLess.split('.')
const item = prtParts[0]
const particular = prtParts.length > 1 ? prtParts.slice(1).join('.') : undefined
return {
resource,
item,
particular,
}
}
/**
* Return an array of all loaded canonical names.
*/
public all(namespace?: string): string[] {
if ( namespace ) {
const resolver = this.loadedNamespaces[namespace]
if ( !resolver ) {
throw new ErrorWithContext(`Unable to find namespace for ${this.canonicalItem}: ${namespace}`, {
canonicalItem: this.canonicalItem,
namespace,
})
}
if ( typeof resolver === 'function' ) {
return []
} else {
return resolver.all()
}
}
return Object.keys(this.loadedItems)
}
/**
* Return an array of all loaded canonical namespaces.
*/
public namespaces(): string[] {
return Object.keys(this.loadedNamespaces)
}
/**
* Get a Universal path to the base directory where this unit loads its canonical files from.
*/
public get path(): UniversalPath {
return this.app().appPath(...this.appPath)
}
/** Get the plural name of the canonical items provided by this unit. */
public get canonicalItems(): string {
return `${this.canonicalItem}s`
}
/** Get a canonical item by key. */
public get(key: string): T | undefined {
if ( key.startsWith('@') ) {
const [namespace, ...rest] = key.split(':')
key = rest.join(':')
const resolver = this.loadedNamespaces[namespace]
if ( !resolver ) {
throw new ErrorWithContext(`Unable to find namespace for ${this.canonicalItem}: ${namespace}`, {
canonicalItem: this.canonicalItem,
namespace,
key,
})
}
if ( typeof resolver === 'function' ) {
return resolver(key)
} else {
return resolver.get(key)
}
}
return this.loadedItems[key]
}
/**
* Register a namespace resolver with the canonical unit.
*
* Namespaces are canonical names that start with a particular key, beginning with the `@` character,
* which resolve their resources using a resolver function.
*
* @example
* ```typescript
* const items = {
* 'foo:bar': 123,
* 'bob': 456,
* }
*
* const resolver = (key: string) => items[key]
*
* canonical.registerNamespace('@mynamespace', resolver)
* ```
*
* Now, the items in the `@mynamespace` namespace can be accessed like so:
*
* ```typescript
* canonical.get('@mynamespace:foo:bar') // => 123
* canonical.get('@mynamespace:bob') // => 456
* ```
*
* @param name
* @param resolver
*/
public registerNamespace(name: string, resolver: CanonicalResolver<T>): void {
if ( !name.startsWith('@') ) {
throw new ErrorWithContext(`Canonical namespaces must start with @.`, { name })
}
if ( this.loadedNamespaces[name] ) {
this.logging.warn(`Replacing canonical namespace resolver for: ${name}`)
}
this.loadedNamespaces[name] = resolver
}
public async up(): Promise<void> {
for await ( const entry of this.path.walk() ) {
if ( !entry.endsWith(this.suffix) ) {
this.logging.debug(`Skipping file with invalid suffix: ${entry}`)
continue
}
const definition = await this.buildCanonicalDefinition(entry)
this.logging.verbose(`Registering canonical ${this.canonicalItem} "${definition.canonicalName}" from ${entry}`)
const resolvedItem = await this.initCanonicalItem(definition)
if ( isCanonicalReceiver(resolvedItem) ) {
resolvedItem.setCanonicalResolver(
`${this.canonicalItems}::${definition.canonicalName}`,
definition.canonicalName,
)
}
this.loadedItems[definition.canonicalName] = resolvedItem
}
this.canon.registerCanonical(this)
}
/**
* Called for each canonical item loaded from a file. This function should do any setup necessary and return the item
* that should be associated with the canonical name.
* @param definition
*/
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
return definition.imported.default ?? definition.imported[definition.canonicalName.split(':').reverse()[0]]
}
/**
* Given the path to a file in the canonical items directory, create a CanonicalDefinition record from that file.
* @param filePath
* @param basePath
* @protected
*/
protected async buildCanonicalDefinition(filePath: string, basePath?: UniversalPath): Promise<CanonicalDefinition> {
const originalName = filePath.replace((basePath || this.path).toLocal, '').substr(1)
const pathRegex = new RegExp(nodePath.sep, 'g')
const canonicalName = originalName.replace(pathRegex, ':')
.split('')
.reverse()
.join('')
.substr(this.suffix.length)
.split('')
.reverse()
.join('')
const fullUniversalPath = universalPath(filePath)
this.logging.verbose(`Importing from: ${fullUniversalPath}`)
const imported = await import(fullUniversalPath.toLocal)
return { canonicalName,
originalName,
imported }
}
}