skypager-project
Version:
skypager project framework
252 lines (206 loc) • 7.03 kB
JavaScript
/*
* @name Model
* @description The Model helper is used to define patterns which can turn a certain
* document or set of documents into an ActiveRecord like object which
* can be queried or acted on, and which can have relationships with other
* documents in the project.
*/
import Helper from '../helper'
import isArray from 'lodash/isArray'
import isFunction from 'lodash/isArray'
import flatten from 'lodash/flatten'
import testRule from 'skypager-util/lib/path-matcher'
import castArray from 'lodash/castArray'
import bindAll from 'lodash/bindAll'
import { mixinPropertyUtils } from 'skypager-util/lib/properties'
/**
* A Model is a way of taking any document and treating it as a thing with attributes,
* a thing with a status, relationships, context, etc. These things can be derived by
* analyzing the contents, or through explicit annotations made by their author.
*
* For example, a javascript application has many screens. these screens have multiple components,
* style sheets, sources of content, images and assets. By turning the file which defines this screens
* as a model we can use these relationships in many ways.
*/
export class Model extends Helper {
static isCacheable = false
static dirname = __dirname
// A Model returns a set of documents
get documentInstances() {
return this.chain
.get('routeResults')
.map(result => {
const doc = result.doc
const data = {
...result.meta,
...doc.get('data', {}),
}
doc.set('data', data)
return doc
})
.value()
}
get resultIds() {
return this.get('routeResults').map(result => result.id)
}
buildId(fromDocumentId) {
return this.idBuilder(fromDocumentId)
}
createContextRegistry(options = {}, filter) {
return this.createRequireContext({
stringify: true,
...options,
}, filter)
}
createModuleMap(options = {}, filter) {
return this.chain.get('documentsMap')
.pickBy((v,k) => !filter || filter.call(this, v,k))
.mapValues((v) => v.get(options.pathAttribute || 'baseRelativePath'))
.mapValues(v =>
`${options.prepend || ''}${options.resolve ? this.project.resolve(v) : v}${options.append || ''}`
)
.value()
}
createRequireContext(options = {}, filter) {
const moduleMap = options.moduleMap || this.createModuleMap(options, filter)
return this.project.select('require-context', {
moduleMap, ...options,
})
}
get include() {
return flatten([
...(castArray(this.result('options.include', []))),
...(castArray(this.result('provider.include', []))),
])
}
get exclude() {
return flatten([
...(castArray(this.result('options.exclude', []))),
...(castArray(this.result('provider.exclude', []))),
])
}
get matchPattern() {
return this.tryGet('matchPattern')
}
get routeResults() {
const { include, exclude } = this
if (this.matchPattern) {
include.unshift(this.matchPattern)
}
exclude.push(
...this.project.chain.get('paths').pick('bundles','ast','logs','temp','library','cache','output')
)
const filterBy = this.tryGet('filterBy')
return this.chain
.get('routes')
.map(route => this.project.route(route))
.flatten()
.filter((item, ...args) => {
if (!item) { return false }
if (exclude.length > 0 && exclude.find(rule => testRule(rule, item.file.path))) {
return false
}
if (include.length > 0 && !include.find(rule => testRule(rule, item.file.path))) {
return false
}
if (isFunction(filterBy)) {
return filterBy.call(this, item, ...args)
}
return true
})
.compact()
.value()
}
get resultsMappedByRoute() {
return this.routeResultsMap
}
get routeResultsMap() {
return this.chain
.get('routes')
.keyBy(i => i)
.mapValues((route) => this.project.route(route))
.value()
}
get idBuilder() {
if (this.tryGet('idBuilder')) {
return this.tryGet('idBuilder').bind(this)
}
return (r) => r.id || r
}
get idAttribute() {
return this.tryGet('idAttribute', 'id')
}
get routes() {
const cfg = this.tryResult('routes', [])
if (!isArray(cfg)) {
return flatten(Object.values(cfg))
}
return cfg.map(o => typeof o === 'string' ? o : o.pattern || o.route)
}
get instanceMethods() {
return this.chain.invoke('tryResult', 'instanceMethods', {})
.pickBy(v => typeof v === 'function')
.value()
}
get collectionMethods() {
return this.chain.invoke('tryResult', 'collectionMethods', {})
.pickBy(v => typeof v === 'function')
.mapValues(v => v.bind(this))
.value()
}
get documentsMap() {
return this.chain.get('documentInstances').keyBy(this.idBuilder).value()
}
initialize() {
this.applyMixin(this.collectionMethods, this.context, this)
this.lazy('instances', () => this.createInstances(this.instanceMethods))
this.hideGetter('instanceChain', () => this.chain.get('instances'))
this.hideGetter('refreshed', () => this.refresh())
}
refresh() {
delete(this.instances)
this.lazy('instances', () => this.createInstances(this.instanceMethods))
return this
}
createInstanceObjects(...args) {
return this.createInstances(...args)
}
createInstances(instanceMethods = {}, stayChained = false) {
const instanceOptions = this.tryResult('instanceOptions', ({
propUtils: true,
lodash: true,
chain: true,
}))
const selector = this.tryGet('selector', (this.tryGet('active') ? 'active-entity' : 'entity'))
const propUtils = !!instanceOptions && instanceOptions.propUtils
const mixinLodash = !!instanceOptions && instanceOptions.lodash
const applyChain = !!instanceOptions && instanceOptions.chain
const chain = this.chain
.get('documentsMap', {})
.mapValues(doc => {
const e = doc.select(selector, instanceMethods)
const instance = bindAll(Object.assign(e, instanceMethods))
return propUtils ? mixinPropertyUtils(instance, mixinLodash, applyChain) : instance
})
return stayChained ? chain : chain.value()
}
static attach(project, options = {}) {
return Helper.attach(project, Model, {
registryProp: 'models',
lookupProp: 'model',
registry: options.registry || Helper.createContextRegistry('models', {
context: require.context('../models', true, /\.js$/),
}),
...options,
})
}
}
export default Model
export function validate (providerModule = {}) {
const report = { pass: true, errors: [], warnings: [] }
if (!has(providerModule, 'routes')) {
report.pass = false
report.errors.push(`Model modules must export a routes object`)
}
return report
}