mirror of
https://github.com/vale981/grapher
synced 2025-03-04 17:11:38 -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",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
"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": {
|
"dot-object": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-1.5.4.tgz",
|
"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 = {
|
export const ExposureSchema = {
|
||||||
firewall: Match.Maybe(Function),
|
firewall: Match.Maybe(
|
||||||
|
Match.OneOf(Function, [Function])
|
||||||
|
),
|
||||||
maxLimit: Match.Maybe(Match.Integer),
|
maxLimit: Match.Maybe(Match.Integer),
|
||||||
maxDepth: Match.Maybe(Match.Integer),
|
maxDepth: Match.Maybe(Match.Integer),
|
||||||
publication: Match.Maybe(Boolean),
|
publication: Match.Maybe(Boolean),
|
||||||
|
|
|
@ -215,9 +215,7 @@ export default class Exposure {
|
||||||
|
|
||||||
collection.firewall = (filters, options, userId) => {
|
collection.firewall = (filters, options, userId) => {
|
||||||
if (userId !== undefined) {
|
if (userId !== undefined) {
|
||||||
if (firewall) {
|
this._callFirewall({collection: collection}, filters, options, userId);
|
||||||
firewall.call({collection: collection}, filters, options, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
enforceMaxLimit(options, maxLimit);
|
enforceMaxLimit(options, maxLimit);
|
||||||
|
|
||||||
|
@ -257,4 +255,22 @@ export default class Exposure {
|
||||||
return findOne(filters, options);
|
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 {Match} from 'meteor/check';
|
||||||
import {Mongo} from 'meteor/mongo';
|
import {Mongo} from 'meteor/mongo';
|
||||||
|
|
||||||
export const CacheSchema = {
|
export const DenormalizeSchema = {
|
||||||
field: String,
|
field: String,
|
||||||
body: Object,
|
body: Object,
|
||||||
bypassSchema: Match.Maybe(Boolean)
|
bypassSchema: Match.Maybe(Boolean)
|
||||||
|
@ -23,5 +23,5 @@ export const LinkConfigSchema = {
|
||||||
index: Match.Maybe(Boolean),
|
index: Match.Maybe(Boolean),
|
||||||
unique: Match.Maybe(Boolean),
|
unique: Match.Maybe(Boolean),
|
||||||
autoremove: 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 { Mongo } from 'meteor/mongo';
|
||||||
import { linkStorage } from './symbols.js';
|
import {LINK_STORAGE} from './constants.js';
|
||||||
import Linker from './linker.js';
|
import Linker from './linker.js';
|
||||||
|
|
||||||
_.extend(Mongo.Collection.prototype, {
|
_.extend(Mongo.Collection.prototype, {
|
||||||
|
@ -7,43 +7,43 @@ _.extend(Mongo.Collection.prototype, {
|
||||||
* The data we add should be valid for config.schema.js
|
* The data we add should be valid for config.schema.js
|
||||||
*/
|
*/
|
||||||
addLinks(data) {
|
addLinks(data) {
|
||||||
if (!this[linkStorage]) {
|
if (!this[LINK_STORAGE]) {
|
||||||
this[linkStorage] = {};
|
this[LINK_STORAGE] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
_.each(data, (linkConfig, linkName) => {
|
_.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`)
|
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);
|
const linker = new Linker(this, linkName, linkConfig);
|
||||||
|
|
||||||
_.extend(this[linkStorage], {
|
_.extend(this[LINK_STORAGE], {
|
||||||
[linkName]: linker
|
[linkName]: linker
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getLinks() {
|
getLinks() {
|
||||||
return this[linkStorage];
|
return this[LINK_STORAGE];
|
||||||
},
|
},
|
||||||
|
|
||||||
getLinker(name) {
|
getLinker(name) {
|
||||||
if (this[linkStorage]) {
|
if (this[LINK_STORAGE]) {
|
||||||
return this[linkStorage][name];
|
return this[LINK_STORAGE][name];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hasLink(name) {
|
hasLink(name) {
|
||||||
if (!this[linkStorage]) {
|
if (!this[LINK_STORAGE]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!this[linkStorage][name];
|
return !!this[LINK_STORAGE][name];
|
||||||
},
|
},
|
||||||
|
|
||||||
getLink(objectOrId, name) {
|
getLink(objectOrId, name) {
|
||||||
let linkData = this[linkStorage];
|
let linkData = this[LINK_STORAGE];
|
||||||
|
|
||||||
if (!linkData) {
|
if (!linkData) {
|
||||||
throw new Meteor.Error(`There are no links defined for collection: ${this._name}`);
|
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.
|
// initialize cascade removal hooks.
|
||||||
this._initAutoremove();
|
this._initAutoremove();
|
||||||
this._initCache();
|
this._initDenormalization();
|
||||||
|
|
||||||
if (this.isVirtual()) {
|
if (this.isVirtual()) {
|
||||||
// if it's a virtual field make sure that when this is deleted, it will be removed from the references
|
// 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;
|
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`)
|
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 cacheConfig;
|
||||||
|
|
||||||
let referenceFieldSuffix = '';
|
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}
|
* @returns {boolean}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
isCached() {
|
isDenormalized() {
|
||||||
return !!this.linkConfig.cache;
|
return !!this.linkConfig.denormalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -443,8 +447,8 @@ export default class Linker {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
isSubBodyCache(body) {
|
isSubBodyDenormalized(body) {
|
||||||
const cacheBody = this.linkConfig.cache.body;
|
const cacheBody = this.linkConfig.denormalize.body;
|
||||||
|
|
||||||
const cacheBodyFields = _.keys(dot.dot(cacheBody));
|
const cacheBodyFields = _.keys(dot.dot(cacheBody));
|
||||||
const bodyFields = _.keys(dot.dot(body));
|
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 genCountEndpoint from '../../query/counts/genEndpoint.server';
|
||||||
import {check} from 'meteor/check';
|
import {check} from 'meteor/check';
|
||||||
|
|
||||||
const specialParameters = ['$body'];
|
|
||||||
|
|
||||||
_.extend(NamedQuery.prototype, {
|
_.extend(NamedQuery.prototype, {
|
||||||
|
/**
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
expose(config = {}) {
|
expose(config = {}) {
|
||||||
if (!Meteor.isServer) {
|
if (!Meteor.isServer) {
|
||||||
throw new Meteor.Error('invalid-environment', `You must run this in server-side code`);
|
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);
|
this.exposeConfig = Object.assign({}, ExposeDefaults, config);
|
||||||
check(this.exposeConfig, ExposeSchema);
|
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();
|
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();
|
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')
|
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._initCountMethod();
|
||||||
this._initCountPublication();
|
this._initCountPublication();
|
||||||
|
|
||||||
if (this.exposeConfig.embody) {
|
if (config.embody) {
|
||||||
this.body = mergeDeep(
|
this.body = mergeDeep(
|
||||||
deepClone(this.body),
|
deepClone(this.body),
|
||||||
this.exposeConfig.embody
|
config.embody
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isExposed = true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_unblockIfNecessary(context) {
|
||||||
|
if (this.exposeConfig.unblock) {
|
||||||
|
context.unblock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
_initMethod() {
|
_initMethod() {
|
||||||
const self = this;
|
const self = this;
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
[this.name](newParams) {
|
[this.name](newParams) {
|
||||||
this.unblock();
|
self._unblockIfNecessary(this);
|
||||||
|
|
||||||
self._validateParams(newParams);
|
// security is done in the fetching because we provide a context
|
||||||
|
return self.clone(newParams).fetch(this);
|
||||||
if (self.exposeConfig.firewall) {
|
|
||||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.clone(newParams).fetch();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
_initCountMethod() {
|
_initCountMethod() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
[this.name + '.count'](newParams) {
|
[this.name + '.count'](newParams) {
|
||||||
this.unblock();
|
self._unblockIfNecessary(this);
|
||||||
self._validateParams(newParams);
|
|
||||||
|
|
||||||
if (self.exposeConfig.firewall) {
|
// security is done in the fetching because we provide a context
|
||||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
return self.clone(newParams).getCount(this);
|
||||||
}
|
|
||||||
|
|
||||||
return self.clone(newParams).getCount();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
_initCountPublication() {
|
_initCountPublication() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
@ -92,27 +122,24 @@ _.extend(NamedQuery.prototype, {
|
||||||
},
|
},
|
||||||
|
|
||||||
getSession(newParams) {
|
getSession(newParams) {
|
||||||
self._validateParams(newParams);
|
self.doValidateParams(newParams);
|
||||||
if (self.exposeConfig.firewall) {
|
self._callFirewall(this, this.userId, params);
|
||||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { params: newParams };
|
return { params: newParams };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
_initPublication() {
|
_initPublication() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
Meteor.publishComposite(this.name, function (newParams) {
|
Meteor.publishComposite(this.name, function (params) {
|
||||||
self._validateParams(newParams);
|
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 body = prepareForProcess(self.body, params);
|
||||||
|
|
||||||
const rootNode = createGraph(self.collection, body);
|
const rootNode = createGraph(self.collection, body);
|
||||||
|
@ -121,20 +148,24 @@ _.extend(NamedQuery.prototype, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_validateParams(params) {
|
/**
|
||||||
if (this.exposeConfig.schema) {
|
* @param context
|
||||||
const paramsToValidate = _.omit(params, ...specialParameters);
|
* @param userId
|
||||||
|
* @param params
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_callFirewall(context, userId, params) {
|
||||||
|
const {firewall} = this.exposeConfig;
|
||||||
|
if (!firewall) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (_.isArray(firewall)) {
|
||||||
try {
|
firewall.forEach(fire => {
|
||||||
check(paramsToValidate, this._paramSchema);
|
fire.call(context, userId, params);
|
||||||
} catch (validationError) {
|
})
|
||||||
console.error(`Invalid parameters supplied to query ${this.queryName}`, validationError);
|
} else {
|
||||||
throw validationError; // rethrow
|
firewall.call(context, userId, params);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
check(paramsToValidate, this._paramSchema);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -3,12 +3,18 @@ import {Match} from 'meteor/check';
|
||||||
export const ExposeDefaults = {
|
export const ExposeDefaults = {
|
||||||
publication: true,
|
publication: true,
|
||||||
method: true,
|
method: true,
|
||||||
|
unblock: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExposeSchema = {
|
export const ExposeSchema = {
|
||||||
firewall: Match.Maybe(Function),
|
firewall: Match.Maybe(
|
||||||
|
Match.OneOf(Function, [Function])
|
||||||
|
),
|
||||||
publication: Match.Maybe(Boolean),
|
publication: Match.Maybe(Boolean),
|
||||||
|
unblock: Match.Maybe(Boolean),
|
||||||
method: Match.Maybe(Boolean),
|
method: Match.Maybe(Boolean),
|
||||||
embody: Match.Maybe(Object),
|
embody: Match.Maybe(Object),
|
||||||
schema: Match.Maybe(Object),
|
validateParams: Match.Maybe(
|
||||||
|
Match.OneOf(Object, Function)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import deepClone from 'lodash.clonedeep';
|
import deepClone from 'lodash.clonedeep';
|
||||||
|
|
||||||
|
const specialParameters = ['$body'];
|
||||||
|
|
||||||
export default class NamedQueryBase {
|
export default class NamedQueryBase {
|
||||||
constructor(name, collection, body, params = {}) {
|
constructor(name, collection, body, options = {}) {
|
||||||
this.queryName = name;
|
this.queryName = name;
|
||||||
|
|
||||||
this.body = deepClone(body);
|
if (_.isFunction(body)) {
|
||||||
Object.freeze(this.body);
|
this.resolver = body;
|
||||||
|
} else {
|
||||||
|
this.body = deepClone(body);
|
||||||
|
}
|
||||||
|
|
||||||
this.subscriptionHandle = null;
|
this.subscriptionHandle = null;
|
||||||
this.params = params;
|
this.params = options.params || {};
|
||||||
|
this.options = options;
|
||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
this.isExposed = false;
|
this.isExposed = false;
|
||||||
}
|
}
|
||||||
|
@ -17,22 +23,65 @@ export default class NamedQueryBase {
|
||||||
return `named_query_${this.queryName}`;
|
return `named_query_${this.queryName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isResolver() {
|
||||||
|
return !!this.resolver;
|
||||||
|
}
|
||||||
|
|
||||||
setParams(params) {
|
setParams(params) {
|
||||||
this.params = _.extend({}, this.params, params);
|
this.params = _.extend({}, this.params, params);
|
||||||
|
|
||||||
return this;
|
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) {
|
clone(newParams) {
|
||||||
|
const params = _.extend({}, deepClone(this.params), newParams);
|
||||||
|
|
||||||
let clone = new this.constructor(
|
let clone = new this.constructor(
|
||||||
this.queryName,
|
this.queryName,
|
||||||
this.collection,
|
this.collection,
|
||||||
deepClone(this.body),
|
this.isResolver ? this.resolver : deepClone(this.body),
|
||||||
_.extend({}, deepClone(this.params), newParams)
|
{
|
||||||
|
...this.options,
|
||||||
|
params,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
clone.cacher = this.cacher;
|
clone.cacher = this.cacher;
|
||||||
|
if (this.exposeConfig) {
|
||||||
|
clone.exposeConfig = this.exposeConfig;
|
||||||
|
}
|
||||||
|
|
||||||
return clone;
|
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|*}
|
* @returns {null|any|*}
|
||||||
*/
|
*/
|
||||||
subscribe(callback) {
|
subscribe(callback) {
|
||||||
|
if (this.isResolver) {
|
||||||
|
throw new Meteor.Error('not-allowed', `You cannot subscribe to a resolver query`);
|
||||||
|
}
|
||||||
|
|
||||||
this.subscriptionHandle = Meteor.subscribe(
|
this.subscriptionHandle = Meteor.subscribe(
|
||||||
this.name,
|
this.name,
|
||||||
this.params,
|
this.params,
|
||||||
|
@ -30,6 +34,10 @@ export default class extends Base {
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
subscribeCount(callback) {
|
subscribeCount(callback) {
|
||||||
|
if (this.isResolver) {
|
||||||
|
throw new Meteor.Error('not-allowed', `You cannot subscribe to a resolver query`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this._counter) {
|
if (!this._counter) {
|
||||||
this._counter = new CountSubscription(this);
|
this._counter = new CountSubscription(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,18 +9,26 @@ export default class extends Base {
|
||||||
* Retrieves the data.
|
* Retrieves the data.
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
fetch() {
|
fetch(context) {
|
||||||
const query = this.collection.createQuery(
|
this._performSecurityChecks(context, this.params);
|
||||||
deepClone(this.body),
|
|
||||||
deepClone(this.params)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.cacher) {
|
if (this.isResolver) {
|
||||||
const cacheId = generateQueryId(this.queryName, this.params);
|
return this._fetchResolverData(context);
|
||||||
return this.cacher.get(cacheId, {query});
|
} 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}
|
* @returns {any}
|
||||||
*/
|
*/
|
||||||
getCount() {
|
getCount(context) {
|
||||||
|
this._performSecurityChecks(context, this.params);
|
||||||
|
|
||||||
const countCursor = this.getCursorForCounting();
|
const countCursor = this.getCursorForCounting();
|
||||||
|
|
||||||
if (this.cacher) {
|
if (this.cacher) {
|
||||||
|
@ -68,4 +78,51 @@ export default class extends Base {
|
||||||
|
|
||||||
this.cacher = cacher;
|
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';
|
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
export default createQuery('postListExposure', {
|
const postListExposure = createQuery('postListExposure', {
|
||||||
posts: {
|
posts: {
|
||||||
title: 1,
|
title: 1,
|
||||||
author: {
|
author: {
|
||||||
|
@ -10,4 +10,18 @@ export default createQuery('postListExposure', {
|
||||||
name: 1
|
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 './queries';
|
||||||
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,
|
|
||||||
}));
|
|
|
@ -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';
|
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
|
||||||
describe('Named Query', function () {
|
describe('Named Query', function () {
|
||||||
it('Should return the proper values', function () {
|
it('Should return the proper values', function () {
|
||||||
const createdQuery = createQuery({
|
const createdQuery = createQuery({
|
||||||
|
@ -96,5 +102,58 @@ describe('Named Query', function () {
|
||||||
assert.isObject(post.group);
|
assert.isObject(post.group);
|
||||||
assert.isUndefined(post.group.createdAt);
|
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) {
|
if (!this.isVirtual) {
|
||||||
return {
|
return {
|
||||||
_id: {
|
_id: {
|
||||||
$in: _.pluck(this.parentObjects, this.linkStorageField)
|
$in: _.uniq(
|
||||||
|
_.pluck(this.parentObjects, this.linkStorageField)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
[this.linkStorageField]: {
|
[this.linkStorageField]: {
|
||||||
$in: _.pluck(this.parentObjects, '_id')
|
$in: _.uniq(
|
||||||
|
_.pluck(this.parentObjects, '_id')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -67,7 +71,7 @@ export default class AggregateFilters {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: {$in: ids}
|
_id: {$in: _.uniq(ids)}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let filters = {};
|
let filters = {};
|
||||||
|
@ -78,7 +82,9 @@ export default class AggregateFilters {
|
||||||
}
|
}
|
||||||
|
|
||||||
filters[this.linkStorageField + '._id'] = {
|
filters[this.linkStorageField + '._id'] = {
|
||||||
$in: _.pluck(this.parentObjects, '_id')
|
$in: _.uniq(
|
||||||
|
_.pluck(this.parentObjects, '_id')
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
|
@ -90,14 +96,18 @@ export default class AggregateFilters {
|
||||||
const arrayOfIds = _.pluck(this.parentObjects, this.linkStorageField);
|
const arrayOfIds = _.pluck(this.parentObjects, this.linkStorageField);
|
||||||
return {
|
return {
|
||||||
_id: {
|
_id: {
|
||||||
$in: _.union(...arrayOfIds)
|
$in: _.uniq(
|
||||||
|
_.union(...arrayOfIds)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const arrayOfIds = _.pluck(this.parentObjects, '_id');
|
const arrayOfIds = _.pluck(this.parentObjects, '_id');
|
||||||
return {
|
return {
|
||||||
[this.linkStorageField]: {
|
[this.linkStorageField]: {
|
||||||
$in: _.union(...arrayOfIds)
|
$in: _.uniq(
|
||||||
|
_.union(...arrayOfIds)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -125,7 +135,7 @@ export default class AggregateFilters {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: {$in: ids}
|
_id: {$in: _.uniq(ids)}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let filters = {};
|
let filters = {};
|
||||||
|
@ -136,7 +146,9 @@ export default class AggregateFilters {
|
||||||
}
|
}
|
||||||
|
|
||||||
filters._id = {
|
filters._id = {
|
||||||
$in: _.pluck(this.parentObjects, '_id')
|
$in: _.uniq(
|
||||||
|
_.pluck(this.parentObjects, '_id')
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -49,9 +49,9 @@ export function createNodes(root) {
|
||||||
// check if it is a cached link
|
// check if it is a cached link
|
||||||
// if yes, then we need to explicitly define this at collection level
|
// 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
|
// so when we transform the data for delivery, we move it to the link name
|
||||||
if (linker.isCached()) {
|
if (linker.isDenormalized()) {
|
||||||
if (linker.isSubBodyCache(body)) {
|
if (linker.isSubBodyDenormalized(body)) {
|
||||||
const cacheField = linker.linkConfig.cache.field;
|
const cacheField = linker.linkConfig.denormalize.field;
|
||||||
|
|
||||||
root.snapCache(cacheField, fieldName);
|
root.snapCache(cacheField, fieldName);
|
||||||
addFieldNode(body, cacheField, root);
|
addFieldNode(body, cacheField, root);
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
import deepClone from 'lodash.clonedeep';
|
import deepClone from 'lodash.clonedeep';
|
||||||
|
import {check} from 'meteor/check';
|
||||||
|
|
||||||
export default class QueryBase {
|
export default class QueryBase {
|
||||||
constructor(collection, body, params = {}) {
|
constructor(collection, body, options = {}) {
|
||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
|
|
||||||
this.body = deepClone(body);
|
this.body = deepClone(body);
|
||||||
Object.freeze(this.body);
|
|
||||||
|
|
||||||
this._params = params;
|
this.params = options.params || {};
|
||||||
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(newParams) {
|
clone(newParams) {
|
||||||
|
const params = _.extend({}, deepClone(this.params), newParams);
|
||||||
|
|
||||||
return new this.constructor(
|
return new this.constructor(
|
||||||
this.collection,
|
this.collection,
|
||||||
deepClone(this.body),
|
deepClone(this.body),
|
||||||
_.extend({}, deepClone(this.params), newParams)
|
{
|
||||||
|
params,
|
||||||
|
...this.options
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,18 +28,28 @@ export default class QueryBase {
|
||||||
return `exposure_${this.collection._name}`;
|
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.
|
* Merges the params with previous params.
|
||||||
*
|
*
|
||||||
* @param data
|
* @param params
|
||||||
* @returns {Query}
|
* @returns {Query}
|
||||||
*/
|
*/
|
||||||
setParams(data) {
|
setParams(params) {
|
||||||
_.extend(this._params, data);
|
this.params = _.extend({}, this.params, params);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ export default class Query extends Base {
|
||||||
* @returns {null|any|*}
|
* @returns {null|any|*}
|
||||||
*/
|
*/
|
||||||
subscribe(callback) {
|
subscribe(callback) {
|
||||||
|
this.doValidateParams();
|
||||||
|
|
||||||
this.subscriptionHandle = Meteor.subscribe(
|
this.subscriptionHandle = Meteor.subscribe(
|
||||||
this.name,
|
this.name,
|
||||||
prepareForProcess(this.body, this.params),
|
prepareForProcess(this.body, this.params),
|
||||||
|
@ -30,6 +32,8 @@ export default class Query extends Base {
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
subscribeCount(callback) {
|
subscribeCount(callback) {
|
||||||
|
this.doValidateParams();
|
||||||
|
|
||||||
if (!this._counter) {
|
if (!this._counter) {
|
||||||
this._counter = new CountSubscription(this);
|
this._counter = new CountSubscription(this);
|
||||||
}
|
}
|
||||||
|
@ -66,6 +70,8 @@ export default class Query extends Base {
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
async fetchSync() {
|
async fetchSync() {
|
||||||
|
this.doValidateParams();
|
||||||
|
|
||||||
if (this.subscriptionHandle) {
|
if (this.subscriptionHandle) {
|
||||||
throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.');
|
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 {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
fetch(callbackOrOptions) {
|
fetch(callbackOrOptions) {
|
||||||
|
this.doValidateParams();
|
||||||
|
|
||||||
if (!this.subscriptionHandle) {
|
if (!this.subscriptionHandle) {
|
||||||
return this._fetchStatic(callbackOrOptions)
|
return this._fetchStatic(callbackOrOptions)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,7 +17,7 @@ Posts.addLinks({
|
||||||
type: 'one',
|
type: 'one',
|
||||||
collection: Authors,
|
collection: Authors,
|
||||||
field: 'authorId',
|
field: 'authorId',
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'authorCache',
|
field: 'authorCache',
|
||||||
body: {
|
body: {
|
||||||
name: 1,
|
name: 1,
|
||||||
|
@ -30,7 +30,7 @@ Posts.addLinks({
|
||||||
metadata: true,
|
metadata: true,
|
||||||
collection: Categories,
|
collection: Categories,
|
||||||
field: 'categoryIds',
|
field: 'categoryIds',
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'categoriesCache',
|
field: 'categoriesCache',
|
||||||
body: {
|
body: {
|
||||||
name: 1,
|
name: 1,
|
||||||
|
@ -43,7 +43,7 @@ Authors.addLinks({
|
||||||
posts: {
|
posts: {
|
||||||
collection: Posts,
|
collection: Posts,
|
||||||
inversedBy: 'author',
|
inversedBy: 'author',
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'postCache',
|
field: 'postCache',
|
||||||
body: {
|
body: {
|
||||||
title: 1,
|
title: 1,
|
||||||
|
@ -54,7 +54,7 @@ Authors.addLinks({
|
||||||
type: 'many',
|
type: 'many',
|
||||||
collection: Groups,
|
collection: Groups,
|
||||||
field: 'groupIds',
|
field: 'groupIds',
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'groupsCache',
|
field: 'groupsCache',
|
||||||
body: {
|
body: {
|
||||||
name: 1,
|
name: 1,
|
||||||
|
@ -67,7 +67,7 @@ Authors.addLinks({
|
||||||
collection: AuthorProfiles,
|
collection: AuthorProfiles,
|
||||||
field: 'profileId',
|
field: 'profileId',
|
||||||
unique: true,
|
unique: true,
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'profileCache',
|
field: 'profileCache',
|
||||||
body: {
|
body: {
|
||||||
name: 1,
|
name: 1,
|
||||||
|
@ -81,7 +81,7 @@ AuthorProfiles.addLinks({
|
||||||
collection: Authors,
|
collection: Authors,
|
||||||
inversedBy: 'profile',
|
inversedBy: 'profile',
|
||||||
unique: true,
|
unique: true,
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'authorCache',
|
field: 'authorCache',
|
||||||
body: {
|
body: {
|
||||||
name: 1,
|
name: 1,
|
||||||
|
@ -94,7 +94,7 @@ Groups.addLinks({
|
||||||
authors: {
|
authors: {
|
||||||
collection: Authors,
|
collection: Authors,
|
||||||
inversedBy: 'groups',
|
inversedBy: 'groups',
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'authorsCache',
|
field: 'authorsCache',
|
||||||
body: {
|
body: {
|
||||||
name: 1,
|
name: 1,
|
||||||
|
@ -107,7 +107,7 @@ Categories.addLinks({
|
||||||
posts: {
|
posts: {
|
||||||
collection: Posts,
|
collection: Posts,
|
||||||
inversedBy: 'categories',
|
inversedBy: 'categories',
|
||||||
cache: {
|
denormalize: {
|
||||||
field: 'postsCache',
|
field: 'postsCache',
|
||||||
body: {
|
body: {
|
||||||
title: 1,
|
title: 1,
|
||||||
|
|
|
@ -3,6 +3,20 @@ import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||||
import {Authors, AuthorProfiles, Groups, Posts, Categories} from './collections';
|
import {Authors, AuthorProfiles, Groups, Posts, Categories} from './collections';
|
||||||
|
|
||||||
describe('Query Link Cache', function () {
|
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 () {
|
it('Should work properly - One Direct', function () {
|
||||||
let query = Posts.createQuery({
|
let query = Posts.createQuery({
|
||||||
$options: {limit: 5},
|
$options: {limit: 5},
|
||||||
|
|
|
@ -473,9 +473,11 @@ describe('Hypernova', function () {
|
||||||
authors: {}
|
authors: {}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
options: {limit: 1},
|
params: {
|
||||||
filters: {
|
options: {limit: 1},
|
||||||
name: 'JavaScript'
|
filters: {
|
||||||
|
name: 'JavaScript'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import './lib/extension.js';
|
||||||
import './lib/links/extension.js';
|
import './lib/links/extension.js';
|
||||||
import './lib/query/extension.js';
|
|
||||||
import './lib/query/reducers/extension.js';
|
import './lib/query/reducers/extension.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as createQuery
|
default as createQuery
|
||||||
} from './lib/query/createQuery.js';
|
} from './lib/createQuery.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as prepareForProcess
|
default as prepareForProcess
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
|
import './lib/extension.js';
|
||||||
import './lib/aggregate';
|
import './lib/aggregate';
|
||||||
import './lib/exposure/extension.js';
|
import './lib/exposure/extension.js';
|
||||||
import './lib/links/extension.js';
|
import './lib/links/extension.js';
|
||||||
import './lib/query/extension.js';
|
|
||||||
import './lib/query/reducers/extension.js';
|
import './lib/query/reducers/extension.js';
|
||||||
import './lib/namedQuery/expose/extension.js';
|
import './lib/namedQuery/expose/extension.js';
|
||||||
|
import NamedQueryStore from './lib/namedQuery/store';
|
||||||
|
import LinkConstants from './lib/links/constants';
|
||||||
|
|
||||||
|
export {
|
||||||
|
NamedQueryStore,
|
||||||
|
LinkConstants
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as createQuery
|
default as createQuery
|
||||||
} from './lib/query/createQuery.js';
|
} from './lib/createQuery.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as Exposure
|
default as Exposure
|
||||||
} from './lib/exposure/exposure.js';
|
} from './lib/exposure/exposure.js';
|
||||||
|
|
||||||
export {
|
|
||||||
default as getDocumentationObject
|
|
||||||
} from './lib/documentor/index.js';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as MemoryResultCacher
|
default as MemoryResultCacher
|
||||||
} from './lib/namedQuery/cache/MemoryResultCacher';
|
} from './lib/namedQuery/cache/MemoryResultCacher';
|
||||||
|
|
|
@ -14,6 +14,7 @@ Npm.depends({
|
||||||
'sift': '3.2.6',
|
'sift': '3.2.6',
|
||||||
'dot-object': '1.5.4',
|
'dot-object': '1.5.4',
|
||||||
'lodash.clonedeep': '4.5.0',
|
'lodash.clonedeep': '4.5.0',
|
||||||
|
'deep-extend': '0.5.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function (api) {
|
Package.onUse(function (api) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue