base-domain
Version:
simple module to help build Domain-Driven Design
384 lines (262 loc) • 10.7 kB
text/coffeescript
fs = require 'fs'
Path = require 'path'
Path.isAbsolute ?= (str) -> str.charAt(0) is '/'
{ camelize } = require './util'
Facade = require './main'
{ requireFile } = Facade
{ Base, BaseModel } = Facade
MasterDataResource = require './master-data-resource'
class ClassInfo
constructor: (@name, @relPath, @className, @moduleName) ->
Object.defineProperties @::,
modFullName: get: ->
if @moduleName
@moduleName + '/' + @name
else
@name
fullClassName: get: ->
camelize(@moduleName) + @className
class EntryGeneratorInput
constructor: (facadePath, dirname, outfile) ->
@validate(facadePath, dirname, outfile)
@absDirname = @absolutePath(dirname)
# public data
@absOutfilePath = @absolutePath(outfile)
@facadePath = @relativePath(facadePath)
@coreClasses = @getClassInfoList(@absDirname)
@modules = @getModulesClasses()
@facadeClassName = requireFile(@absolutePath(facadePath)).name
@facade = @createFacade()
@masterJSONStr = JSON.stringify @getMasterJSON()
@factories = @getPreferredFactoryNames()
createFacade: ->
allModules = {}
for moduleName in @getModuleNames()
allModules[moduleName] = Path.join(@absDirname, moduleName)
return Facade.createInstance
dirname: @absDirname
modules: allModules
master: true
###*
@return {Array(ClassInfo)}
###
getClassInfoList: (dirPath, moduleName = '') ->
relDirname = @relativePath(dirPath)
for filename in @getClassFiles(dirPath)
name = filename.split('.')[0]
relPath = relDirname + '/' + name
className = requireFile(Path.resolve dirPath, name).name
new ClassInfo(name, relPath, className, moduleName)
###*
@return {{[string]: Array(ClassInfo)}}
###
getModulesClasses: ->
modules = {}
for moduleName in @getModuleNames()
modulePath = Path.join(@absDirname, moduleName)
modules[moduleName] = @getClassInfoList(modulePath, moduleName)
return modules
###*
@method getMasterJSON
@private
@return {Object} master data
###
getMasterJSON: ->
try
{ masterJSONPath } = @facade.master
return null if not fs.existsSync(masterJSONPath)
return require(masterJSONPath)
catch e
return null
###*
@return {Array(string)} array of module names
###
getModuleNames: ->
fs.readdirSync(@absDirname)
.filter (subDirName) -> subDirName isnt 'master-data'
.filter (subDirName) -> subDirName isnt 'custom-roles'
.map (subDirname) => Path.join @absDirname, subDirname
.filter (subDirPath) -> fs.statSync(subDirPath).isDirectory()
.filter (subDirPath) ->
fs.readdirSync(subDirPath).some (filename) ->
klass = requireFile Path.join(subDirPath, filename)
klass.isBaseDomainClass
.map (subDirPath) -> Path.basename(subDirPath)
###*
get domain files to load
@method getClassFiles
@private
@return {Array(string)} filenames
###
getClassFiles: (path) ->
fileInfoDict = {}
for filename in fs.readdirSync(path)
[ name, ext ] = filename.split('.')
continue if ext not in ['js', 'coffee']
klass = requireFile path + '/' + filename
fileInfoDict[name] = filename: filename, klass: klass
files = []
for name, fileInfo of fileInfoDict
{ klass, filename } = fileInfo
continue if filename in files
ParentClass = Object.getPrototypeOf(klass::).constructor
if ParentClass.className and pntFileName = fileInfoDict[ParentClass.getName()]?.filename
files.push pntFileName unless pntFileName in files
files.push filename
return files
###*
get entities with no factory
###
getPreferredFactoryNames: ->
factories = {}
for classInfo in @coreClasses
factories[classInfo.modFullName] = @getPreferredFactoryName(classInfo)
for modName, classes of @modules
for classInfo in classes
factories[classInfo.modFullName] = @getPreferredFactoryName(classInfo)
delete factories[k] for k, v of factories when not v?
return factories
getPreferredFactoryName: (classInfo) ->
ModelClass = @facade.require(classInfo.modFullName)
return if (ModelClass::) not instanceof BaseModel
try
factory = @facade.createPreferredFactory(classInfo.modFullName)
return "'#{factory.constructor.className}'"
catch e
return 'null'
###*
validate input data
###
validate: (facadePath, dirname, outfile) ->
absFacadePath = @absolutePath facadePath
absDirname = @absolutePath dirname
outDir = Path.dirname(@absolutePath outfile)
throw new Error("'#{absFacadePath}' is not found.") if not fs.existsSync(absFacadePath)
throw new Error("dirname: '#{absDirname}' is not found.") if not fs.existsSync(absDirname)
throw new Error("output directory: '#{outDir}' is not found.") if not fs.existsSync(outDir)
absolutePath: (path) ->
return Path.resolve(path) if Path.isAbsolute path
return Path.resolve process.cwd(), path
relativePath: (path) ->
relPath = Path.relative(Path.dirname(@absOutfilePath), path)
if relPath.charAt(0) isnt '.'
relPath = './' + relPath
return relPath
class EntryGenerator
@generate: (facadePath, dirname, outfile, esCode = false) ->
input = new EntryGeneratorInput(facadePath, dirname, outfile)
if esCode
generator = new ESCodeGenerator(input, 'const')
else
generator = new JSCodeGenerator(input, 'var')
generator.generate()
constructor: (@input, @declaration) ->
generate: ->
code = [
@getPragmas()
@getImportStatements()
@getPackedData()
@getExportStatements()
].join('\n') + '\n'
fs.writeFileSync(@input.absOutfilePath, code)
getPragmas: -> ''
getPackedData: ->
{ factories, coreClasses, modules, masterJSONStr, facadeClassName } = @input
"""
#{@declaration} packedData = {
// eslint-disable-next-line quotes, key-spacing, object-curly-spacing, comma-spacing
masterData : #{masterJSONStr},
core: {
#{@getPackedCode(coreClasses, 2)},
},
modules: {
#{@getModulesPackedData(modules)}
},
factories: {
#{@getFactoriesPackedData(factories, 2)}
}
}
#{facadeClassName}.prototype.init = function init() { return this.initWithPacked(packedData) }
"""
getPackedCode: (classes, indent) ->
spaces = [0...indent * 4].map((x) -> ' ').join('')
spaces + classes.map (classInfo) ->
"'#{classInfo.name}': #{classInfo.fullClassName}"
.join(',\n' + spaces)
getModulesPackedData: (modules) ->
_ = ' '
Object.keys(modules).map (modName) =>
modClasses = modules[modName]
"""
#{_}'#{modName}': {
#{@getPackedCode(modClasses, 3)}
#{_}}
"""
.join(',\n')
getFactoriesPackedData: (factories, indent) ->
spaces = [0...indent * 4].map((x) -> ' ').join('')
spaces + Object.keys(factories).map (modelName) =>
factoryName = factories[modelName]
return "'#{modelName}': #{factoryName}"
.join(',\n' + spaces)
class JSCodeGenerator extends EntryGenerator
getPragmas: ->
"""
/* eslint quote-props: 0, object-shorthand: 0, no-underscore-dangle: 0 */
#{@declaration} __ = function __(m) { return m.default ? m.default : m }
"""
getImportStatements: ->
{ coreClasses, modules, facadePath, facadeClassName } = @input
# importing modules
code = @getRequireStatement(facadeClassName, facadePath)
code += @getRequireStatement(classInfo.className, classInfo.relPath) for classInfo in coreClasses
for modName, modClasses of modules
code += @getRequireStatement(classInfo.fullClassName, classInfo.relPath) for classInfo in modClasses
return code
getExportStatements: ->
{ coreClasses, modules, facadeClassName } = @input
classNames = coreClasses.map((coreClass) -> coreClass.className)
for modName, modClasses of modules
classNames = classNames.concat modClasses.map (modClass) -> modClass.fullClassName
keyValues = classNames.map (className) ->
"#{className}: #{className}"
return """
module.exports = {
"default": #{facadeClassName},
#{facadeClassName}: #{facadeClassName},
#{keyValues.join(',\n ')}
}
"""
getRequireStatement: (className, path) ->
return "#{@declaration} #{className} = __(require('#{path}'))\n"
class ESCodeGenerator extends EntryGenerator
getPragmas: ->
"""
// @flow
/* eslint quote-props: 0, max-len: 0 */
"""
getImportStatements: ->
{ coreClasses, modules, facadePath, facadeClassName } = @input
code = @getImportStatement(facadeClassName, facadePath)
code += @getImportStatement(classInfo.className, classInfo.relPath) for classInfo in coreClasses
for modName, modClasses of modules
code += @getImportStatement(classInfo.fullClassName, classInfo.relPath) for classInfo in modClasses
return code
getExportStatements: ->
{ coreClasses, modules, facadeClassName } = @input
classNames = coreClasses.map((coreClass) -> coreClass.className)
for modName, modClasses of modules
classNames = classNames.concat modClasses.map (modClass) -> modClass.fullClassName
return """
export default #{facadeClassName}
export {
#{facadeClassName},
#{classNames.join(',\n ')}
}
"""
###*
get import statement from className and path
###
getImportStatement: (className, path) ->
return "import #{className} from '#{path}'\n"
module.exports = EntryGenerator