grapher/lib/links/linker.js

329 lines
8.8 KiB
JavaScript
Raw Normal View History

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';
2016-09-15 09:14:15 +03:00
import createFieldSchema from './lib/createFieldSchema.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
*/
constructor(mainCollection, linkName, linkConfig)
{
this.mainCollection = mainCollection;
this.linkConfig = linkConfig;
this.linkName = linkName;
// check linkName must not exist in schema
this._validateAndClean();
this._extendSchema();
// if it's a virtual field make sure that when this is deleted, it will be removed from the references
if (this.isVirtual()) {
2016-09-16 19:22:12 +03:00
this._handleReferenceRemovalForVirtualLinks();
} else {
this._initIndex();
this._initAutoremove();
2016-09-14 16:04:08 +03:00
}
}
/**
* Values which represent for the relation a single link
* @returns {string[]}
*/
get oneTypes()
{
return ['one', '1', 'single'];
}
/**
* Returns the strategies: one, many, one-meta, many-meta
* @returns {string}
*/
get strategy()
{
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
*/
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
*/
getLinkedCollection()
{
// 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.
*/
isMany()
{
return !this.isSingle();
}
2016-09-21 18:33:50 +03:00
/**
* If the relationship for this link contains metadata
*/
isMeta()
{
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}
*/
isSingle()
{
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}
*/
isVirtual()
{
return !!this.linkConfig.inversedBy;
}
/**
* @returns {boolean}
*/
isResolver()
{
return _.isFunction(this.linkConfig.resolve);
}
/**
* @param object
* @returns {*}
*/
2016-09-21 18:33:50 +03:00
createLink(object, collection)
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
*/
_validateAndClean()
{
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.`)
}
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.
*/
_prepareVirtual()
{
const {collection, inversedBy} = this.linkConfig;
const linker = collection.getLinker(inversedBy);
_.extend(this.linkConfig, {
metadata: linker.linkConfig.metadata,
relatedLinker: linker
});
}
/**
* Depending on the strategy, we create the proper helper class
* @private
*/
_getHelperClass()
{
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
*/
_extendSchema()
{
if (this.isVirtual() || this.isResolver()) {
return;
}
if (this.mainCollection.simpleSchema && this.mainCollection.simpleSchema()) {
2016-09-15 09:14:15 +03:00
const schema = createFieldSchema(this.isMany(), this.linkConfig.metadata);
this.mainCollection.attachSchema({
[this.linkConfig.field]: schema
});
2016-09-14 16:04:08 +03:00
}
}
/**
* If field name not present, we generate it.
* @private
*/
_generateFieldName()
{
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
*/
2016-09-16 19:22:12 +03:00
_handleReferenceRemovalForVirtualLinks()
2016-09-14 16:04:08 +03:00
{
this.mainCollection.after.remove((userId, doc) => {
let accessor = this.createLink(doc);
_.each(accessor.fetch(), linkedObj => {
const {relatedLinker} = this.linkConfig;
let link = relatedLinker.createLink(linkedObj);
if (relatedLinker.isSingle()) {
link.unset();
} else {
link.remove(doc);
}
});
})
}
2016-09-16 19:22:12 +03:00
_initIndex() {
2016-09-22 13:46:27 +03:00
if (Meteor.isServer) {
let field = this.linkConfig.field;
if (this.linkConfig.metadata) {
2016-09-22 13:46:27 +03:00
field = field + '._id';
}
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() {
if (this.linkConfig.autoremove) {
this.mainCollection.after.remove((userId, doc) => {
this.getLinkedCollection().remove({
_id: {
$in: smartArguments.getIds(doc[this.linkStorageField])
}
})
})
}
}
2016-09-14 16:04:08 +03:00
}