@leansdk/leanrc
Version:
LeanRC is a MVC framework for creating graceful applications
568 lines (472 loc) • 22.3 kB
text/coffeescript
# This file is part of LeanRC.
#
# LeanRC is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LeanRC is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with LeanRC. If not, see <https://www.gnu.org/licenses/>.
###
```coffee
module.exports = (Module)->
Module.defineMixin Module::Record, (BaseClass) ->
class TomatoEntryMixin extends BaseClass
# Place for attributes and computeds definitions
title: String,
validate: -> joi.string() # !!! нужен для сложной валидации данных
# transform указывать не надо, т.к. стандартный тип, Module::StringTransform
nameObj: Module::NameObj,
validate: -> joi.object().required().start().end().default({})
transform: -> Module::NameObjTransform # or some record class Module::OnionRecord
description: String
registeredAt: Date,
validate: -> joi.date().iso()
transform: -> Module::MyDateTransform
TomatoEntryMixin.initializeMixin()
```
```coffee
module.exports = (Module)->
{
Record
TomatoEntryMixin
} = Module::
class TomatoRecord extends Record
TomatoEntryMixin
Module
# business logic and before-, after- colbacks
TomatoRecord.initialize()
```
###
module.exports = (Module)->
{
AnyT, PointerT, JoiT
PropertyDefinitionT, AttributeOptionsT, ComputedOptionsT
AttributeConfigT, ComputedConfigT
FuncG, TupleG, MaybeG, SubsetG, DictG, ListG, UnionG
RecordInterface, CollectionInterface
CoreObject
ChainsMixin
Utils: { _, inflect, joi }
} = Module::
class Record extends CoreObject
ChainsMixin
RecordInterface
Module
ipoInternalRecord = PointerT internalRecord: MaybeG Object
ipoSchemas = PointerT schemas: DictG(String, JoiT),
default: {}
collection: CollectionInterface
schema: JoiT,
get: ->
@[ipoSchemas][] ?= do =>
vhAttrs = {}
for own asAttr, ahValue of
vhAttrs[asAttr] = do (asAttr, ahValue)=>
if _.isFunction ahValue.validate
ahValue.validate.call(@)
else
ahValue.validate
for own asAttr, ahValue of
vhAttrs[asAttr] = do (asAttr, ahValue)=>
if _.isFunction ahValue.validate
ahValue.validate.call(@)
else
ahValue.validate
joi.object vhAttrs
@[ipoSchemas][]
normalize: FuncG([MaybeG(Object), CollectionInterface], RecordInterface),
default: (ahPayload, aoCollection)->
unless ahPayload?
return null
vhAttributes = {}
unless ahPayload.type?
throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'"
RecordClass = if is ahPayload.type.split('::')[1]
@
else
ahPayload.type
for own asAttr, { transform } of RecordClass.attributes
vhAttributes[asAttr] = yield transform.call(RecordClass).normalize ahPayload[asAttr]
vhAttributes.type = ahPayload.type
# NOTE: vhAttributes processed before new - it for StateMachine in record (when it has)
voRecord = RecordClass.new vhAttributes, aoCollection
voRecord[ipoInternalRecord] = voRecord.constructor.makeSnapshot voRecord
yield return voRecord
serialize: FuncG([MaybeG RecordInterface], MaybeG Object),
default: (aoRecord)->
unless aoRecord?
return null
unless aoRecord.type?
throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'"
vhResult = {}
for own asAttr, { transform } of aoRecord.constructor.attributes
vhResult[asAttr] = yield transform.call(@).serialize aoRecord[asAttr]
yield return vhResult
recoverize: FuncG([MaybeG(Object), CollectionInterface], MaybeG RecordInterface),
default: (ahPayload, aoCollection)->
unless ahPayload?
return null
vhAttributes = {}
unless ahPayload.type?
throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'"
RecordClass = if is ahPayload.type.split('::')[1]
@
else
ahPayload.type
for own asAttr, { transform } of RecordClass.attributes when asAttr of ahPayload
vhAttributes[asAttr] = yield transform.call(RecordClass).normalize ahPayload[asAttr]
vhAttributes.type = ahPayload.type
# NOTE: vhAttributes processed before new - it for StateMachine in record (when it has)
voRecord = RecordClass.new vhAttributes, aoCollection
yield return voRecord
objectize: FuncG([MaybeG(RecordInterface), MaybeG Object], MaybeG Object),
default: (aoRecord)->
unless aoRecord?
return null
unless aoRecord.type?
throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'"
vhResult = {}
for own asAttr, { transform } of aoRecord.constructor.attributes
vhResult[asAttr] = transform.call(@).objectize aoRecord[asAttr]
for own asAttr, { transform } of aoRecord.constructor.computeds
vhResult[asAttr] = transform.call(@).objectize aoRecord[asAttr]
return vhResult
makeSnapshot: FuncG([MaybeG RecordInterface], MaybeG Object),
default: (aoRecord)->
unless aoRecord?
return null
unless aoRecord.type?
throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'"
vhResult = {}
for own asAttr, { transform } of aoRecord.constructor.attributes
vhResult[asAttr] = transform.call(@).objectize aoRecord[asAttr]
vhResult
parseRecordName: FuncG(String, TupleG String, String),
default: (asName)->
if /.*[:][:].*/.test(asName)
[vsModuleName, vsRecordName] = asName.split '::'
else
[vsModuleName, vsRecordName] = [, inflect.camelize inflect.underscore inflect.singularize asName]
unless /(Record$)|(Migration$)/.test vsRecordName
vsRecordName += 'Record'
[vsModuleName, vsRecordName]
parseRecordName: FuncG(String, TupleG String, String),
default: (args...)-> .parseRecordName args...
findRecordByName: FuncG(String, SubsetG RecordInterface),
default: (asName)->
[vsModuleName, vsRecordName] = asName
(.NS ? ::)[vsRecordName] ? @
findRecordByName: FuncG(String, SubsetG RecordInterface),
default: (asName)-> .findRecordByName asName
###
->
reason:
'$eq': (value)->
# string of some aql code for example
'$neq': (value)->
# string of some aql code for example
###
customFilters: Object,
get: -> .getGroup 'customFilters', no
customFilter: FuncG(Function),
default: (amStatementFunc)->
config = amStatementFunc.call @
for own asFilterName, aoStatement of config
.addMetaData 'customFilters', asFilterName, aoStatement
return
parentClassNames: FuncG([MaybeG SubsetG RecordInterface], ListG String),
default: (AbstractClass = null)->
AbstractClass ?= @
SuperClass = Reflect.getPrototypeOf AbstractClass
fromSuper = unless _.isEmpty SuperClass?.name
SuperClass
_.uniq [].concat(fromSuper ? [])
.concat [AbstractClass.name]
attributes: DictG(String, AttributeConfigT),
get: -> .getGroup 'attributes', no
computeds: DictG(String, ComputedConfigT),
get: -> .getGroup 'computeds', no
attribute: FuncG([PropertyDefinitionT, AttributeOptionsT]),
default: ->
arguments...
return
attr: FuncG([PropertyDefinitionT, AttributeOptionsT]),
default: (typeDefinition, opts={})->
[vsAttr] = Object.keys typeDefinition
vcAttrType = typeDefinition[vsAttr]
# NOTE: это всего лишь автоматическое применение трансформа, если он не указан явно. здесь НЕ надо автоматически подставить нужный рекорд или кастомный трансформ - они если должны использоваться, должны быть указаны вручную в схеме рекорда программистом.
opts.transform ?= switch vcAttrType
when String, Date, Number, Boolean, Array, Object
-> Module::["#{vcAttrType.name}Transform"]
else
-> Module::Transform
opts.validate ?= -> opts.transform.call(@).schema
{set} = opts
opts.set = (aoData)->
{value:voData} = opts.validate.call(@).validate aoData
if _.isFunction set
set.apply @, [voData]
else
voData
if [vsAttr]?
throw new Error "attribute `#{vsAttr}` has been defined previously"
else
.addMetaData 'attributes', vsAttr, opts
{[vsAttr]: Module::MaybeG vcAttrType}, opts
return
computed: FuncG([PropertyDefinitionT, ComputedOptionsT]),
default: ->
arguments...
return
# NOTE: изначальная задумка была в том, чтобы определять вычисляемые значения - НЕ ПРОМИСЫ! (т.е. некоторое значение, которое отправляется в респонзе реально не хранится в базе, но вычисляется НЕ асинхронной функцией-гетером)
comp: FuncG([PropertyDefinitionT, ComputedOptionsT]),
default: (typeDefinition, opts)->
# [typeDefinition, ..., opts] = args
# if typeDefinition is opts
# typeDefinition = "#{opts.attr}": opts.attrType
[vsAttr] = Object.keys typeDefinition
vcAttrType = typeDefinition[vsAttr]
# NOTE: это всего лишь автоматическое применение трансформа, если он не указан явно. здесь не надо автоматически подставить нужный рекорд или кастомный трансформ - они если должны использоваться, должны быть указаны вручную в схеме рекорда программистом.
opts.transform ?= switch vcAttrType
when String, Date, Number, Boolean, Array, Object
-> Module::["#{vcAttrType.name}Transform"]
else
-> Module::Transform
opts.validate ?= -> opts.transform.call(@).schema.strip()
unless opts.get?
throw new Error 'getter `lambda` options is required'
if opts.set?
throw new Error 'setter `lambda` options is forbidden'
if [vsAttr]?
throw new Error "computed `#{vsAttr}` has been defined previously"
else
.addMetaData 'computeds', vsAttr, opts
{[vsAttr]: Module::MaybeG vcAttrType}, opts
return
new: FuncG([Object, CollectionInterface], RecordInterface),
default: (aoAttributes, aoCollection)->
aoAttributes ?= {}
unless aoAttributes.type?
throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'"
if is aoAttributes.type.split('::')[1]
aoAttributes, aoCollection
else
RecordClass = aoAttributes.type
if RecordClass is @
aoAttributes, aoCollection
else
RecordClass.new(aoAttributes, aoCollection)
save: FuncG([], RecordInterface),
default: ->
result = if yield
yield
else
yield
return result
create: FuncG([], RecordInterface),
default: ->
# console.log '>>??? create push ', @,
response = yield .push @
# response = yield .push.body.call , @
# console.log '>>>>?????????????????????', response, response.collection
# console.log '>>>>????????????????????? is', CollectionInterface.is response.collection
yield response
yield return @
update: FuncG([], RecordInterface),
default: ->
response = yield .override , @
yield response
yield return @
delete: FuncG([], RecordInterface),
default: ->
if yield
throw new Error 'Document is not exist in collection'
= yes
= new Date()
yield
destroy: Function,
default: ->
if yield
throw new Error 'Document is not exist in collection'
yield .remove
return
id: UnionG(String, Number),
transform: -> Module::StringTransform
rev: String
type: String
isHidden: Boolean,
validate: -> joi.boolean().empty(null).default(no, 'false by default')
default: no
createdAt: Date
updatedAt: Date
deletedAt: Date
['create', 'update', 'delete', 'destroy']
'beforeUpdate', only: ['update']
'beforeCreate', only: ['create']
'afterUpdate', only: ['update']
'afterCreate', only: ['create']
'beforeDelete', only: ['delete']
'afterDelete', only: ['delete']
'afterDestroy', only: ['destroy']
afterCreate: FuncG(RecordInterface, RecordInterface),
default: (aoRecord)->
.recordHasBeenChanged 'createdRecord', aoRecord
yield return @
beforeUpdate: Function,
default: (args...)->
= new Date()
yield return args
beforeCreate: Function,
default: (args...)->
?= yield .generateId(@)
now = new Date()
?= now
?= now
yield return args
afterUpdate: FuncG(RecordInterface, RecordInterface),
default: (aoRecord)->
.recordHasBeenChanged 'updatedRecord', aoRecord
yield return @
beforeDelete: Function,
default: (args...)->
= yes
now = new Date()
= now
= now
yield return args
afterDelete: FuncG(RecordInterface, RecordInterface),
default: (aoRecord)->
.recordHasBeenChanged 'deletedRecord', aoRecord
yield return @
afterDestroy: FuncG([]),
default: ->
.recordHasBeenChanged 'destroyedRecord', @
yield return
# NOTE: метод должен вернуть список атрибутов данного рекорда.
attributes: FuncG([], Object),
default: -> Object.keys .attributes
# NOTE: в оперативной памяти создается клон рекорда, НО с другим id
clone: FuncG([], RecordInterface),
default: -> yield .clone @
# NOTE: в коллекции создается копия рекорда, НО с другим id
copy: FuncG([], RecordInterface),
default: -> yield .copy @
decrement: FuncG([String, MaybeG Number], RecordInterface),
default: (asAttribute, step = 1)->
unless _.isNumber @[asAttribute]
throw new Error "doc.attribute `#{asAttribute}` is not Number"
@[asAttribute] -= step
yield
increment: FuncG([String, MaybeG Number], RecordInterface),
default: (asAttribute, step = 1)->
unless _.isNumber @[asAttribute]
throw new Error "doc.attribute `#{asAttribute}` is not Number"
@[asAttribute] += step
yield
toggle: FuncG(String, RecordInterface),
default: (asAttribute)->
unless _.isBoolean @[asAttribute]
throw new Error "doc.attribute `#{asAttribute}` is not Boolean"
@[asAttribute] = not @[asAttribute]
yield
touch: FuncG([], RecordInterface),
default: ->
= new Date()
yield
updateAttribute: FuncG([String, MaybeG AnyT], RecordInterface),
default: (name, value)->
@[name] = value
yield
updateAttributes: FuncG(Object, RecordInterface),
default: (aoAttributes)->
for own vsAttrName, voAttrValue of aoAttributes
@[vsAttrName] = voAttrValue
yield
isNew: FuncG([], Boolean),
default: ->
return yes unless ?
return not (yield .includes )
reload: FuncG([], RecordInterface),
default: ->
return unless ?
response = yield .take
yield response
yield return @
reloadRecord: FuncG(UnionG Object, RecordInterface),
default: (response)->
if response?
for own asAttr of .attributes
@[asAttr] = response[asAttr]
@[ipoInternalRecord] = response[ipoInternalRecord]
yield return
# TODO: не учтены установки значений, которые раньше не были установлены
changedAttributes: FuncG([], DictG String, Array),
default: ->
vhResult = {}
for own vsAttrName, { transform } of .attributes
voOldValue = @[ipoInternalRecord]?[vsAttrName]
voNewValue = transform.call().objectize @[vsAttrName]
unless _.isEqual voNewValue, voOldValue
vhResult[vsAttrName] = [voOldValue, voNewValue]
yield return vhResult
resetAttribute: FuncG(String),
default: (asAttribute)->
if @[ipoInternalRecord]?
if (attrConf = .attributes[asAttribute])?
{ transform } = attrConf
@[asAttribute] = yield transform.call().normalize @[ipoInternalRecord][asAttribute]
yield return
rollbackAttributes: Function,
default: ->
if @[ipoInternalRecord]?
for own vsAttrName, { transform } of .attributes
voOldValue = @[ipoInternalRecord][vsAttrName]
@[vsAttrName] = yield transform.call().normalize voOldValue
yield return
restoreObject: FuncG([SubsetG(Module), Object], RecordInterface),
default: (Module, replica)->
if replica?.class is and replica?.type is 'instance'
Facade = Module::ApplicationFacade ? Module::Facade
facade = Facade.getInstance replica.multitonKey
collection = facade.retrieveProxy replica.collectionName
if replica.isNew
# NOTE: оставлено временно для обратной совместимости. Понятно что в будущем надо эту ветку удалить.
instance = yield collection.build replica.attributes
else
instance = yield collection.find replica.id
yield return instance
else
return yield Module, replica
replicateObject: FuncG(RecordInterface, Object),
default: (instance)->
replica = yield instance
ipsMultitonKey = Symbol.for '~multitonKey'
replica.multitonKey = instance.collection[ipsMultitonKey]
replica.collectionName = instance.collection.getProxyName()
replica.isNew = yield instance.isNew()
if replica.isNew
throw new Error "Replicating record is `new`. It must be seved previously"
else
changedAttributes = yield instance.changedAttributes()
if (changedKeys = Object.keys changedAttributes).length > 0
throw new Error "Replicating record has changedAttributes #{changedKeys}"
replica.id = instance.id
yield return replica
init: FuncG([Object, CollectionInterface]),
default: (aoProperties, aoCollection) ->
arguments...
= aoCollection
for own vsAttrName, voAttrValue of aoProperties
@[vsAttrName] = voAttrValue
return
toJSON: FuncG([], Object), { default: -> .objectize @ }