mirror of
https://github.com/vale981/grapher
synced 2025-03-04 09:01:40 -05:00
Modified createQuery to accept options and implemented resolver queries
This commit is contained in:
parent
9cc083420b
commit
4802724f97
39 changed files with 662 additions and 370 deletions
5
.npm/package/npm-shrinkwrap.json
generated
5
.npm/package/npm-shrinkwrap.json
generated
|
@ -21,6 +21,11 @@
|
|||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.0.tgz",
|
||||
"integrity": "sha1-bvSgmwX5iw41jW2T1Mo8rsZnKAM="
|
||||
},
|
||||
"dot-object": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-1.5.4.tgz",
|
||||
|
|
67
lib/createQuery.js
Normal file
67
lib/createQuery.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import Query from './query/query.js';
|
||||
import NamedQuery from './namedQuery/namedQuery.js';
|
||||
import NamedQueryStore from './namedQuery/store.js';
|
||||
|
||||
/**
|
||||
* This is a polymorphic function, it allows you to create a query as an object
|
||||
* or it also allows you to re-use an existing query if it's a named one
|
||||
*
|
||||
* @param args
|
||||
* @returns {*}
|
||||
*/
|
||||
export default (...args) => {
|
||||
if (typeof args[0] === 'string') {
|
||||
let [name, body, options] = args;
|
||||
options = options || {};
|
||||
|
||||
// It's a resolver query
|
||||
if (_.isFunction(body)) {
|
||||
return createNamedQuery(name, null, body, options);
|
||||
}
|
||||
|
||||
const entryPointName = _.first(_.keys(body));
|
||||
const collection = Mongo.Collection.get(entryPointName);
|
||||
|
||||
if (!collection) {
|
||||
throw new Meteor.Error('invalid-name', `We could not find any collection with the name "${entryPointName}". Make sure it is imported prior to using this`)
|
||||
}
|
||||
|
||||
return createNamedQuery(name, collection, body[entryPointName], options);
|
||||
} else {
|
||||
// Query Creation, it can have an endpoint as collection or as a NamedQuery
|
||||
let [body, options] = args;
|
||||
options = options || {};
|
||||
|
||||
const entryPointName = _.first(_.keys(body));
|
||||
const collection = Mongo.Collection.get(entryPointName);
|
||||
|
||||
if (!collection) {
|
||||
if (Meteor.isDevelopment && !NamedQueryStore.get(entryPointName)) {
|
||||
console.warn(`You are creating a query with the entry point "${entryPointName}", but there was no collection found for it (maybe you forgot to import it client-side?). It's assumed that it's referencing a NamedQuery.`)
|
||||
}
|
||||
|
||||
return createNamedQuery(entryPointName, null, {}, {params: body[entryPointName]});
|
||||
} else {
|
||||
return createNormalQuery(collection, body[entryPointName], options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createNamedQuery(name, collection, body, options = {}) {
|
||||
// if it exists already, we re-use it
|
||||
const namedQuery = NamedQueryStore.get(name);
|
||||
let query;
|
||||
|
||||
if (!namedQuery) {
|
||||
query = new NamedQuery(name, collection, body, options);
|
||||
NamedQueryStore.add(name, query);
|
||||
} else {
|
||||
query = namedQuery.clone(options.params);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function createNormalQuery(collection, body, options) {
|
||||
return new Query(collection, body, options);
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import { linkStorage } from '../links/symbols.js';
|
||||
import NamedQueryStore from '../namedQuery/store';
|
||||
import deepClone from 'lodash.clonedeep';
|
||||
|
||||
export default function extract() {
|
||||
return {
|
||||
namedQueries: extractNamedQueryDocumentation(),
|
||||
collections: extractCollectionDocumentation()
|
||||
}
|
||||
};
|
||||
|
||||
function extractNamedQueryDocumentation() {
|
||||
const namedQueries = NamedQueryStore.getAll();
|
||||
|
||||
let DocumentationObject = {};
|
||||
|
||||
_.each(namedQueries, namedQuery => {
|
||||
DocumentationObject[namedQuery.queryName] = {
|
||||
body: namedQuery.body,
|
||||
collection: namedQuery.collection._name,
|
||||
isExposed: namedQuery.isExposed,
|
||||
paramsSchema: (namedQuery.exposeConfig.schema)
|
||||
?
|
||||
formatSchemaType(
|
||||
deepClone(namedQuery.exposeConfig.schema)
|
||||
)
|
||||
: null
|
||||
};
|
||||
});
|
||||
|
||||
return DocumentationObject;
|
||||
}
|
||||
|
||||
function extractCollectionDocumentation() {
|
||||
const collections = Mongo.Collection.getAll();
|
||||
let DocumentationObject = {};
|
||||
|
||||
_.each(collections, ({name, instance}) => {
|
||||
if (name.substr(0, 7) == 'meteor_') {
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentationObject[name] = {};
|
||||
var isExposed = !!instance.__isExposedForGrapher;
|
||||
DocumentationObject[name]['isExposed'] = isExposed;
|
||||
|
||||
if (isExposed && instance.__exposure.config.body) {
|
||||
DocumentationObject[name]['exposureBody'] = deepClone(instance.__exposure.config.body);
|
||||
}
|
||||
|
||||
extractSchema(DocumentationObject[name], instance);
|
||||
extractLinks(DocumentationObject[name], instance);
|
||||
extractReducers(DocumentationObject[name], instance);
|
||||
});
|
||||
|
||||
return DocumentationObject;
|
||||
}
|
||||
|
||||
|
||||
function extractSchema(storage, collection) {
|
||||
storage.schema = {};
|
||||
|
||||
if (collection.simpleSchema && collection.simpleSchema()) {
|
||||
storage.schema = deepClone(collection.simpleSchema()._schema);
|
||||
|
||||
formatSchemaType(storage.schema);
|
||||
}
|
||||
}
|
||||
|
||||
function extractReducers(storage, collection) {
|
||||
storage.reducers = {};
|
||||
|
||||
if (collection.__reducers) {
|
||||
_.each(collection.__reducers, (value, key) => {
|
||||
storage.reducers[key] = {
|
||||
body: deepClone(value.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function formatSchemaType(schema) {
|
||||
_.each(schema, (value, key) => {
|
||||
if (value.type && value.type.name) {
|
||||
value.type = value.type.name;
|
||||
}
|
||||
});
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function extractLinks(storage, collection) {
|
||||
storage.links = {};
|
||||
const collectionLinkStorage = collection[linkStorage];
|
||||
|
||||
_.each(collectionLinkStorage, (linker, name) => {
|
||||
storage.links[name] = {
|
||||
collection: !linker.isResolver() ? linker.getLinkedCollection()._name : null,
|
||||
strategy: linker.strategy,
|
||||
metadata: linker.linkConfig.metadata,
|
||||
isVirtual: linker.isVirtual(),
|
||||
inversedBy: linker.linkConfig.inversedBy,
|
||||
isResolver: linker.isResolver(),
|
||||
resolverFunction: linker.isResolver() ? linker.linkConfig.resolve.toString() : null,
|
||||
isOneResult: linker.isOneResult(),
|
||||
linkStorageField: linker.linkStorageField
|
||||
}
|
||||
})
|
||||
}
|
|
@ -8,7 +8,9 @@ export const ExposureDefaults = {
|
|||
};
|
||||
|
||||
export const ExposureSchema = {
|
||||
firewall: Match.Maybe(Function),
|
||||
firewall: Match.Maybe(
|
||||
Match.OneOf(Function, [Function])
|
||||
),
|
||||
maxLimit: Match.Maybe(Match.Integer),
|
||||
maxDepth: Match.Maybe(Match.Integer),
|
||||
publication: Match.Maybe(Boolean),
|
||||
|
|
|
@ -215,9 +215,7 @@ export default class Exposure {
|
|||
|
||||
collection.firewall = (filters, options, userId) => {
|
||||
if (userId !== undefined) {
|
||||
if (firewall) {
|
||||
firewall.call({collection: collection}, filters, options, userId);
|
||||
}
|
||||
this._callFirewall({collection: collection}, filters, options, userId);
|
||||
|
||||
enforceMaxLimit(options, maxLimit);
|
||||
|
||||
|
@ -257,4 +255,22 @@ export default class Exposure {
|
|||
return findOne(filters, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_callFirewall(...args) {
|
||||
const {firewall} = this.config;
|
||||
if (!firewall) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isArray(firewall)) {
|
||||
firewall.forEach(fire => {
|
||||
fire.call(...args);
|
||||
})
|
||||
} else {
|
||||
firewall.call(...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
20
lib/extension.js
Normal file
20
lib/extension.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Query from './query/query.js';
|
||||
import NamedQuery from './namedQuery/namedQuery.js';
|
||||
import NamedQueryStore from './namedQuery/store.js';
|
||||
|
||||
_.extend(Mongo.Collection.prototype, {
|
||||
createQuery(...args) {
|
||||
if (typeof args[0] === 'string') {
|
||||
//NamedQuery
|
||||
const [name, body, options] = args;
|
||||
const query = new NamedQuery(name, this, body, options);
|
||||
NamedQueryStore.add(name, query);
|
||||
|
||||
return query;
|
||||
} else {
|
||||
const [body, params] = args;
|
||||
|
||||
return new Query(this, body, params);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import {Match} from 'meteor/check';
|
||||
import {Mongo} from 'meteor/mongo';
|
||||
|
||||
export const CacheSchema = {
|
||||
export const DenormalizeSchema = {
|
||||
field: String,
|
||||
body: Object,
|
||||
bypassSchema: Match.Maybe(Boolean)
|
||||
|
@ -23,5 +23,5 @@ export const LinkConfigSchema = {
|
|||
index: Match.Maybe(Boolean),
|
||||
unique: Match.Maybe(Boolean),
|
||||
autoremove: Match.Maybe(Boolean),
|
||||
cache: Match.Maybe(Match.ObjectIncluding(CacheSchema)),
|
||||
denormalize: Match.Maybe(Match.ObjectIncluding(DenormalizeSchema)),
|
||||
};
|
3
lib/links/constants.js
Normal file
3
lib/links/constants.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
LINK_STORAGE: '__links'
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import { Mongo } from 'meteor/mongo';
|
||||
import { linkStorage } from './symbols.js';
|
||||
import {LINK_STORAGE} from './constants.js';
|
||||
import Linker from './linker.js';
|
||||
|
||||
_.extend(Mongo.Collection.prototype, {
|
||||
|
@ -7,43 +7,43 @@ _.extend(Mongo.Collection.prototype, {
|
|||
* The data we add should be valid for config.schema.js
|
||||
*/
|
||||
addLinks(data) {
|
||||
if (!this[linkStorage]) {
|
||||
this[linkStorage] = {};
|
||||
if (!this[LINK_STORAGE]) {
|
||||
this[LINK_STORAGE] = {};
|
||||
}
|
||||
|
||||
_.each(data, (linkConfig, linkName) => {
|
||||
if (this[linkStorage][linkName]) {
|
||||
if (this[LINK_STORAGE][linkName]) {
|
||||
throw new Meteor.Error(`You cannot add the link with name: ${linkName} because it was already added to ${this._name} collection`)
|
||||
}
|
||||
|
||||
const linker = new Linker(this, linkName, linkConfig);
|
||||
|
||||
_.extend(this[linkStorage], {
|
||||
_.extend(this[LINK_STORAGE], {
|
||||
[linkName]: linker
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getLinks() {
|
||||
return this[linkStorage];
|
||||
return this[LINK_STORAGE];
|
||||
},
|
||||
|
||||
getLinker(name) {
|
||||
if (this[linkStorage]) {
|
||||
return this[linkStorage][name];
|
||||
if (this[LINK_STORAGE]) {
|
||||
return this[LINK_STORAGE][name];
|
||||
}
|
||||
},
|
||||
|
||||
hasLink(name) {
|
||||
if (!this[linkStorage]) {
|
||||
if (!this[LINK_STORAGE]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this[linkStorage][name];
|
||||
return !!this[LINK_STORAGE][name];
|
||||
},
|
||||
|
||||
getLink(objectOrId, name) {
|
||||
let linkData = this[linkStorage];
|
||||
let linkData = this[LINK_STORAGE];
|
||||
|
||||
if (!linkData) {
|
||||
throw new Meteor.Error(`There are no links defined for collection: ${this._name}`);
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class Linker {
|
|||
|
||||
// initialize cascade removal hooks.
|
||||
this._initAutoremove();
|
||||
this._initCache();
|
||||
this._initDenormalization();
|
||||
|
||||
if (this.isVirtual()) {
|
||||
// if it's a virtual field make sure that when this is deleted, it will be removed from the references
|
||||
|
@ -381,8 +381,12 @@ export default class Linker {
|
|||
}
|
||||
}
|
||||
|
||||
_initCache() {
|
||||
if (!this.linkConfig.cache || !Meteor.isServer) {
|
||||
/**
|
||||
* Initializes denormalization using herteby:denormalize
|
||||
* @private
|
||||
*/
|
||||
_initDenormalization() {
|
||||
if (!this.linkConfig.denormalize || !Meteor.isServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -391,7 +395,7 @@ export default class Linker {
|
|||
throw new Meteor.Error('missing-package', `Please add the herteby:denormalize package to your Meteor application in order to make caching work`)
|
||||
}
|
||||
|
||||
const {field, body, bypassSchema} = this.linkConfig.cache;
|
||||
const {field, body, bypassSchema} = this.linkConfig.denormalize;
|
||||
let cacheConfig;
|
||||
|
||||
let referenceFieldSuffix = '';
|
||||
|
@ -427,13 +431,13 @@ export default class Linker {
|
|||
}
|
||||
|
||||
/**
|
||||
* Verifies if this linker is cached. It can be cached from the inverse side as well.
|
||||
* Verifies if this linker is denormalized. It can be denormalized from the inverse side as well.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
isCached() {
|
||||
return !!this.linkConfig.cache;
|
||||
isDenormalized() {
|
||||
return !!this.linkConfig.denormalize;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -443,8 +447,8 @@ export default class Linker {
|
|||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
isSubBodyCache(body) {
|
||||
const cacheBody = this.linkConfig.cache.body;
|
||||
isSubBodyDenormalized(body) {
|
||||
const cacheBody = this.linkConfig.denormalize.body;
|
||||
|
||||
const cacheBodyFields = _.keys(dot.dot(cacheBody));
|
||||
const bodyFields = _.keys(dot.dot(body));
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
const linkStorage = Symbol('linkStorage');
|
||||
|
||||
export {linkStorage}
|
|
@ -8,9 +8,10 @@ import deepClone from 'lodash.clonedeep';
|
|||
import genCountEndpoint from '../../query/counts/genEndpoint.server';
|
||||
import {check} from 'meteor/check';
|
||||
|
||||
const specialParameters = ['$body'];
|
||||
|
||||
_.extend(NamedQuery.prototype, {
|
||||
/**
|
||||
* @param config
|
||||
*/
|
||||
expose(config = {}) {
|
||||
if (!Meteor.isServer) {
|
||||
throw new Meteor.Error('invalid-environment', `You must run this in server-side code`);
|
||||
|
@ -23,65 +24,94 @@ _.extend(NamedQuery.prototype, {
|
|||
this.exposeConfig = Object.assign({}, ExposeDefaults, config);
|
||||
check(this.exposeConfig, ExposeSchema);
|
||||
|
||||
if (this.exposeConfig.method) {
|
||||
if (this.exposeConfig.validateParams) {
|
||||
this.options.validateParams = this.exposeConfig.validateParams;
|
||||
}
|
||||
|
||||
if (!this.isResolver) {
|
||||
this._initNormalQuery();
|
||||
} else {
|
||||
this._initMethod();
|
||||
}
|
||||
|
||||
if (this.exposeConfig.publication) {
|
||||
this.isExposed = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes a normal NamedQuery (normal == not a resolver)
|
||||
* @private
|
||||
*/
|
||||
_initNormalQuery() {
|
||||
const config = this.exposeConfig;
|
||||
if (config.method) {
|
||||
this._initMethod();
|
||||
}
|
||||
|
||||
if (config.publication) {
|
||||
this._initPublication();
|
||||
}
|
||||
|
||||
if (!this.exposeConfig.method && !this.exposeConfig.publication) {
|
||||
if (!config.method && !config.publication) {
|
||||
throw new Meteor.Error('weird', 'If you want to expose your named query you need to specify at least one of ["method", "publication"] options to true')
|
||||
}
|
||||
|
||||
this._initCountMethod();
|
||||
this._initCountPublication();
|
||||
|
||||
if (this.exposeConfig.embody) {
|
||||
if (config.embody) {
|
||||
this.body = mergeDeep(
|
||||
deepClone(this.body),
|
||||
this.exposeConfig.embody
|
||||
config.embody
|
||||
);
|
||||
}
|
||||
|
||||
this.isExposed = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @private
|
||||
*/
|
||||
_unblockIfNecessary(context) {
|
||||
if (this.exposeConfig.unblock) {
|
||||
context.unblock();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_initMethod() {
|
||||
const self = this;
|
||||
Meteor.methods({
|
||||
[this.name](newParams) {
|
||||
this.unblock();
|
||||
self._unblockIfNecessary(this);
|
||||
|
||||
self._validateParams(newParams);
|
||||
|
||||
if (self.exposeConfig.firewall) {
|
||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
||||
}
|
||||
|
||||
return self.clone(newParams).fetch();
|
||||
// security is done in the fetching because we provide a context
|
||||
return self.clone(newParams).fetch(this);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
_initCountMethod() {
|
||||
const self = this;
|
||||
|
||||
Meteor.methods({
|
||||
[this.name + '.count'](newParams) {
|
||||
this.unblock();
|
||||
self._validateParams(newParams);
|
||||
self._unblockIfNecessary(this);
|
||||
|
||||
if (self.exposeConfig.firewall) {
|
||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
||||
}
|
||||
|
||||
return self.clone(newParams).getCount();
|
||||
// security is done in the fetching because we provide a context
|
||||
return self.clone(newParams).getCount(this);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_initCountPublication() {
|
||||
const self = this;
|
||||
|
||||
|
@ -92,27 +122,24 @@ _.extend(NamedQuery.prototype, {
|
|||
},
|
||||
|
||||
getSession(newParams) {
|
||||
self._validateParams(newParams);
|
||||
if (self.exposeConfig.firewall) {
|
||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
||||
}
|
||||
self.doValidateParams(newParams);
|
||||
self._callFirewall(this, this.userId, params);
|
||||
|
||||
return { params: newParams };
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_initPublication() {
|
||||
const self = this;
|
||||
|
||||
Meteor.publishComposite(this.name, function (newParams) {
|
||||
self._validateParams(newParams);
|
||||
Meteor.publishComposite(this.name, function (params) {
|
||||
self.doValidateParams(params);
|
||||
self._callFirewall(this, this.userId, params);
|
||||
|
||||
if (self.exposeConfig.firewall) {
|
||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
||||
}
|
||||
|
||||
let params = _.extend({}, self.params, newParams);
|
||||
const body = prepareForProcess(self.body, params);
|
||||
|
||||
const rootNode = createGraph(self.collection, body);
|
||||
|
@ -121,20 +148,24 @@ _.extend(NamedQuery.prototype, {
|
|||
});
|
||||
},
|
||||
|
||||
_validateParams(params) {
|
||||
if (this.exposeConfig.schema) {
|
||||
const paramsToValidate = _.omit(params, ...specialParameters);
|
||||
/**
|
||||
* @param context
|
||||
* @param userId
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
_callFirewall(context, userId, params) {
|
||||
const {firewall} = this.exposeConfig;
|
||||
if (!firewall) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
try {
|
||||
check(paramsToValidate, this._paramSchema);
|
||||
} catch (validationError) {
|
||||
console.error(`Invalid parameters supplied to query ${this.queryName}`, validationError);
|
||||
throw validationError; // rethrow
|
||||
}
|
||||
} else {
|
||||
check(paramsToValidate, this._paramSchema);
|
||||
}
|
||||
if (_.isArray(firewall)) {
|
||||
firewall.forEach(fire => {
|
||||
fire.call(context, userId, params);
|
||||
})
|
||||
} else {
|
||||
firewall.call(context, userId, params);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -3,12 +3,18 @@ import {Match} from 'meteor/check';
|
|||
export const ExposeDefaults = {
|
||||
publication: true,
|
||||
method: true,
|
||||
unblock: true,
|
||||
};
|
||||
|
||||
export const ExposeSchema = {
|
||||
firewall: Match.Maybe(Function),
|
||||
firewall: Match.Maybe(
|
||||
Match.OneOf(Function, [Function])
|
||||
),
|
||||
publication: Match.Maybe(Boolean),
|
||||
unblock: Match.Maybe(Boolean),
|
||||
method: Match.Maybe(Boolean),
|
||||
embody: Match.Maybe(Object),
|
||||
schema: Match.Maybe(Object),
|
||||
validateParams: Match.Maybe(
|
||||
Match.OneOf(Object, Function)
|
||||
),
|
||||
};
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import deepClone from 'lodash.clonedeep';
|
||||
|
||||
const specialParameters = ['$body'];
|
||||
|
||||
export default class NamedQueryBase {
|
||||
constructor(name, collection, body, params = {}) {
|
||||
constructor(name, collection, body, options = {}) {
|
||||
this.queryName = name;
|
||||
|
||||
this.body = deepClone(body);
|
||||
Object.freeze(this.body);
|
||||
if (_.isFunction(body)) {
|
||||
this.resolver = body;
|
||||
} else {
|
||||
this.body = deepClone(body);
|
||||
}
|
||||
|
||||
this.subscriptionHandle = null;
|
||||
this.params = params;
|
||||
this.params = options.params || {};
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
this.isExposed = false;
|
||||
}
|
||||
|
@ -17,22 +23,65 @@ export default class NamedQueryBase {
|
|||
return `named_query_${this.queryName}`;
|
||||
}
|
||||
|
||||
get isResolver() {
|
||||
return !!this.resolver;
|
||||
}
|
||||
|
||||
setParams(params) {
|
||||
this.params = _.extend({}, this.params, params);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the parameters
|
||||
*/
|
||||
doValidateParams(params) {
|
||||
params = params || this.params;
|
||||
params = _.omit(params, ...specialParameters);
|
||||
|
||||
const {validateParams} = this.options;
|
||||
if (!validateParams) return;
|
||||
|
||||
try {
|
||||
this._validate(validateParams, params);
|
||||
} catch (validationError) {
|
||||
console.error(`Invalid parameters supplied to the query "${this.queryName}"\n`, validationError);
|
||||
throw validationError; // rethrow
|
||||
}
|
||||
}
|
||||
|
||||
clone(newParams) {
|
||||
const params = _.extend({}, deepClone(this.params), newParams);
|
||||
|
||||
let clone = new this.constructor(
|
||||
this.queryName,
|
||||
this.collection,
|
||||
deepClone(this.body),
|
||||
_.extend({}, deepClone(this.params), newParams)
|
||||
this.isResolver ? this.resolver : deepClone(this.body),
|
||||
{
|
||||
...this.options,
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
clone.cacher = this.cacher;
|
||||
if (this.exposeConfig) {
|
||||
clone.exposeConfig = this.exposeConfig;
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function|object} validator
|
||||
* @param {object} params
|
||||
* @private
|
||||
*/
|
||||
_validate(validator, params) {
|
||||
if (_.isFunction(validator)) {
|
||||
validator.call(null, params)
|
||||
} else {
|
||||
check(params, validator)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,10 @@ export default class extends Base {
|
|||
* @returns {null|any|*}
|
||||
*/
|
||||
subscribe(callback) {
|
||||
if (this.isResolver) {
|
||||
throw new Meteor.Error('not-allowed', `You cannot subscribe to a resolver query`);
|
||||
}
|
||||
|
||||
this.subscriptionHandle = Meteor.subscribe(
|
||||
this.name,
|
||||
this.params,
|
||||
|
@ -30,6 +34,10 @@ export default class extends Base {
|
|||
* @returns {Object}
|
||||
*/
|
||||
subscribeCount(callback) {
|
||||
if (this.isResolver) {
|
||||
throw new Meteor.Error('not-allowed', `You cannot subscribe to a resolver query`);
|
||||
}
|
||||
|
||||
if (!this._counter) {
|
||||
this._counter = new CountSubscription(this);
|
||||
}
|
||||
|
|
|
@ -9,18 +9,26 @@ export default class extends Base {
|
|||
* Retrieves the data.
|
||||
* @returns {*}
|
||||
*/
|
||||
fetch() {
|
||||
const query = this.collection.createQuery(
|
||||
deepClone(this.body),
|
||||
deepClone(this.params)
|
||||
);
|
||||
fetch(context) {
|
||||
this._performSecurityChecks(context, this.params);
|
||||
|
||||
if (this.cacher) {
|
||||
const cacheId = generateQueryId(this.queryName, this.params);
|
||||
return this.cacher.get(cacheId, {query});
|
||||
if (this.isResolver) {
|
||||
return this._fetchResolverData(context);
|
||||
} else {
|
||||
const query = this.collection.createQuery(
|
||||
deepClone(this.body),
|
||||
{
|
||||
params: deepClone(this.params)
|
||||
}
|
||||
);
|
||||
|
||||
if (this.cacher) {
|
||||
const cacheId = generateQueryId(this.queryName, this.params);
|
||||
return this.cacher.get(cacheId, {query});
|
||||
}
|
||||
|
||||
return query.fetch();
|
||||
}
|
||||
|
||||
return query.fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,7 +44,9 @@ export default class extends Base {
|
|||
*
|
||||
* @returns {any}
|
||||
*/
|
||||
getCount() {
|
||||
getCount(context) {
|
||||
this._performSecurityChecks(context, this.params);
|
||||
|
||||
const countCursor = this.getCursorForCounting();
|
||||
|
||||
if (this.cacher) {
|
||||
|
@ -68,4 +78,51 @@ export default class extends Base {
|
|||
|
||||
this.cacher = cacher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure resolve. This doesn't actually call the resolver, it just sets it
|
||||
* @param fn
|
||||
*/
|
||||
resolve(fn) {
|
||||
if (!this.isResolver) {
|
||||
throw new Meteor.Error('invalid-call', `You cannot use resolve() on a non resolver NamedQuery`);
|
||||
}
|
||||
|
||||
this.resolver = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_fetchResolverData(context) {
|
||||
const resolver = this.resolver;
|
||||
const self = this;
|
||||
const query = {
|
||||
fetch() {
|
||||
return resolver.call(context, self.params);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.cacher) {
|
||||
const cacheId = generateQueryId(this.queryName, this.params);
|
||||
return this.cacher.get(cacheId, {query});
|
||||
}
|
||||
|
||||
return query.fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context Meteor method/publish context
|
||||
* @param params
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_performSecurityChecks(context, params) {
|
||||
if (context && this.exposeConfig) {
|
||||
this._callFirewall(context, context.userId, params);
|
||||
}
|
||||
|
||||
this.doValidateParams(params);
|
||||
}
|
||||
}
|
17
lib/namedQuery/testing/bootstrap/queries/index.js
Normal file
17
lib/namedQuery/testing/bootstrap/queries/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import postList from './postList';
|
||||
import postListCached from './postListCached';
|
||||
import postListExposure from './postListExposure';
|
||||
import postListParamsCheck from './postListParamsCheck';
|
||||
import postListParamsCheckServer from './postListParamsCheckServer';
|
||||
import postListResolver from './postListResolver';
|
||||
import postListResolverCached from './postListResolverCached';
|
||||
|
||||
export {
|
||||
postList,
|
||||
postListCached,
|
||||
postListExposure,
|
||||
postListParamsCheck,
|
||||
postListParamsCheckServer,
|
||||
postListResolver,
|
||||
postListResolverCached
|
||||
}
|
24
lib/namedQuery/testing/bootstrap/queries/postList.js
Normal file
24
lib/namedQuery/testing/bootstrap/queries/postList.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createQuery, MemoryResultCacher } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const postList = createQuery('postList', {
|
||||
posts: {
|
||||
$filter({filters, options, params}) {
|
||||
if (params.title) {
|
||||
filters.title = params.title;
|
||||
}
|
||||
|
||||
if (params.limit) {
|
||||
options.limit = params.limit;
|
||||
}
|
||||
},
|
||||
title: 1,
|
||||
author: {
|
||||
name: 1
|
||||
},
|
||||
group: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default postList;
|
13
lib/namedQuery/testing/bootstrap/queries/postListCached.js
Normal file
13
lib/namedQuery/testing/bootstrap/queries/postListCached.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { createQuery, MemoryResultCacher } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const postListCached = createQuery('postListCached', {
|
||||
posts: {
|
||||
title: 1,
|
||||
}
|
||||
});
|
||||
|
||||
postListCached.cacheResults(new MemoryResultCacher({
|
||||
ttl: 200,
|
||||
}));
|
||||
|
||||
export default postListCached;
|
|
@ -1,6 +1,6 @@
|
|||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
export default createQuery('postListExposure', {
|
||||
const postListExposure = createQuery('postListExposure', {
|
||||
posts: {
|
||||
title: 1,
|
||||
author: {
|
||||
|
@ -10,4 +10,18 @@ export default createQuery('postListExposure', {
|
|||
name: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
postListExposure.expose({
|
||||
firewall(userId, params) {
|
||||
},
|
||||
embody: {
|
||||
$filter({filters, params}) {
|
||||
filters.title = params.title
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default postListExposure;
|
|
@ -0,0 +1,19 @@
|
|||
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const postList = createQuery('postListResolverParamsCheck', () => {}, {
|
||||
validateParams: {
|
||||
title: String,
|
||||
}
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
postList.expose({});
|
||||
|
||||
postList.resolve(params => {
|
||||
return [
|
||||
params.title
|
||||
];
|
||||
})
|
||||
}
|
||||
|
||||
export default postList;
|
|
@ -0,0 +1,19 @@
|
|||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const postList = createQuery('postListResolverParamsCheckServer', () => {});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
postList.expose({
|
||||
validateParams: {
|
||||
title: String
|
||||
}
|
||||
});
|
||||
|
||||
postList.resolve(params => {
|
||||
return [
|
||||
params.title
|
||||
];
|
||||
})
|
||||
}
|
||||
|
||||
export default postList;
|
15
lib/namedQuery/testing/bootstrap/queries/postListResolver.js
Normal file
15
lib/namedQuery/testing/bootstrap/queries/postListResolver.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { createQuery, MemoryResultCacher } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const postList = createQuery('postListResolver', () => {});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
postList.expose({});
|
||||
|
||||
postList.resolve(params => {
|
||||
return [
|
||||
params.title
|
||||
];
|
||||
})
|
||||
}
|
||||
|
||||
export default postList;
|
|
@ -0,0 +1,19 @@
|
|||
import { createQuery, MemoryResultCacher } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const postList = createQuery('postListResolverCached', () => {});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
postList.expose({});
|
||||
|
||||
postList.resolve(params => {
|
||||
return [
|
||||
params.title
|
||||
];
|
||||
});
|
||||
|
||||
postList.cacheResults(new MemoryResultCacher({
|
||||
ttl: 200,
|
||||
}));
|
||||
}
|
||||
|
||||
export default postList;
|
|
@ -1,49 +1 @@
|
|||
import { createQuery, MemoryResultCacher } from 'meteor/cultofcoders:grapher';
|
||||
import postListExposure from './queries/postListExposure.js';
|
||||
|
||||
const postList = createQuery('postList', {
|
||||
posts: {
|
||||
$filter({filters, options, params}) {
|
||||
if (params.title) {
|
||||
filters.title = params.title;
|
||||
}
|
||||
|
||||
if (params.limit) {
|
||||
options.limit = params.limit;
|
||||
}
|
||||
},
|
||||
title: 1,
|
||||
author: {
|
||||
name: 1
|
||||
},
|
||||
group: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export { postList };
|
||||
export { postListExposure };
|
||||
|
||||
postListExposure.expose({
|
||||
firewall(userId, params) {
|
||||
},
|
||||
embody: {
|
||||
$filter({filters, params}) {
|
||||
filters.title = params.title
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const postListCached = createQuery('postListCached', {
|
||||
posts: {
|
||||
title: 1,
|
||||
}
|
||||
});
|
||||
|
||||
export {postListCached};
|
||||
|
||||
postListCached.cacheResults(new MemoryResultCacher({
|
||||
ttl: 200,
|
||||
}));
|
||||
import './queries';
|
|
@ -1,7 +1,13 @@
|
|||
import { postList, postListCached } from './bootstrap/server.js';
|
||||
import {
|
||||
postList,
|
||||
postListCached,
|
||||
postListResolver,
|
||||
postListResolverCached,
|
||||
postListParamsCheck,
|
||||
postListParamsCheckServer,
|
||||
} from './bootstrap/queries';
|
||||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
|
||||
describe('Named Query', function () {
|
||||
it('Should return the proper values', function () {
|
||||
const createdQuery = createQuery({
|
||||
|
@ -96,5 +102,58 @@ describe('Named Query', function () {
|
|||
assert.isObject(post.group);
|
||||
assert.isUndefined(post.group.createdAt);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Should work with resolver() queries with params', function () {
|
||||
const title = 'User Post - 3';
|
||||
const createdQuery = createQuery({
|
||||
postListResolver: {
|
||||
title
|
||||
}
|
||||
});
|
||||
|
||||
const directQuery = postListResolver.clone({
|
||||
title
|
||||
});
|
||||
|
||||
let data = createdQuery.fetch();
|
||||
assert.isArray(data);
|
||||
assert.equal(title, data[0]);
|
||||
|
||||
|
||||
data = directQuery.fetch();
|
||||
assert.isArray(data);
|
||||
assert.equal(title, data[0]);
|
||||
});
|
||||
|
||||
it('Should work with resolver() that is cached', function () {
|
||||
const title = 'User Post - 3';
|
||||
let data = postListResolverCached.clone({title}).fetch();
|
||||
|
||||
assert.isArray(data);
|
||||
assert.equal(title, data[0]);
|
||||
|
||||
data = postListResolverCached.clone({title}).fetch();
|
||||
|
||||
assert.isArray(data);
|
||||
assert.equal(title, data[0]);
|
||||
});
|
||||
|
||||
it('Should work with resolver() that has params validation', function (done) {
|
||||
try {
|
||||
postListParamsCheck.clone({}).fetch();
|
||||
} catch (e) {
|
||||
assert.isObject(e);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('Should work with resolver() that has params server-side validation', function (done) {
|
||||
try {
|
||||
postListParamsCheckServer.clone({}).fetch();
|
||||
} catch (e) {
|
||||
assert.isObject(e);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1 +1 @@
|
|||
export const COUNTS_COLLECTION_CLIENT = '$grapher.counts';
|
||||
export const COUNTS_COLLECTION_CLIENT = 'grapher_counts';
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import Query from './query.js';
|
||||
import NamedQuery from '../namedQuery/namedQuery.js';
|
||||
import NamedQueryStore from '../namedQuery/store.js';
|
||||
|
||||
export default (...args) => {
|
||||
let name;
|
||||
let body;
|
||||
let rest;
|
||||
if (typeof args[0] === 'string') { //NamedQuery
|
||||
name = args[0];
|
||||
body = args[1];
|
||||
rest = args.slice(2)
|
||||
} else { //Query
|
||||
body = args[0];
|
||||
rest = args.slice(1)
|
||||
}
|
||||
|
||||
if (_.keys(body).length > 1) {
|
||||
throw new Meteor.Error('invalid-query', 'When using createQuery you should only have one main root point that represents the collection name.')
|
||||
}
|
||||
|
||||
const entryPointName = _.first(_.keys(body));
|
||||
|
||||
const collection = Mongo.Collection.get(entryPointName);
|
||||
if (!collection) {
|
||||
if (name) { //is a NamedQuery
|
||||
throw new Meteor.Error('invalid-name', `We could not find any collection with the name "${entryPointName}". Make sure it is imported prior to using this`)
|
||||
}
|
||||
const namedQuery = NamedQueryStore.get(entryPointName);
|
||||
|
||||
if (!namedQuery) {
|
||||
throw new Meteor.Error('entry-point-not-found', `We could not find any collection or named query with the name "${entryPointName}". Make sure you have them loaded in the environment you are executing *createQuery*`)
|
||||
} else {
|
||||
return namedQuery.clone(body[entryPointName], ...rest);
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
const query = new NamedQuery(name, collection, body[entryPointName], ...rest);
|
||||
NamedQueryStore.add(name, query);
|
||||
|
||||
return query;
|
||||
} else {
|
||||
return new Query(collection, body[entryPointName], ...rest);
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import Query from './query.js';
|
||||
import NamedQuery from '../namedQuery/namedQuery.js';
|
||||
import NamedQueryStore from '../namedQuery/store.js';
|
||||
|
||||
_.extend(Mongo.Collection.prototype, {
|
||||
createQuery(...args) {
|
||||
if (typeof args[0] === 'string') {
|
||||
//NamedQuery
|
||||
const name = args[0];
|
||||
const body = args[1];
|
||||
const params = args[2];
|
||||
|
||||
const query = new NamedQuery(name, this, body, params);
|
||||
NamedQueryStore.add(name, query);
|
||||
|
||||
return query;
|
||||
} else {
|
||||
//Query
|
||||
const body = args[0];
|
||||
const params = args[1];
|
||||
|
||||
return new Query(this, body, params);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -36,13 +36,17 @@ export default class AggregateFilters {
|
|||
if (!this.isVirtual) {
|
||||
return {
|
||||
_id: {
|
||||
$in: _.pluck(this.parentObjects, this.linkStorageField)
|
||||
$in: _.uniq(
|
||||
_.pluck(this.parentObjects, this.linkStorageField)
|
||||
)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
[this.linkStorageField]: {
|
||||
$in: _.pluck(this.parentObjects, '_id')
|
||||
$in: _.uniq(
|
||||
_.pluck(this.parentObjects, '_id')
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -67,7 +71,7 @@ export default class AggregateFilters {
|
|||
});
|
||||
|
||||
return {
|
||||
_id: {$in: ids}
|
||||
_id: {$in: _.uniq(ids)}
|
||||
};
|
||||
} else {
|
||||
let filters = {};
|
||||
|
@ -78,7 +82,9 @@ export default class AggregateFilters {
|
|||
}
|
||||
|
||||
filters[this.linkStorageField + '._id'] = {
|
||||
$in: _.pluck(this.parentObjects, '_id')
|
||||
$in: _.uniq(
|
||||
_.pluck(this.parentObjects, '_id')
|
||||
)
|
||||
};
|
||||
|
||||
return filters;
|
||||
|
@ -90,14 +96,18 @@ export default class AggregateFilters {
|
|||
const arrayOfIds = _.pluck(this.parentObjects, this.linkStorageField);
|
||||
return {
|
||||
_id: {
|
||||
$in: _.union(...arrayOfIds)
|
||||
$in: _.uniq(
|
||||
_.union(...arrayOfIds)
|
||||
)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const arrayOfIds = _.pluck(this.parentObjects, '_id');
|
||||
return {
|
||||
[this.linkStorageField]: {
|
||||
$in: _.union(...arrayOfIds)
|
||||
$in: _.uniq(
|
||||
_.union(...arrayOfIds)
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -125,7 +135,7 @@ export default class AggregateFilters {
|
|||
});
|
||||
|
||||
return {
|
||||
_id: {$in: ids}
|
||||
_id: {$in: _.uniq(ids)}
|
||||
};
|
||||
} else {
|
||||
let filters = {};
|
||||
|
@ -136,7 +146,9 @@ export default class AggregateFilters {
|
|||
}
|
||||
|
||||
filters._id = {
|
||||
$in: _.pluck(this.parentObjects, '_id')
|
||||
$in: _.uniq(
|
||||
_.pluck(this.parentObjects, '_id')
|
||||
)
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -49,9 +49,9 @@ export function createNodes(root) {
|
|||
// check if it is a cached link
|
||||
// if yes, then we need to explicitly define this at collection level
|
||||
// so when we transform the data for delivery, we move it to the link name
|
||||
if (linker.isCached()) {
|
||||
if (linker.isSubBodyCache(body)) {
|
||||
const cacheField = linker.linkConfig.cache.field;
|
||||
if (linker.isDenormalized()) {
|
||||
if (linker.isSubBodyDenormalized(body)) {
|
||||
const cacheField = linker.linkConfig.denormalize.field;
|
||||
|
||||
root.snapCache(cacheField, fieldName);
|
||||
addFieldNode(body, cacheField, root);
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
import deepClone from 'lodash.clonedeep';
|
||||
import {check} from 'meteor/check';
|
||||
|
||||
export default class QueryBase {
|
||||
constructor(collection, body, params = {}) {
|
||||
constructor(collection, body, options = {}) {
|
||||
this.collection = collection;
|
||||
|
||||
this.body = deepClone(body);
|
||||
Object.freeze(this.body);
|
||||
|
||||
this._params = params;
|
||||
this.params = options.params || {};
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
clone(newParams) {
|
||||
const params = _.extend({}, deepClone(this.params), newParams);
|
||||
|
||||
return new this.constructor(
|
||||
this.collection,
|
||||
deepClone(this.body),
|
||||
_.extend({}, deepClone(this.params), newParams)
|
||||
{
|
||||
params,
|
||||
...this.options
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,18 +28,28 @@ export default class QueryBase {
|
|||
return `exposure_${this.collection._name}`;
|
||||
}
|
||||
|
||||
get params() {
|
||||
return this._params;
|
||||
/**
|
||||
* Validates the parameters
|
||||
*/
|
||||
doValidateParams() {
|
||||
const {validateParams} = this.options;
|
||||
if (!validateParams) return;
|
||||
|
||||
if (_.isFunction(validateParams)) {
|
||||
validateParams.call(null, this.params)
|
||||
} else {
|
||||
check(this.params)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the params with previous params.
|
||||
*
|
||||
* @param data
|
||||
* @param params
|
||||
* @returns {Query}
|
||||
*/
|
||||
setParams(data) {
|
||||
_.extend(this._params, data);
|
||||
setParams(params) {
|
||||
this.params = _.extend({}, this.params, params);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ export default class Query extends Base {
|
|||
* @returns {null|any|*}
|
||||
*/
|
||||
subscribe(callback) {
|
||||
this.doValidateParams();
|
||||
|
||||
this.subscriptionHandle = Meteor.subscribe(
|
||||
this.name,
|
||||
prepareForProcess(this.body, this.params),
|
||||
|
@ -30,6 +32,8 @@ export default class Query extends Base {
|
|||
* @returns {Object}
|
||||
*/
|
||||
subscribeCount(callback) {
|
||||
this.doValidateParams();
|
||||
|
||||
if (!this._counter) {
|
||||
this._counter = new CountSubscription(this);
|
||||
}
|
||||
|
@ -66,6 +70,8 @@ export default class Query extends Base {
|
|||
* @return {*}
|
||||
*/
|
||||
async fetchSync() {
|
||||
this.doValidateParams();
|
||||
|
||||
if (this.subscriptionHandle) {
|
||||
throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.');
|
||||
}
|
||||
|
@ -87,6 +93,8 @@ export default class Query extends Base {
|
|||
* @returns {*}
|
||||
*/
|
||||
fetch(callbackOrOptions) {
|
||||
this.doValidateParams();
|
||||
|
||||
if (!this.subscriptionHandle) {
|
||||
return this._fetchStatic(callbackOrOptions)
|
||||
} else {
|
||||
|
|
|
@ -17,7 +17,7 @@ Posts.addLinks({
|
|||
type: 'one',
|
||||
collection: Authors,
|
||||
field: 'authorId',
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'authorCache',
|
||||
body: {
|
||||
name: 1,
|
||||
|
@ -30,7 +30,7 @@ Posts.addLinks({
|
|||
metadata: true,
|
||||
collection: Categories,
|
||||
field: 'categoryIds',
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'categoriesCache',
|
||||
body: {
|
||||
name: 1,
|
||||
|
@ -43,7 +43,7 @@ Authors.addLinks({
|
|||
posts: {
|
||||
collection: Posts,
|
||||
inversedBy: 'author',
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'postCache',
|
||||
body: {
|
||||
title: 1,
|
||||
|
@ -54,7 +54,7 @@ Authors.addLinks({
|
|||
type: 'many',
|
||||
collection: Groups,
|
||||
field: 'groupIds',
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'groupsCache',
|
||||
body: {
|
||||
name: 1,
|
||||
|
@ -67,7 +67,7 @@ Authors.addLinks({
|
|||
collection: AuthorProfiles,
|
||||
field: 'profileId',
|
||||
unique: true,
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'profileCache',
|
||||
body: {
|
||||
name: 1,
|
||||
|
@ -81,7 +81,7 @@ AuthorProfiles.addLinks({
|
|||
collection: Authors,
|
||||
inversedBy: 'profile',
|
||||
unique: true,
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'authorCache',
|
||||
body: {
|
||||
name: 1,
|
||||
|
@ -94,7 +94,7 @@ Groups.addLinks({
|
|||
authors: {
|
||||
collection: Authors,
|
||||
inversedBy: 'groups',
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'authorsCache',
|
||||
body: {
|
||||
name: 1,
|
||||
|
@ -107,7 +107,7 @@ Categories.addLinks({
|
|||
posts: {
|
||||
collection: Posts,
|
||||
inversedBy: 'categories',
|
||||
cache: {
|
||||
denormalize: {
|
||||
field: 'postsCache',
|
||||
body: {
|
||||
title: 1,
|
||||
|
|
|
@ -3,6 +3,20 @@ import {createQuery} from 'meteor/cultofcoders:grapher';
|
|||
import {Authors, AuthorProfiles, Groups, Posts, Categories} from './collections';
|
||||
|
||||
describe('Query Link Cache', function () {
|
||||
it('Should work with nested filters', function () {
|
||||
let query = Posts.createQuery({
|
||||
$options: {limit: 5},
|
||||
author: {
|
||||
name: 1,
|
||||
}
|
||||
});
|
||||
|
||||
let insideFind = false;
|
||||
stubFind(Authors, function () {
|
||||
insideFind = true;
|
||||
});
|
||||
});
|
||||
|
||||
it('Should work properly - One Direct', function () {
|
||||
let query = Posts.createQuery({
|
||||
$options: {limit: 5},
|
||||
|
|
|
@ -473,9 +473,11 @@ describe('Hypernova', function () {
|
|||
authors: {}
|
||||
}
|
||||
}, {
|
||||
options: {limit: 1},
|
||||
filters: {
|
||||
name: 'JavaScript'
|
||||
params: {
|
||||
options: {limit: 1},
|
||||
filters: {
|
||||
name: 'JavaScript'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import './lib/extension.js';
|
||||
import './lib/links/extension.js';
|
||||
import './lib/query/extension.js';
|
||||
import './lib/query/reducers/extension.js';
|
||||
|
||||
export {
|
||||
default as createQuery
|
||||
} from './lib/query/createQuery.js';
|
||||
} from './lib/createQuery.js';
|
||||
|
||||
export {
|
||||
default as prepareForProcess
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
import './lib/extension.js';
|
||||
import './lib/aggregate';
|
||||
import './lib/exposure/extension.js';
|
||||
import './lib/links/extension.js';
|
||||
import './lib/query/extension.js';
|
||||
import './lib/query/reducers/extension.js';
|
||||
import './lib/namedQuery/expose/extension.js';
|
||||
import NamedQueryStore from './lib/namedQuery/store';
|
||||
import LinkConstants from './lib/links/constants';
|
||||
|
||||
export {
|
||||
NamedQueryStore,
|
||||
LinkConstants
|
||||
}
|
||||
|
||||
export {
|
||||
default as createQuery
|
||||
} from './lib/query/createQuery.js';
|
||||
} from './lib/createQuery.js';
|
||||
|
||||
export {
|
||||
default as Exposure
|
||||
} from './lib/exposure/exposure.js';
|
||||
|
||||
export {
|
||||
default as getDocumentationObject
|
||||
} from './lib/documentor/index.js';
|
||||
|
||||
export {
|
||||
default as MemoryResultCacher
|
||||
} from './lib/namedQuery/cache/MemoryResultCacher';
|
||||
|
|
|
@ -14,6 +14,7 @@ Npm.depends({
|
|||
'sift': '3.2.6',
|
||||
'dot-object': '1.5.4',
|
||||
'lodash.clonedeep': '4.5.0',
|
||||
'deep-extend': '0.5.0',
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
|
Loading…
Add table
Reference in a new issue