@leansdk/leanrc
Version:
LeanRC is a MVC framework for creating graceful applications
1,056 lines (943 loc) • 50 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/>.
# 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()