2016-09-14 16:04:08 +03:00
import LinkMany from './linkTypes/linkMany.js' ;
import LinkManyMeta from './linkTypes/linkManyMeta.js' ;
import LinkOne from './linkTypes/linkOne.js' ;
import LinkOneMeta from './linkTypes/linkOneMeta.js' ;
import LinkResolve from './linkTypes/linkResolve.js' ;
import { linkStorage as linkStorageSymbol } from './symbols.js' ;
import ConfigSchema from './config.schema.js' ;
2017-06-14 11:42:29 +03:00
import attachFieldSchema from './lib/attachFieldSchema.js' ;
2016-09-16 19:22:12 +03:00
import smartArguments from './linkTypes/lib/smartArguments' ;
2016-09-14 16:04:08 +03:00
export default class Linker {
/ * *
* @ param mainCollection
* @ param linkName
* @ param linkConfig
* /
2017-01-04 11:51:16 +02:00
constructor ( mainCollection , linkName , linkConfig ) {
2016-09-14 16:04:08 +03:00
this . mainCollection = mainCollection ;
this . linkConfig = linkConfig ;
this . linkName = linkName ;
// check linkName must not exist in schema
this . _validateAndClean ( ) ;
this . _extendSchema ( ) ;
2016-11-29 11:16:23 +02:00
// initialize cascade removal hooks.
if ( linkConfig . autoremove ) {
this . _initAutoremove ( ) ;
}
2016-09-14 16:04:08 +03:00
if ( this . isVirtual ( ) ) {
2016-11-29 11:16:23 +02:00
// if it's a virtual field make sure that when this is deleted, it will be removed from the references
if ( ! linkConfig . autoremove ) {
this . _handleReferenceRemovalForVirtualLinks ( ) ;
}
2016-09-16 19:22:12 +03:00
} else {
this . _initIndex ( ) ;
2016-09-14 16:04:08 +03:00
}
}
/ * *
* Values which represent for the relation a single link
* @ returns { string [ ] }
* /
2017-01-04 11:51:16 +02:00
get oneTypes ( ) {
2016-09-14 16:04:08 +03:00
return [ 'one' , '1' , 'single' ] ;
}
/ * *
* Returns the strategies : one , many , one - meta , many - meta
* @ returns { string }
* /
2017-01-04 11:51:16 +02:00
get strategy ( ) {
2016-09-14 16:04:08 +03:00
if ( this . isResolver ( ) ) {
return 'resolver' ;
}
let strategy = this . isMany ( ) ? 'many' : 'one' ;
if ( this . linkConfig . metadata ) {
strategy += '-meta' ;
}
return strategy ;
}
/ * *
* Returns the field name in the document where the actual relationships are stored .
* @ returns string
* /
2017-01-04 11:51:16 +02:00
get linkStorageField ( ) {
2016-09-22 13:46:27 +03:00
if ( this . isVirtual ( ) ) {
2016-09-22 15:25:08 +03:00
return this . linkConfig . relatedLinker . linkStorageField ;
2016-09-22 13:46:27 +03:00
}
2016-09-14 16:04:08 +03:00
return this . linkConfig . field ;
}
/ * *
* The collection that is linked with the current collection
* @ returns Mongo . Collection
* /
2017-01-04 11:51:16 +02:00
getLinkedCollection ( ) {
2016-09-14 16:04:08 +03:00
// if our link is a resolver, then we really don't have a linked collection.
if ( this . isResolver ( ) ) {
return null ;
}
return this . linkConfig . collection ;
}
/ * *
* If the relationship for this link is of "many" type .
* /
2017-01-04 11:51:16 +02:00
isMany ( ) {
2016-09-14 16:04:08 +03:00
return ! this . isSingle ( ) ;
}
2016-09-21 18:33:50 +03:00
/ * *
* If the relationship for this link contains metadata
* /
2017-01-04 11:51:16 +02:00
isMeta ( ) {
2016-09-22 17:06:05 +03:00
if ( this . isVirtual ( ) ) {
return this . linkConfig . relatedLinker . isMeta ( ) ;
}
2016-09-21 18:33:50 +03:00
return ! ! this . linkConfig . metadata ;
}
2016-09-14 16:04:08 +03:00
/ * *
* @ returns { boolean }
* /
2017-01-04 11:51:16 +02:00
isSingle ( ) {
2016-09-22 17:06:05 +03:00
if ( this . isVirtual ( ) ) {
return this . linkConfig . relatedLinker . isSingle ( ) ;
}
2016-09-14 16:04:08 +03:00
return _ . contains ( this . oneTypes , this . linkConfig . type ) ;
}
/ * *
* @ returns { boolean }
* /
2017-01-04 11:51:16 +02:00
isVirtual ( ) {
2016-09-14 16:04:08 +03:00
return ! ! this . linkConfig . inversedBy ;
}
/ * *
* @ returns { boolean }
* /
2017-01-04 11:51:16 +02:00
isResolver ( ) {
2016-09-14 16:04:08 +03:00
return _ . isFunction ( this . linkConfig . resolve ) ;
}
2016-09-26 10:01:29 +03:00
/ * *
* Should return a single result .
* /
2017-01-04 11:51:16 +02:00
isOneResult ( ) {
2016-09-26 10:01:29 +03:00
return (
( this . isVirtual ( ) && this . linkConfig . relatedLinker . linkConfig . unique )
|| ( ! this . isVirtual ( ) && this . isSingle ( ) )
) ;
}
2016-09-14 16:04:08 +03:00
/ * *
* @ param object
2016-09-28 18:30:12 +03:00
* @ param collection To impersonate the getLinkedCollection ( ) of the "Linker"
*
* @ returns { LinkOne | LinkMany | LinkManyMeta | LinkOneMeta | LinkResolve }
2016-09-14 16:04:08 +03:00
* /
2017-01-04 11:51:16 +02:00
createLink ( object , collection = null ) {
2016-09-14 16:04:08 +03:00
let helperClass = this . _getHelperClass ( ) ;
2016-09-21 18:33:50 +03:00
return new helperClass ( this , object , collection ) ;
2016-09-14 16:04:08 +03:00
}
/ * *
* @ returns { * }
* @ private
* /
2017-01-04 11:51:16 +02:00
_validateAndClean ( ) {
2016-09-14 16:04:08 +03:00
if ( ! this . isResolver ( ) ) {
if ( ! this . linkConfig . collection ) {
throw new Meteor . Error ( 'invalid-config' , ` For the link ${ this . linkName } you did not provide a collection. Collection is mandatory for non-resolver links. ` )
}
2016-09-23 13:31:33 +03:00
if ( typeof ( this . linkConfig . collection ) === 'string' ) {
const collectionName = this . linkConfig . collection ;
this . linkConfig . collection = Mongo . Collection . get ( collectionName ) ;
if ( ! this . linkConfig . collection ) {
2016-11-21 13:28:15 +02:00
throw new Meteor . Error ( 'invalid-collection' , ` Could not find a collection with the name: ${ collectionName } ` ) ;
2016-09-23 13:31:33 +03:00
}
}
2016-09-14 16:04:08 +03:00
if ( this . isVirtual ( ) ) {
return this . _prepareVirtual ( ) ;
} else {
if ( ! this . linkConfig . type ) {
this . linkConfig . type = 'one' ;
}
if ( ! this . linkConfig . field ) {
this . linkConfig . field = this . _generateFieldName ( ) ;
} else {
if ( this . linkConfig . field == this . linkName ) {
throw new Meteor . Error ( 'invalid-config' , ` For the link ${ this . linkName } you must not use the same name for the field, otherwise it will cause conflicts when fetching data ` ) ;
}
}
}
}
ConfigSchema . validate ( this . linkConfig ) ;
}
/ * *
* We need to apply same type of rules in this case .
2017-01-04 11:51:16 +02:00
* @ private
2016-09-14 16:04:08 +03:00
* /
2017-01-04 11:51:16 +02:00
_prepareVirtual ( ) {
2016-09-14 16:04:08 +03:00
const { collection , inversedBy } = this . linkConfig ;
2017-01-04 11:51:16 +02:00
let linker = collection . getLinker ( inversedBy ) ;
if ( ! linker ) {
// it is possible that the collection doesn't have a linker created yet.
// so we will create it on startup after all links have been defined
Meteor . startup ( ( ) => {
linker = collection . getLinker ( inversedBy ) ;
if ( ! linker ) {
2017-03-03 10:00:30 +02:00
throw new Meteor . Error ( ` You tried setting up an inversed link in " ${ this . mainCollection . _name } " pointing to collection: " ${ collection . _name } " link: " ${ inversedBy } ", but no such link was found. Maybe a typo ? ` )
2017-01-04 11:51:16 +02:00
} else {
this . _setupVirtualConfig ( linker ) ;
}
} )
} else {
this . _setupVirtualConfig ( linker ) ;
}
}
2016-09-14 16:04:08 +03:00
2017-01-04 11:51:16 +02:00
/ * *
* @ param linker
* @ private
* /
_setupVirtualConfig ( linker ) {
2016-11-21 13:36:12 +02:00
const virtualLinkConfig = linker . linkConfig ;
2017-01-04 11:51:16 +02:00
2016-11-21 13:36:12 +02:00
if ( ! virtualLinkConfig ) {
throw new Meteor . Error ( ` There is no link-config for the related collection on ${ inversedBy } . Make sure you added the direct links before specifying virtual ones. ` )
}
2016-09-14 16:04:08 +03:00
_ . extend ( this . linkConfig , {
2016-11-21 13:36:12 +02:00
metadata : virtualLinkConfig . metadata ,
2016-09-14 16:04:08 +03:00
relatedLinker : linker
} ) ;
}
/ * *
* Depending on the strategy , we create the proper helper class
* @ private
* /
2017-01-04 11:51:16 +02:00
_getHelperClass ( ) {
2016-09-14 16:04:08 +03:00
switch ( this . strategy ) {
case 'resolver' :
return LinkResolve ;
case 'many-meta' :
return LinkManyMeta ;
case 'many' :
return LinkMany ;
case 'one-meta' :
return LinkOneMeta ;
case 'one' :
return LinkOne ;
}
throw new Meteor . Error ( 'invalid-strategy' , ` ${ this . strategy } is not a valid strategy ` ) ;
}
/ * *
* Extends the schema of the collection .
* @ private
* /
2017-01-04 11:51:16 +02:00
_extendSchema ( ) {
2016-09-14 16:04:08 +03:00
if ( this . isVirtual ( ) || this . isResolver ( ) ) {
return ;
}
if ( this . mainCollection . simpleSchema && this . mainCollection . simpleSchema ( ) ) {
2017-06-14 11:42:29 +03:00
attachFieldSchema ( this . mainCollection , this . linkConfig , this . isMany ( ) ) ;
2016-09-14 16:04:08 +03:00
}
}
/ * *
* If field name not present , we generate it .
* @ private
* /
2017-01-04 11:51:16 +02:00
_generateFieldName ( ) {
2016-09-14 16:04:08 +03:00
let cleanedCollectionName = this . linkConfig . collection . _name . replace ( /\./g , '_' ) ;
let defaultFieldPrefix = this . linkName + '_' + cleanedCollectionName ;
switch ( this . strategy ) {
case 'many-meta' :
return ` ${ defaultFieldPrefix } _metas ` ;
case 'many' :
return ` ${ defaultFieldPrefix } _ids ` ;
case 'one-meta' :
return ` ${ defaultFieldPrefix } _meta ` ;
case 'one' :
return ` ${ defaultFieldPrefix } _id ` ;
}
}
/ * *
* When a link that is declared virtual is removed , the reference will be removed from every other link .
* @ private
* /
2017-01-04 11:51:16 +02:00
_handleReferenceRemovalForVirtualLinks ( ) {
2016-09-14 16:04:08 +03:00
this . mainCollection . after . remove ( ( userId , doc ) => {
2017-03-03 10:00:30 +02:00
// this problem may occur when you do a .remove() before Meteor.startup()
if ( ! this . linkConfig . relatedLinker ) {
console . warn ( ` There was an error finding the link for removal for collection: " ${ this . mainCollection . _name } " with link: " ${ this . linkName } ". This may occur when you do a .remove() before Meteor.startup() ` ) ;
return ;
}
2016-09-14 16:04:08 +03:00
let accessor = this . createLink ( doc ) ;
_ . each ( accessor . fetch ( ) , linkedObj => {
const { relatedLinker } = this . linkConfig ;
2017-01-04 11:51:16 +02:00
// We do this check, to avoid self-referencing hell when defining virtual links
// Virtual links if not found "compile-time", we will try again to reprocess them on Meteor.startup
// if a removal happens before Meteor.startup this may fail
if ( relatedLinker ) {
let link = relatedLinker . createLink ( linkedObj ) ;
if ( relatedLinker . isSingle ( ) ) {
link . unset ( ) ;
} else {
link . remove ( doc ) ;
}
2016-09-14 16:04:08 +03:00
}
} ) ;
} )
}
2016-09-16 19:22:12 +03:00
_initIndex ( ) {
2016-09-22 13:46:27 +03:00
if ( Meteor . isServer ) {
2016-09-18 17:47:30 +03:00
let field = this . linkConfig . field ;
if ( this . linkConfig . metadata ) {
2016-09-22 13:46:27 +03:00
field = field + '._id' ;
2016-09-18 17:47:30 +03:00
}
2016-09-22 13:46:27 +03:00
if ( this . linkConfig . index ) {
if ( this . isVirtual ( ) ) {
throw new Meteor . Error ( 'You cannot set index on an inversed link.' ) ;
}
let options ;
if ( this . linkConfig . unique ) {
if ( this . isMany ( ) ) {
throw new Meteor . Error ( 'You cannot set unique property on a multi field.' ) ;
}
options = { unique : true }
}
this . mainCollection . _ensureIndex ( { [ field ] : 1 } , options ) ;
} else {
if ( this . linkConfig . unique ) {
if ( this . isVirtual ( ) ) {
throw new Meteor . Error ( 'You cannot set unique property on an inversed link.' ) ;
}
if ( this . isMany ( ) ) {
throw new Meteor . Error ( 'You cannot set unique property on a multi field.' ) ;
}
this . mainCollection . _ensureIndex ( {
[ field ] : 1
} , { unique : true } )
}
}
2016-09-16 19:22:12 +03:00
}
}
_initAutoremove ( ) {
2016-11-29 11:16:23 +02:00
if ( ! this . isVirtual ( ) ) {
2016-09-16 19:22:12 +03:00
this . mainCollection . after . remove ( ( userId , doc ) => {
this . getLinkedCollection ( ) . remove ( {
_id : {
$in : smartArguments . getIds ( doc [ this . linkStorageField ] )
}
} )
} )
2016-11-29 11:16:23 +02:00
} else {
this . mainCollection . after . remove ( ( userId , doc ) => {
const linker = this . mainCollection . getLink ( doc , this . linkName ) ;
const ids = linker . find ( { } , { fields : { _id : 1 } } ) . fetch ( ) . map ( item => item . _id ) ;
this . getLinkedCollection ( ) . remove ( {
_id : { $in : ids }
} )
} )
2016-09-16 19:22:12 +03:00
}
}
2017-03-02 10:43:47 +02:00
}