UNPKG

@leansdk/leanrc

Version:

LeanRC is a MVC framework for creating graceful applications

1,056 lines (943 loc) 50 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/>. # NOTE: through источники для relatedTo и belongsTo связей с опцией through НАДО ОБЪЯВЛЯТЬ ЧЕРЕЗ hasEmbed чтобы корректно отрабатывал сеттер сохраняющий данные об айдишнике подвязанного объекта в промежуточную коллекцию module.exports = (Module)-> { PointerT, JoiT PropertyDefinitionT, EmbedOptionsT, EmbedConfigT FuncG, MaybeG, DictG, SubsetG, AsyncFuncG, ListG, UnionG EmbeddableInterface, RecordInterface, CollectionInterface, CursorInterface Record, Mixin Utils: { _, inflect, joi, co } } = Module:: Module.defineMixin Mixin 'EmbeddableRecordMixin', (BaseClass = Record) -> class extends BaseClass @inheritProtected() @implements EmbeddableInterface ipoInternalRecord = PointerT @instanceVariables['~internalRecord'].pointer @public @static schema: JoiT, default: joi.object() get: (_data)-> _data[@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 for own asAttr, ahValue of @embeddings vhAttrs[asAttr] = do (asAttr, ahValue)=> if _.isFunction ahValue.validate ahValue.validate.call(@) else ahValue.validate joi.object vhAttrs _data[@name] @public @static relatedEmbed: FuncG([PropertyDefinitionT, EmbedOptionsT]), default: (typeDefinition, opts={})-> [vsAttr] = Object.keys typeDefinition opts.refKey ?= 'id' opts.inverse ?= "#{inflect.pluralize inflect.camelize @name.replace(/Record$/, ''), no}" opts.inverseType ?= null # manually only string opts.attr ?= "#{vsAttr}Id" opts.embedding = 'relatedEmbed' opts.through ?= null opts.putOnly ?= no opts.loadOnly ?= no opts.recordName ?= FuncG([MaybeG String], String) (recordType = null)-> if recordType? recordClass = @findRecordByName recordType classNames = _.filter recordClass.parentClassNames(), (name)-> /.*Record$/.test name vsRecordName = classNames[1] # ['Record', 'FtRecord', 'SdRecord'] else [vsModuleName, vsRecordName] = @parseRecordName vsAttr vsRecordName opts.collectionName ?= FuncG([MaybeG String], String) (recordType = null)-> "#{ inflect.pluralize opts.recordName.call(@, recordType).replace /Record$/, '' }Collection" opts.validate = FuncG([], JoiT) -> if opts.inverseType? return Record.schema.unknown(yes).allow(null).optional() else EmbedRecord = @findRecordByName opts.recordName.call(@) return EmbedRecord.schema.allow(null).optional() opts.load = AsyncFuncG([], RecordInterface) co.wrap -> if opts.putOnly yield return null recordType = null if opts.inverseType? recordType = @[opts.inverseType] EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call @, recordType # NOTE: может быть ситуация, что hasOne связь не хранится в классическом виде атрибуте рекорда, а хранение вынесено в отдельную промежуточную коллекцию по аналогии с М:М , но с добавленным uniq констрейнтом на одном поле (чтобы эмулировать 1:М связи) { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: res = unless opts.through yield (yield EmbedsCollection.takeBy( "@doc.#{opts.refKey}": @[opts.attr] , $limit: 1 )).first() else # NOTE: метаданные о through в случае с релейшеном к одному объекту должны быть описаны с помощью метода relatedEmbed. Поэтому здесь идет обращение только к @constructor.embeddings through = @constructor.embeddings[opts.through[0]] unless through? throw new Error "Metadata about #{opts.through[0]} must be defined by `EmbeddableRecordMixin.relatedEmbed` method" ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] embedId = (yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[through.refKey] , $limit: 1 )).first())[opts.through[1].by] yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": embedId , $limit: 1 )).first() @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbed.load #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.put = AsyncFuncG([]) co.wrap -> if opts.loadOnly yield return EmbedsCollection = null EmbedRecord = null aoRecord = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbed.put #{vsAttr} embed #{JSON.stringify aoRecord}", LEVELS[DEBUG]) if aoRecord? if aoRecord.constructor is Object if opts.inverseType? unless aoRecord.type? throw new Error 'When set polymorphic relatedEmbed `type` is required' EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call @, aoRecord.type EmbedRecord = @findRecordByName aoRecord.type else EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) aoRecord.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" aoRecord = yield EmbedsCollection.build aoRecord unless opts.through aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if (yield aoRecord.isNew()) or Object.keys(yield aoRecord.changedAttributes()).length savedRecord = yield aoRecord.save() else savedRecord = aoRecord @[opts.attr] = savedRecord[opts.refKey] if opts.inverseType? @[opts.inverseType] = savedRecord.type else # NOTE: метаданные о through в случае с релейшеном к одному объекту должны быть описаны с помощью метода relatedEmbed. Поэтому здесь идет обращение только к @constructor.embeddings through = @constructor.embeddings[opts.through[0]] unless through? throw new Error "Metadata about #{opts.through[0]} must be defined by `EmbeddableRecordMixin.relatedEmbed` method" ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if yield aoRecord.isNew() savedRecord = yield aoRecord.save() yield ThroughCollection.create( "#{through.inverse}": @[through.refKey] "#{opts.through[1].by}": savedRecord[inverse.refKey] spaceId: @spaceId if @spaceId? teamId: @teamId if @teamId? spaces: @spaces creatorId: @creatorId editorId: @editorId ownerId: @ownerId ) else if Object.keys(yield aoRecord.changedAttributes()).length savedRecord = yield aoRecord.save() else savedRecord = aoRecord yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[through.refKey] "@doc.#{opts.through[1].by}": $ne: savedRecord[inverse.refKey] )).forEach co.wrap (voRecord)-> yield voRecord.destroy() yield return yield return opts.restore = AsyncFuncG([MaybeG Object], MaybeG RecordInterface) co.wrap (replica)-> EmbedsCollection = null EmbedRecord = null { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbed.restore #{vsAttr} replica #{JSON.stringify replica}", LEVELS[DEBUG]) res = if replica? if opts.inverseType? unless replica.type? throw new Error 'When set polymorphic relatedEmbed `type` is required' EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call @, replica.type EmbedRecord = @findRecordByName replica.type else EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) replica.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" yield EmbedsCollection.build replica else null @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbed.restore #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.replicate = FuncG([], Object) -> aoRecord = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbed.replicate #{vsAttr} embed #{JSON.stringify aoRecord}", LEVELS[DEBUG]) res = aoRecord.constructor.objectize aoRecord @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbed.replicate #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) res @metaObject.addMetaData 'embeddings', vsAttr, opts @public "#{vsAttr}": MaybeG UnionG RecordInterface, Object return @public @static relatedEmbeds: FuncG([PropertyDefinitionT, EmbedOptionsT]), default: (typeDefinition, opts={})-> [vsAttr] = Object.keys typeDefinition opts.refKey ?= 'id' opts.inverse ?= "#{inflect.pluralize inflect.camelize @name.replace(/Record$/, ''), no}" opts.inverseType ?= null # manually only string opts.attr ?= "#{inflect.pluralize inflect.camelize vsAttr, no}" opts.embedding = 'relatedEmbeds' opts.through ?= null opts.putOnly ?= no opts.loadOnly ?= no opts.recordName ?= FuncG([MaybeG String], String) (recordType = null)-> if recordType? recordClass = @findRecordByName recordType classNames = _.filter recordClass.parentClassNames(), (name)-> /.*Record$/.test name vsRecordName = classNames[1] # ['Record', 'FtRecord', 'SdRecord'] else [vsModuleName, vsRecordName] = @parseRecordName vsAttr vsRecordName opts.collectionName ?= FuncG([MaybeG String], String) (recordType = null)-> "#{ inflect.pluralize opts.recordName.call(@, recordType).replace /Record$/, '' }Collection" opts.validate = FuncG([], JoiT) -> if inverseType? return joi.array().items [ Record.schema.unknown(yes) joi.any().strip() ] else EmbedRecord = @findRecordByName opts.recordName.call(@) return joi.array().items [EmbedRecord.schema, joi.any().strip()] opts.load = AsyncFuncG([], ListG RecordInterface) co.wrap -> if opts.putOnly yield return null EmbedsCollection = null # NOTE: может быть ситуация, что hasOne связь не хранится в классическом виде атрибуте рекорда, а хранение вынесено в отдельную промежуточную коллекцию по аналогии с М:М , но с добавленным uniq констрейнтом на одном поле (чтобы эмулировать 1:М связи) { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: res = unless opts.through if opts.inverseType? for { id, inverseType } in @[opts.attr] EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call @, inverseType yield EmbedsCollection.take id else EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) yield (yield EmbedsCollection.takeBy( "@doc.#{opts.refKey}": $in: @[opts.attr] )).toArray() else through = @constructor.embeddings[opts.through[0]] ? @constructor.relations?[opts.through[0]] ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] embedIds = yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[through.refKey] )).map (voRecord)-> voRecord[opts.through[1].by] yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": $in: embedIds )).toArray() @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbeds.load #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.put = AsyncFuncG([]) co.wrap -> if opts.loadOnly yield return EmbedsCollection = null EmbedRecord = null alRecords = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbeds.put #{vsAttr} embeds #{JSON.stringify alRecords}", LEVELS[DEBUG]) if alRecords.length > 0 unless opts.through alRecordIds = [] for aoRecord in alRecords if aoRecord.constructor is Object if opts.inverseType? unless aoRecord.type? throw new Error 'When set polymorphic relatedEmbeds `type` is required' EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call @, aoRecord.type EmbedRecord = @findRecordByName aoRecord.type else EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) aoRecord.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" aoRecord = yield EmbedsCollection.build aoRecord aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if (yield aoRecord.isNew()) or Object.keys(yield aoRecord.changedAttributes()).length { id, type: inverseType } = yield aoRecord.save() else { id, type: inverseType } = aoRecord if opts.inverseType? alRecordIds.push { id, inverseType } else alRecordIds.push id @[opts.attr] = alRecordIds else through = @constructor.embeddings[opts.through[0]] ? @constructor.relations?[opts.through[0]] ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] alRecordIds = [] newRecordIds = [] for aoRecord in alRecords if aoRecord.constructor is Object aoRecord.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" aoRecord = yield EmbedsCollection.build aoRecord aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if yield aoRecord.isNew() savedRecord = yield aoRecord.save() alRecordIds.push savedRecord[inverse.refKey] newRecordIds.push savedRecord[inverse.refKey] else if Object.keys(yield aoRecord.changedAttributes()).length savedRecord = yield aoRecord.save() else savedRecord = aoRecord alRecordIds.push savedRecord[inverse.refKey] unless opts.putOnly yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[through.refKey] "@doc.#{opts.through[1].by}": $nin: alRecordIds )).forEach co.wrap (voRecord)-> yield voRecord.destroy() yield return for newRecordId in newRecordIds yield ThroughCollection.create( "#{through.inverse}": @[through.refKey] "#{opts.through[1].by}": newRecordId spaceId: @spaceId if @spaceId? teamId: @teamId if @teamId? spaces: @spaces creatorId: @creatorId editorId: @editorId ownerId: @ownerId ) yield return opts.restore = AsyncFuncG([MaybeG Object], ListG RecordInterface) co.wrap (replica)-> EmbedsCollection = null EmbedRecord = null { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbeds.restore #{vsAttr} replica #{JSON.stringify replica}", LEVELS[DEBUG]) res = if replica? and replica.length > 0 for item in replica if opts.inverseType? unless replica.type? throw new Error 'When set polymorphic relatedEmbeds `type` is required' EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call @, replica.type EmbedRecord = @findRecordByName replica.type else EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) item.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" yield EmbedsCollection.build item else [] @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbeds.restore #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.replicate = FuncG([], ListG Object) -> alRecords = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbeds.replicate #{vsAttr} embed #{JSON.stringify alRecords}", LEVELS[DEBUG]) res = for item in alRecords EmbedRecord = item.constructor EmbedRecord.objectize item @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.relatedEmbeds.replicate #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) res @metaObject.addMetaData 'embeddings', vsAttr, opts @public "#{vsAttr}": MaybeG ListG UnionG RecordInterface, Object return @public @static hasEmbed: FuncG([PropertyDefinitionT, EmbedOptionsT]), default: (typeDefinition, opts={})-> [vsAttr] = Object.keys typeDefinition opts.refKey ?= 'id' opts.inverse ?= "#{inflect.singularize inflect.camelize @name.replace(/Record$/, ''), no}Id" opts.attr = null opts.inverseType ?= null # manually only string opts.embedding = 'hasEmbed' opts.through ?= null opts.putOnly ?= no opts.loadOnly ?= no opts.recordName ?= FuncG([MaybeG String], String) (recordType = null)-> if recordType? recordClass = @findRecordByName recordType classNames = _.filter recordClass.parentClassNames(), (name)-> /.*Record$/.test name vsRecordName = classNames[1] # ['Record', 'FtRecord', 'SdRecord'] else [vsModuleName, vsRecordName] = @parseRecordName vsAttr vsRecordName opts.collectionName ?= FuncG([MaybeG String], String) (recordType = null)-> "#{ inflect.pluralize opts.recordName.call(@, recordType).replace /Record$/, '' }Collection" opts.validate = FuncG([], JoiT) -> if opts.inverseType? return Record.schema.unknown(yes).allow(null).optional() else EmbedRecord = @findRecordByName opts.recordName.call(@) return EmbedRecord.schema.allow(null).optional() opts.load = AsyncFuncG([], RecordInterface) co.wrap -> if opts.putOnly yield return null EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) # NOTE: может быть ситуация, что hasOne связь не хранится в классическом виде атрибуте рекорда, а хранение вынесено в отдельную промежуточную коллекцию по аналогии с М:М , но с добавленным uniq констрейнтом на одном поле (чтобы эмулировать 1:М связи) { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: res = unless opts.through query = "@doc.#{opts.inverse}": @[opts.refKey] if inverseType? query["@doc.#{opts.inverseType}"] = @type yield (yield EmbedsCollection.takeBy( query, $limit: 1 )).first() else # NOTE: метаданные о through в случае с релейшеном к одному объекту должны быть описаны с помощью метода hasEmbed. Поэтому здесь идет обращение только к @constructor.embeddings through = @constructor.embeddings[opts.through[0]] unless through? throw new Error "Metadata about #{opts.through[0]} must be defined by `EmbeddableRecordMixin.hasEmbed` method" ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] embedId = (yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[opts.refKey] , $limit: 1 )).first())[opts.through[1].by] yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": embedId , $limit: 1 )).first() @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbed.load #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.put = AsyncFuncG([]) co.wrap -> if opts.loadOnly yield return EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) aoRecord = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbed.put #{vsAttr} embed #{JSON.stringify aoRecord}", LEVELS[DEBUG]) if aoRecord? if aoRecord.constructor is Object aoRecord.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" aoRecord = yield EmbedsCollection.build aoRecord unless opts.through aoRecord[opts.inverse] = @[opts.refKey] aoRecord[opts.inverseType] = @type if opts.inverseType? aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if (yield aoRecord.isNew()) or Object.keys(yield aoRecord.changedAttributes()).length savedRecord = yield aoRecord.save() else savedRecord = aoRecord query = "@doc.#{opts.inverse}": @[opts.refKey] "@doc.id": $ne: savedRecord.id # NOTE: проверяем по айдишнику только-что сохраненного if inverseType? query["@doc.#{opts.inverseType}"] = @type yield (yield EmbedsCollection.takeBy( query )).forEach co.wrap (voRecord)-> yield voRecord.destroy() else # NOTE: метаданные о through в случае с релейшеном к одному объекту должны быть описаны с помощью метода hasEmbed. Поэтому здесь идет обращение только к @constructor.embeddings through = @constructor.embeddings[opts.through[0]] unless through? throw new Error "Metadata about #{opts.through[0]} must be defined by `EmbeddableRecordMixin.hasEmbed` method" ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if yield aoRecord.isNew() savedRecord = yield aoRecord.save() yield ThroughCollection.create( "#{through.inverse}": @[opts.refKey] "#{opts.through[1].by}": savedRecord[inverse.refKey] spaceId: @spaceId if @spaceId? teamId: @teamId if @teamId? spaces: @spaces creatorId: @creatorId editorId: @editorId ownerId: @ownerId ) else if Object.keys(yield aoRecord.changedAttributes()).length savedRecord = yield aoRecord.save() else savedRecord = aoRecord embedIds = yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[opts.refKey] "@doc.#{opts.through[1].by}": $ne: savedRecord[inverse.refKey] )).map co.wrap (voRecord)-> id = voRecord[opts.through[1].by] yield voRecord.destroy() yield return id yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": $in: embedIds )).forEach co.wrap (voRecord)-> yield voRecord.destroy() else unless opts.putOnly unless opts.through voRecord = yield (yield EmbedsCollection.takeBy( "@doc.#{opts.inverse}": @[opts.refKey] , $limit: 1 )).first() if voRecord? yield voRecord.destroy() else # NOTE: метаданные о through в случае с релейшеном к одному объекту должны быть описаны с помощью метода hasEmbed. Поэтому здесь идет обращение только к @constructor.embeddings through = @constructor.embeddings[opts.through[0]] unless through? throw new Error "Metadata about #{opts.through[0]} must be defined by `EmbeddableRecordMixin.hasEmbed` method" ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] embedIds = yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[opts.refKey] , $limit: 1 )).map co.wrap (voRecord)-> id = voRecord[opts.through[1].by] yield voRecord.destroy() yield return id yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": $in: embedIds , $limit: 1 )).forEach co.wrap (voRecord)-> yield voRecord.destroy() yield return opts.restore = AsyncFuncG([MaybeG Object], MaybeG RecordInterface) co.wrap (replica)-> EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbed.restore #{vsAttr} replica #{JSON.stringify replica}", LEVELS[DEBUG]) res = if replica? replica.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" yield EmbedsCollection.build replica else null @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbed.restore #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.replicate = FuncG([], Object) -> aoRecord = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbed.replicate #{vsAttr} embed #{JSON.stringify aoRecord}", LEVELS[DEBUG]) res = aoRecord.constructor.objectize aoRecord @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbed.replicate #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) res @metaObject.addMetaData 'embeddings', vsAttr, opts @public "#{vsAttr}": MaybeG UnionG RecordInterface, Object return @public @static hasEmbeds: FuncG([PropertyDefinitionT, EmbedOptionsT]), default: (typeDefinition, opts={})-> [vsAttr] = Object.keys typeDefinition opts.refKey ?= 'id' opts.inverse ?= "#{inflect.singularize inflect.camelize @name.replace(/Record$/, ''), no}Id" opts.attr = null opts.inverseType ?= null # manually only string opts.embedding = 'hasEmbeds' opts.through ?= null opts.putOnly ?= no opts.loadOnly ?= no opts.recordName ?= FuncG([MaybeG String], String) (recordType = null)-> if recordType? recordClass = @findRecordByName recordType classNames = _.filter recordClass.parentClassNames(), (name)-> /.*Record$/.test name vsRecordName = classNames[1] # ['Record', 'FtRecord', 'SdRecord'] else [vsModuleName, vsRecordName] = @parseRecordName vsAttr vsRecordName opts.collectionName ?= FuncG([MaybeG String], String) (recordType = null)-> "#{ inflect.pluralize opts.recordName.call(@, recordType).replace /Record$/, '' }Collection" opts.validate = FuncG([], JoiT) -> if inverseType? return joi.array().items [ Record.schema.unknown(yes) joi.any().strip() ] else EmbedRecord = @findRecordByName opts.recordName.call(@) return joi.array().items [EmbedRecord.schema, joi.any().strip()] opts.load = AsyncFuncG([], ListG RecordInterface) co.wrap -> if opts.putOnly yield return [] EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: res = unless opts.through query = "@doc.#{opts.inverse}": @[opts.refKey] if inverseType? query["@doc.#{opts.inverseType}"] = @type yield (yield EmbedsCollection.takeBy( query )).toArray() else through = @constructor.embeddings[opts.through[0]] ? @constructor.relations?[opts.through[0]] ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] embedIds = yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[opts.refKey] )).map (voRecord)-> voRecord[opts.through[1].by] yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": $in: embedIds )).toArray() @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbeds.load #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.put = AsyncFuncG([]) co.wrap -> if opts.loadOnly yield return EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) alRecords = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbeds.put #{vsAttr} embeds #{JSON.stringify alRecords}", LEVELS[DEBUG]) if alRecords.length > 0 unless opts.through alRecordIds = [] for aoRecord in alRecords if aoRecord.constructor is Object aoRecord.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" aoRecord = yield EmbedsCollection.build aoRecord aoRecord[opts.inverse] = @[opts.refKey] aoRecord[opts.inverseType] = @type if opts.inverseType? aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if (yield aoRecord.isNew()) or Object.keys(yield aoRecord.changedAttributes()).length { id } = yield aoRecord.save() else { id } = aoRecord alRecordIds.push id unless opts.putOnly query = "@doc.#{opts.inverse}": @[opts.refKey] "@doc.id": $nin: alRecordIds # NOTE: проверяем айдишники всех только-что сохраненных if inverseType? query["@doc.#{opts.inverseType}"] = @type yield (yield EmbedsCollection.takeBy( query )).forEach co.wrap (voRecord)-> yield voRecord.destroy() else through = @constructor.embeddings[opts.through[0]] ? @constructor.relations?[opts.through[0]] ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] alRecordIds = [] newRecordIds = [] for aoRecord in alRecords if aoRecord.constructor is Object aoRecord.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" aoRecord = yield EmbedsCollection.build aoRecord aoRecord.spaceId = @spaceId if @spaceId? aoRecord.teamId = @teamId if @teamId? aoRecord.spaces = @spaces aoRecord.creatorId = @creatorId aoRecord.editorId = @editorId aoRecord.ownerId = @ownerId if yield aoRecord.isNew() savedRecord = yield aoRecord.save() alRecordIds.push savedRecord[inverse.refKey] newRecordIds.push savedRecord[inverse.refKey] else if Object.keys(yield aoRecord.changedAttributes()).length savedRecord = yield aoRecord.save() else savedRecord = aoRecord alRecordIds.push savedRecord[inverse.refKey] unless opts.putOnly embedIds = yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[opts.refKey] "@doc.#{opts.through[1].by}": $nin: alRecordIds )).map co.wrap (voRecord)-> id = voRecord[opts.through[1].by] yield voRecord.destroy() yield return id yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": $in: embedIds )).forEach co.wrap (voRecord)-> yield voRecord.destroy() for newRecordId in newRecordIds yield ThroughCollection.create( "#{through.inverse}": @[opts.refKey] "#{opts.through[1].by}": newRecordId spaceId: @spaceId if @spaceId? teamId: @teamId if @teamId? spaces: @spaces creatorId: @creatorId editorId: @editorId ownerId: @ownerId ) else unless opts.putOnly unless opts.through yield (yield EmbedsCollection.takeBy( "@doc.#{opts.inverse}": @[opts.refKey] )).forEach co.wrap (voRecord)-> yield voRecord.destroy() else through = @constructor.embeddings[opts.through[0]] ? @constructor.relations?[opts.through[0]] ThroughCollection = @collection.facade.retrieveProxy through.collectionName.call(@) ThroughRecord = @findRecordByName through.recordName.call(@) inverse = ThroughRecord.relations[opts.through[1].by] embedIds = yield (yield ThroughCollection.takeBy( "@doc.#{through.inverse}": @[opts.refKey] )).map co.wrap (voRecord)-> id = voRecord[opts.through[1].by] yield voRecord.destroy() yield return id yield (yield EmbedsCollection.takeBy( "@doc.#{inverse.refKey}": $in: embedIds )).forEach co.wrap (voRecord)-> yield voRecord.destroy() yield return opts.restore = AsyncFuncG([MaybeG Object], ListG RecordInterface) co.wrap (replica)-> EmbedsCollection = @collection.facade.retrieveProxy opts.collectionName.call(@) EmbedRecord = @findRecordByName opts.recordName.call(@) { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbeds.restore #{vsAttr} replica #{JSON.stringify replica}", LEVELS[DEBUG]) res = if replica? and replica.length > 0 for item in replica item.type ?= "#{EmbedRecord.moduleName()}::#{EmbedRecord.name}" yield EmbedsCollection.build item else [] @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbeds.restore #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) yield return res opts.replicate = FuncG([], ListG Object) -> alRecords = @[vsAttr] { LogMessage: { SEND_TO_LOG LEVELS DEBUG } } = Module:: @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbeds.replicate #{vsAttr} embeds #{JSON.stringify alRecords}", LEVELS[DEBUG]) res = for item in alRecords EmbedRecord = item.constructor EmbedRecord.objectize item @collection.sendNotification(SEND_TO_LOG, "EmbeddableRecordMixin.hasEmbeds.replicate #{vsAttr} result #{JSON.stringify res}", LEVELS[DEBUG]) res @metaObject.addMetaData 'embeddings', vsAttr, opts @public "#{vsAttr}": MaybeG ListG UnionG RecordInterface, Object return @public @static embeddings: DictG(String, EmbedConfigT), get: -> @metaObject.getGroup 'embeddings', no @public @static @async normalize: FuncG([MaybeG(Object), CollectionInterface], RecordInterface), default: (args...)-> voRecord = yield @super args... for own asAttr, { load } of voRecord.constructor.embeddings voRecord[asAttr] = yield load.call voRecord voRecord[ipoInternalRecord] = voRecord.constructor.makeSnapshotWithEmbeds voRecord yield return voRecord @public @static @async serialize: FuncG([MaybeG RecordInterface], MaybeG Object), default: (aoRecord)-> for own asAttr, { put } of aoRecord.constructor.embeddings yield put.call aoRecord vhResult = yield @super aoRecord yield return vhResult @public @static @async recoverize: FuncG([MaybeG(Object), CollectionInterface], MaybeG RecordInterface), default: (args...)-> [ahPayload] = args voRecord = yield @super args... for own asAttr, { restore } of voRecord.constructor.embeddings when asAttr of ahPayload voRecord[asAttr] = yield restore.call voRecord, ahPayload[asAttr] yield return voRecord @public @static objectize: FuncG([MaybeG(RecordInterface), MaybeG Object], MaybeG Object), default: (args...)-> [aoRecord] = args vhResult = @super args... for own asAttr, { replicate } of aoRecord.constructor.embeddings when aoRecord[asAttr]? vhResult[asAttr] = replicate.call aoRecord return vhResult @public @static makeSnapshotWithEmbeds: FuncG(RecordInterface, MaybeG Object), default: (aoRecord)-> vhResult = aoRecord[ipoInternalRecord] for own asAttr, { replicate } of aoRecord.constructor.embeddings vhResult[asAttr] = replicate.call aoRecord vhResult @public @async reloadRecord: FuncG(UnionG Object, RecordInterface), default: (response)-> yield @super response if response? for own asEmbed of @constructor.embeddings @[asEmbed] = response[asEmbed] @[ipoInternalRecord] = response[ipoInternalRecord] yield return # TODO: не учтены установки значений, которые раньше не были установлены @public @async changedAttributes: FuncG([], DictG String, Array), default: -> vhResult = yield @super() for own vsAttrName, { replicate } of @constructor.embeddings voOldValue = @[ipoInternalRecord]?[vsAttrName] voNewValue = replicate.call @ unless _.isEqual voNewValue, voOldValue vhResult[vsAttrName] = [voOldValue, voNewValue] yield return vhResult @public @async resetAttribute: FuncG(String), default: (args...)-> yield @super args... [asAttribute] = args if @[ipoInternalRecord]? if (attrConf = @constructor.embeddings[asAttribute])? { restore } = attrConf voOldValue = @[ipoInternalRecord][asAttribute] @[asAttribute] = yield restore.call @, voOldValue yield return @public @async rollbackAttributes: Function, default: (args...)-> yield @super args... if @[ipoInternalRecord]? for own vsAttrName, { restore } of @constructor.embeddings voOldValue = @[ipoInternalRecord][vsAttrName] @[vsAttrName] = yield restore.call @, voOldValue yield return @initializeMixin()