UNPKG

@leansdk/leanrc

Version:

LeanRC is a MVC framework for creating graceful applications

568 lines (472 loc) 22.3 kB
# 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 @inheritProtected() # Place for attributes and computeds definitions @attribute title: String, validate: -> joi.string() # !!! нужен для сложной валидации данных # transform указывать не надо, т.к. стандартный тип, Module::StringTransform @attribute nameObj: Module::NameObj, validate: -> joi.object().required().start().end().default({}) transform: -> Module::NameObjTransform # or some record class Module::OnionRecord @attribute description: String @attribute registeredAt: Date, validate: -> joi.date().iso() transform: -> Module::MyDateTransform TomatoEntryMixin.initializeMixin() ``` ```coffee module.exports = (Module)-> { Record TomatoEntryMixin } = Module:: class TomatoRecord extends Record @inheritProtected() @include TomatoEntryMixin @module 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 @inheritProtected() @include ChainsMixin @implements RecordInterface @module Module ipoInternalRecord = PointerT @protected internalRecord: MaybeG Object ipoSchemas = PointerT @protected @static schemas: DictG(String, JoiT), default: {} @public collection: CollectionInterface @public @static schema: JoiT, get: -> @[ipoSchemas][@name] ?= do => vhAttrs = {} for own asAttr, ahValue of @attributes vhAttrs[asAttr] = do (asAttr, ahValue)=> if _.isFunction ahValue.validate ahValue.validate.call(@) else ahValue.validate for own asAttr, ahValue of @computeds vhAttrs[asAttr] = do (asAttr, ahValue)=> if _.isFunction ahValue.validate ahValue.validate.call(@) else ahValue.validate joi.object vhAttrs @[ipoSchemas][@name] @public @static @async 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 @name is ahPayload.type.split('::')[1] @ else @findRecordByName 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 @public @static @async 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 @public @static @async 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 @name is ahPayload.type.split('::')[1] @ else @findRecordByName 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 @public @static 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 @public @static 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 @public @static parseRecordName: FuncG(String, TupleG String, String), default: (asName)-> if /.*[:][:].*/.test(asName) [vsModuleName, vsRecordName] = asName.split '::' else [vsModuleName, vsRecordName] = [@moduleName(), inflect.camelize inflect.underscore inflect.singularize asName] unless /(Record$)|(Migration$)/.test vsRecordName vsRecordName += 'Record' [vsModuleName, vsRecordName] @public parseRecordName: FuncG(String, TupleG String, String), default: (args...)-> @constructor.parseRecordName args... @public @static findRecordByName: FuncG(String, SubsetG RecordInterface), default: (asName)-> [vsModuleName, vsRecordName] = @parseRecordName asName (@Module.NS ? @Module::)[vsRecordName] ? @ @public findRecordByName: FuncG(String, SubsetG RecordInterface), default: (asName)-> @constructor.findRecordByName asName ### @customFilter -> reason: '$eq': (value)-> # string of some aql code for example '$neq': (value)-> # string of some aql code for example ### @public @static customFilters: Object, get: -> @metaObject.getGroup 'customFilters', no @public @static customFilter: FuncG(Function), default: (amStatementFunc)-> config = amStatementFunc.call @ for own asFilterName, aoStatement of config @metaObject.addMetaData 'customFilters', asFilterName, aoStatement return @public @static parentClassNames: FuncG([MaybeG SubsetG RecordInterface], ListG String), default: (AbstractClass = null)-> AbstractClass ?= @ SuperClass = Reflect.getPrototypeOf AbstractClass fromSuper = unless _.isEmpty SuperClass?.name @parentClassNames SuperClass _.uniq [].concat(fromSuper ? []) .concat [AbstractClass.name] @public @static attributes: DictG(String, AttributeConfigT), get: -> @metaObject.getGroup 'attributes', no @public @static computeds: DictG(String, ComputedConfigT), get: -> @metaObject.getGroup 'computeds', no @public @static attribute: FuncG([PropertyDefinitionT, AttributeOptionsT]), default: -> @attr arguments... return @public @static 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 @attributes[vsAttr]? throw new Error "attribute `#{vsAttr}` has been defined previously" else @metaObject.addMetaData 'attributes', vsAttr, opts @public {[vsAttr]: Module::MaybeG vcAttrType}, opts return @public @static computed: FuncG([PropertyDefinitionT, ComputedOptionsT]), default: -> @comp arguments... return # NOTE: изначальная задумка была в том, чтобы определять вычисляемые значения - НЕ ПРОМИСЫ! (т.е. некоторое значение, которое отправляется в респонзе реально не хранится в базе, но вычисляется НЕ асинхронной функцией-гетером) @public @static 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 @computeds[vsAttr]? throw new Error "computed `#{vsAttr}` has been defined previously" else @metaObject.addMetaData 'computeds', vsAttr, opts @public {[vsAttr]: Module::MaybeG vcAttrType}, opts return @public @static new: FuncG([Object, CollectionInterface], RecordInterface), default: (aoAttributes, aoCollection)-> aoAttributes ?= {} unless aoAttributes.type? throw new Error "Attribute `type` is required and format 'ModuleName::RecordClassName'" if @name is aoAttributes.type.split('::')[1] @super aoAttributes, aoCollection else RecordClass = @findRecordByName aoAttributes.type if RecordClass is @ @super aoAttributes, aoCollection else RecordClass.new(aoAttributes, aoCollection) @public @async save: FuncG([], RecordInterface), default: -> result = if yield @isNew() yield @create() else yield @update() return result @public @async create: FuncG([], RecordInterface), default: -> # console.log '>>??? create push ', @, @collection response = yield @collection.push @ # response = yield @collection.push.body.call @collection, @ # console.log '>>>>?????????????????????', response, response.collection # console.log '>>>>????????????????????? is', CollectionInterface.is response.collection yield @reloadRecord response yield return @ @public @async update: FuncG([], RecordInterface), default: -> response = yield @collection.override @id, @ yield @reloadRecord response yield return @ @public @async delete: FuncG([], RecordInterface), default: -> if yield @isNew() throw new Error 'Document is not exist in collection' @isHidden = yes @updatedAt = new Date() yield @save() @public @async destroy: Function, default: -> if yield @isNew() throw new Error 'Document is not exist in collection' yield @collection.remove @id return @attribute id: UnionG(String, Number), transform: -> Module::StringTransform @attribute rev: String @attribute type: String @attribute isHidden: Boolean, validate: -> joi.boolean().empty(null).default(no, 'false by default') default: no @attribute createdAt: Date @attribute updatedAt: Date @attribute deletedAt: Date @chains ['create', 'update', 'delete', 'destroy'] @beforeHook 'beforeUpdate', only: ['update'] @beforeHook 'beforeCreate', only: ['create'] @afterHook 'afterUpdate', only: ['update'] @afterHook 'afterCreate', only: ['create'] @beforeHook 'beforeDelete', only: ['delete'] @afterHook 'afterDelete', only: ['delete'] @afterHook 'afterDestroy', only: ['destroy'] @public @async afterCreate: FuncG(RecordInterface, RecordInterface), default: (aoRecord)-> @collection.recordHasBeenChanged 'createdRecord', aoRecord yield return @ @public @async beforeUpdate: Function, default: (args...)-> @updatedAt = new Date() yield return args @public @async beforeCreate: Function, default: (args...)-> @id ?= yield @collection.generateId(@) now = new Date() @createdAt ?= now @updatedAt ?= now yield return args @public @async afterUpdate: FuncG(RecordInterface, RecordInterface), default: (aoRecord)-> @collection.recordHasBeenChanged 'updatedRecord', aoRecord yield return @ @public @async beforeDelete: Function, default: (args...)-> @isHidden = yes now = new Date() @updatedAt = now @deletedAt = now yield return args @public @async afterDelete: FuncG(RecordInterface, RecordInterface), default: (aoRecord)-> @collection.recordHasBeenChanged 'deletedRecord', aoRecord yield return @ @public @async afterDestroy: FuncG([]), default: -> @collection.recordHasBeenChanged 'destroyedRecord', @ yield return # NOTE: метод должен вернуть список атрибутов данного рекорда. @public attributes: FuncG([], Object), default: -> Object.keys @constructor.attributes # NOTE: в оперативной памяти создается клон рекорда, НО с другим id @public @async clone: FuncG([], RecordInterface), default: -> yield @collection.clone @ # NOTE: в коллекции создается копия рекорда, НО с другим id @public @async copy: FuncG([], RecordInterface), default: -> yield @collection.copy @ @public @async 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 @save() @public @async 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 @save() @public @async toggle: FuncG(String, RecordInterface), default: (asAttribute)-> unless _.isBoolean @[asAttribute] throw new Error "doc.attribute `#{asAttribute}` is not Boolean" @[asAttribute] = not @[asAttribute] yield @save() @public @async touch: FuncG([], RecordInterface), default: -> @updatedAt = new Date() yield @save() @public @async updateAttribute: FuncG([String, MaybeG AnyT], RecordInterface), default: (name, value)-> @[name] = value yield @save() @public @async updateAttributes: FuncG(Object, RecordInterface), default: (aoAttributes)-> for own vsAttrName, voAttrValue of aoAttributes @[vsAttrName] = voAttrValue yield @save() @public @async isNew: FuncG([], Boolean), default: -> return yes unless @id? return not (yield @collection.includes @id) @public @async reload: FuncG([], RecordInterface), default: -> return unless @id? response = yield @collection.take @id yield @reloadRecord response yield return @ @public @async reloadRecord: FuncG(UnionG Object, RecordInterface), default: (response)-> if response? for own asAttr of @constructor.attributes @[asAttr] = response[asAttr] @[ipoInternalRecord] = response[ipoInternalRecord] yield return # TODO: не учтены установки значений, которые раньше не были установлены @public @async changedAttributes: FuncG([], DictG String, Array), default: -> vhResult = {} for own vsAttrName, { transform } of @constructor.attributes voOldValue = @[ipoInternalRecord]?[vsAttrName] voNewValue = transform.call(@constructor).objectize @[vsAttrName] unless _.isEqual voNewValue, voOldValue vhResult[vsAttrName] = [voOldValue, voNewValue] yield return vhResult @public @async resetAttribute: FuncG(String), default: (asAttribute)-> if @[ipoInternalRecord]? if (attrConf = @constructor.attributes[asAttribute])? { transform } = attrConf @[asAttribute] = yield transform.call(@constructor).normalize @[ipoInternalRecord][asAttribute] yield return @public @async rollbackAttributes: Function, default: -> if @[ipoInternalRecord]? for own vsAttrName, { transform } of @constructor.attributes voOldValue = @[ipoInternalRecord][vsAttrName] @[vsAttrName] = yield transform.call(@constructor).normalize voOldValue yield return @public @static @async restoreObject: FuncG([SubsetG(Module), Object], RecordInterface), default: (Module, replica)-> if replica?.class is @name 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 @super Module, replica @public @static @async replicateObject: FuncG(RecordInterface, Object), default: (instance)-> replica = yield @super 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 @public init: FuncG([Object, CollectionInterface]), default: (aoProperties, aoCollection) -> @super arguments... @collection = aoCollection for own vsAttrName, voAttrValue of aoProperties @[vsAttrName] = voAttrValue return @public toJSON: FuncG([], Object), { default: -> @constructor.objectize @ } @initialize()