Merge branch 'master' into master

This commit is contained in:
Jim LAURIE 2018-11-22 12:57:16 +01:00 committed by GitHub
commit 7e00be7057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 827 additions and 243 deletions

View File

@ -3,15 +3,17 @@
<!-- Uncomment the correct contribution type. !-->
My PR is a:
<!-- 💥 Breaking change -->
<!-- 🐛 Bug fix -->
<!-- 💅 Enhancement -->
<!-- 🚀 New feature -->
- [ ] 💥 Breaking change
- [ ] 🐛 Bug fix #issueNumber
- [ ] 💅 Enhancement
- [ ] 🚀 New feature
Main update on the:
<!-- Admin -->
<!-- Documentation -->
<!-- Framework -->
<!-- Plugin -->
- [ ] Admin
- [ ] Documentation
- [ ] Framework
- [ ] Plugin
<!-- Write a short description of what your PR does and link the concerned issues of your update. -->
<!-- ⚠️ Please link issue(s) you close / fix by using GitHub keywords https://help.github.com/articles/closing-issues-using-keywords/ !-->

View File

@ -37,6 +37,16 @@ Find products having a price equal or greater than `3`.
`GET /products?price_gte=3`
#### Relations
You can also use filters into a relation attribute which will be applied to the first level of the request.
Find users having written a post named `Title`.
`GET /users?posts.name=Title`
Find posts written by a user having more than 12 years old.
`GET /posts?author.age_gt=12`
> Note: You can't use filter to have specific results inside relation, like "Find users and only their posts older than yesterday" as example. If you need it, you can modify or create your own service ou use [GraphQL](./graphql.md#query-api).
> Warning: this filter isn't available for `upload` plugin
### Sort
Sort according to a specific field.

View File

@ -0,0 +1,95 @@
/*
* Copyright@React-FullStory (https://github.com/cereallarceny/react-fullstory)
*/
import React from 'react';
import PropTypes from 'prop-types';
const canUseDOM = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
export const getWindowFullStory = () => window[window['_fs_namespace']];
class FullStory extends React.Component {
constructor(props) {
super(props);
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';
window['_fs_org'] = props.org;
window['_fs_namespace'] = 'FS';
(function(m,n,e,t,l,o,g,y) {
if (e in m) {
if(m.console && m.console.log) {
m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');
}
return;
}
g = m[e]= function(a,b,s) {
g.q ? g.q.push([a,b,s]) : g._api(a,b,s);
};
g.q=[];
o = n.createElement(t);
o.async = 1;
o.src = `https://${window._fs_host}/s/fs.js`;
y = n.getElementsByTagName(t)[0];
y.parentNode.insertBefore(o,y);
g.identify = function(i,v,s) {
g(l,{ uid:i },s);
if (v) {
g(l,v,s);
}
};
g.setUserVars = function(v,s) {
g(l,v,s);
};
g.event = function(i,v,s) {
g('event',{ n:i,p:v },s);
};
g.shutdown = function() {
g("rec",!1);
};
g.restart = function() {
g("rec",!0);
};
g.consent = function(a) {
g("consent",!arguments.length||a);
};
g.identifyAccount = function(i,v) {
o = 'account';
v = v||{};
v.acctId = i;
g(o,v);
};
g.clearUserCookie = function() {};
})(window, document, window['_fs_namespace'], 'script', 'user');
}
shouldComponentUpdate() {
return false;
}
componentWillUnmount() {
if (!canUseDOM || !getWindowFullStory()) return false;
getWindowFullStory().shutdown();
delete getWindowFullStory();
}
render() {
return false;
}
}
FullStory.propTypes = {
org: PropTypes.string.isRequired,
};
export default FullStory;

View File

@ -43,6 +43,7 @@ import Logout from 'components/Logout';
import NotFoundPage from 'containers/NotFoundPage/Loadable';
import OverlayBlocker from 'components/OverlayBlocker';
import PluginPage from 'containers/PluginPage';
import FullStory from 'components/FullStory';
// Utils
import auth from 'utils/auth';
import injectReducer from 'utils/injectReducer';
@ -73,12 +74,12 @@ export class AdminPage extends React.Component {
}
componentDidUpdate(prevProps) {
const { adminPage: { allowGa }, location: { pathname }, plugins } = this.props;
const { adminPage: { uuid }, location: { pathname }, plugins } = this.props;
if (prevProps.location.pathname !== pathname) {
this.checkLogin(this.props);
if (allowGa) {
if (uuid) {
ReactGA.pageview(pathname);
}
}
@ -198,6 +199,7 @@ export class AdminPage extends React.Component {
return (
<div className={styles.adminPage}>
{this.props.adminPage.uuid ? <FullStory org="GK708" /> : ''}
{this.showLeftMenu() && (
<LeftMenu
plugins={this.retrievePlugins()}

View File

@ -11,7 +11,7 @@ import {
} from './constants';
const initialState = fromJS({
allowGa: true,
uuid: false,
currentEnvironment: 'development',
isLoading: true,
layout: Map({}),
@ -22,7 +22,7 @@ function adminPageReducer(state = initialState, action) {
switch (action.type) {
case GET_ADMIN_DATA_SUCCEEDED:
return state
.update('allowGa', () => action.data.allowGa)
.update('uuid', () => action.data.uuid)
.update('currentEnvironment', () => action.data.currentEnvironment)
.update('layout', () => Map(action.data.layout))
.update('strapiVersion', () => action.data.strapiVersion)

View File

@ -16,13 +16,13 @@ function* getData() {
yield call(request, `${strapi.backendURL}/users/me`, { method: 'GET' });
}
const [{ allowGa }, { strapiVersion }, { currentEnvironment }, { layout }] = yield all([
const [{ uuid }, { strapiVersion }, { currentEnvironment }, { layout }] = yield all([
call(request, '/admin/gaConfig', { method: 'GET' }),
call(request, '/admin/strapiVersion', { method: 'GET' }),
call(request, '/admin/currentEnvironment', { method: 'GET' }),
call(request, '/admin/layout', { method: 'GET' }),
]);
yield put(getAdminDataSucceeded({ allowGa, strapiVersion, currentEnvironment, layout }));
yield put(getAdminDataSucceeded({ uuid, strapiVersion, currentEnvironment, layout }));
} catch(err) {
console.log(err); // eslint-disable-line no-console

View File

@ -1,8 +1,7 @@
.containerFluid {
padding: 18px 30px !important;
> div:first-child {
max-height: 33px;
margin-bottom: 48px;
margin-bottom: 11px;
}
}

View File

@ -28,8 +28,7 @@ module.exports = {
getGaConfig: async ctx => {
try {
const allowGa = _.get(strapi.config, 'info.customs.allowGa', true);
ctx.send({ allowGa });
ctx.send({ uuid: _.get(strapi.config, 'uuid', false) });
} catch(err) {
ctx.badRequest(null, [{ messages: [{ id: 'An error occurred' }] }]);
}

View File

@ -10,7 +10,7 @@ If you don't want to share your data with us, you can simply modify the `strapi`
```json
{
"strapi": {
"allowGa": false
"uuid": false
}
}
```

View File

@ -1,4 +1,5 @@
'use strict';
/* global <%= globalID %> */
/**
* <%= filename %> service
@ -9,9 +10,6 @@
// Public dependencies.
const _ = require('lodash');
// Strapi utilities.
const utils = require('strapi-hook-bookshelf/lib/utils/');
module.exports = {
/**
@ -21,6 +19,8 @@ module.exports = {
*/
fetchAll: (params) => {
// Get model hook
const hook = strapi.hook[<%= globalID %>.orm];
// Convert `params` object to filters compatible with Bookshelf.
const filters = strapi.utils.models.convertParams('<%= globalID.toLowerCase() %>', params);
// Select field to populate.
@ -29,22 +29,18 @@ module.exports = {
.map(ast => ast.alias);
return <%= globalID %>.query(function(qb) {
_.forEach(filters.where, (where, key) => {
if (_.isArray(where.value) && where.symbol !== 'IN') {
for (const value in where.value) {
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value])
}
// Generate match stage.
hook.load().generateMatchStage(qb)(<%= globalID %>, filters);
if (_.has(filters, 'start')) qb.offset(filters.start);
if (_.has(filters, 'limit')) qb.limit(filters.limit);
if (!_.isEmpty(filters.sort)) {
if (filters.sort.key) {
qb.orderBy(filters.sort.key, filters.sort.order);
} else {
qb.where(key, where.symbol, where.value);
qb.orderBy(filters.sort);
}
});
if (filters.sort) {
qb.orderBy(filters.sort.key, filters.sort.order);
}
qb.offset(filters.start);
qb.limit(filters.limit);
}).fetchAll({
withRelated: populate
});
@ -81,7 +77,7 @@ module.exports = {
_.forEach(filters.where, (where, key) => {
if (_.isArray(where.value)) {
for (const value in where.value) {
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value])
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]);
}
} else {
qb.where(key, where.symbol, where.value);

View File

@ -1,4 +1,5 @@
'use strict';
/* global <%= globalID %> */
/**
* <%= filename %> service
@ -9,6 +10,8 @@
// Public dependencies.
const _ = require('lodash');
const { models: { mergeStages } } = require('strapi-utils');
module.exports = {
/**
@ -17,22 +20,24 @@ module.exports = {
* @return {Promise}
*/
fetchAll: (params) => {
fetchAll: (params, next, { populate } = {}) => {
// Convert `params` object to filters compatible with Mongo.
const filters = strapi.utils.models.convertParams('<%= globalID.toLowerCase() %>', params);
// Select field to populate.
const populate = <%= globalID %>.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias)
.join(' ');
const hook = strapi.hook[<%= globalID %>.orm];
// Generate stages.
const populateStage = hook.load().generateLookupStage(<%= globalID %>, { whitelistedPopulate: populate }); // Nested-Population
const matchStage = hook.load().generateMatchStage(<%= globalID %>, filters); // Nested relation filter
const aggregateStages = mergeStages(populateStage, matchStage);
return <%= globalID %>
.find()
.where(filters.where)
.sort(filters.sort)
const result = <%= globalID %>.aggregate(aggregateStages)
.skip(filters.start)
.limit(filters.limit)
.populate(populate);
.limit(filters.limit);
if (_.has(filters, 'start')) result.skip(filters.start);
if (_.has(filters, 'limit')) result.limit(filters.limit);
if (!_.isEmpty(filters.sort)) result.sort(filters.sort);
return result;
},
/**

View File

@ -56,6 +56,7 @@ module.exports = scope => {
'dependencies': Object.assign({}, {
'lodash': '^4.17.5',
'strapi': getDependencyVersion(cliPkg, 'strapi'),
'strapi-utils': getDependencyVersion(cliPkg, 'strapi'),
[scope.client.connector]: getDependencyVersion(cliPkg, 'strapi'),
}, additionalsDependencies, {
[scope.client.module]: scope.client.version

View File

@ -502,7 +502,7 @@ module.exports = function(strapi) {
console.log(e);
}
strapi.log.warn(`The SQL database indexes haven't been generated successfully. Please enable the debug mode for more details.`);
strapi.log.warn('The SQL database indexes haven\'t been generated successfully. Please enable the debug mode for more details.');
}
}
};
@ -677,24 +677,11 @@ module.exports = function(strapi) {
}
};
const table = _.get(manyRelations, 'collectionName') ||
_.map(
_.sortBy(
[
collection.attributes[
manyRelations.via
],
manyRelations
],
'collection'
),
table => {
return _.snakeCase(
// eslint-disable-next-line prefer-template
pluralize.plural(table.collection) + ' ' + pluralize.plural(table.via)
);
}
).join('__');
const table = _.get(manyRelations, 'collectionName')
|| utilsModels.getCollectionName(
collection.attributes[manyRelations.via],
manyRelations
);
await handler(table, attributes);
}
@ -813,24 +800,11 @@ module.exports = function(strapi) {
strapi.plugins[details.plugin].models[details.collection]:
strapi.models[details.collection];
const collectionName = _.get(details, 'collectionName') ||
_.map(
_.sortBy(
[
collection.attributes[
details.via
],
details
],
'collection'
),
table => {
return _.snakeCase(
// eslint-disable-next-line prefer-template
pluralize.plural(table.collection) + ' ' + pluralize.plural(table.via)
);
}
).join('__');
const collectionName = _.get(details, 'collectionName')
|| utilsModels.getCollectionName(
collection.attributes[details.via],
details,
);
const relationship = _.clone(
collection.attributes[details.via]

View File

@ -35,6 +35,77 @@ module.exports = {
return _.get(strapi.plugins, [plugin, 'models', model]) || _.get(strapi, ['models', model]) || undefined;
},
generateMatchStage: function (qb) {
return (strapiModel, filters) => {
if (!filters) {
return undefined;
}
// 1st level deep filter
if (filters.where) {
this.generateMatchStage(qb)(strapiModel, { relations: filters.where });
}
// 2nd+ level deep filter
_.forEach(filters.relations, (value, key) => {
if (key !== 'relations') {
const association = strapiModel.associations.find(a => a.alias === key);
if (!association) {
const fieldKey = `${strapiModel.collectionName}.${key}`;
if (_.isArray(value.value) && value.symbol !== 'IN') {
for (const value in value.value) {
qb[value ? 'where' : 'orWhere'](fieldKey, value.symbol, value.value[value]);
}
} else {
qb.where(fieldKey, value.symbol, value.value);
}
} else {
const model = association.plugin ?
strapi.plugins[association.plugin].models[association.model || association.collection] :
strapi.models[association.model || association.collection];
const relationTable = model.collectionName;
qb.distinct();
if (association.nature === 'manyToMany') {
// Join on both ends
qb.innerJoin(
association.tableCollectionName,
`${association.tableCollectionName}.${strapiModel.info.name}_${strapiModel.primaryKey}`,
`${strapiModel.collectionName}.${strapiModel.primaryKey}`,
);
qb.innerJoin(
relationTable,
`${association.tableCollectionName}.${strapiModel.attributes[key].attribute}_${strapiModel.attributes[key].column}`,
`${relationTable}.${model.primaryKey}`,
);
} else {
const externalKey = association.type === 'collection'
? `${relationTable}.${association.via}`
: `${relationTable}.${model.primaryKey}`;
const internalKey = association.type === 'collection'
? `${strapiModel.collectionName}.${strapiModel.primaryKey}`
: `${strapiModel.collectionName}.${association.alias}`;
qb.innerJoin(relationTable, externalKey, internalKey);
}
if (_.isPlainObject(value)) {
this.generateMatchStage(qb)(
model,
{ relations: value.value }
);
}
}
} else {
this.generateMatchStage(qb)(strapiModel, { relations: value });
}
});
};
},
findOne: async function (params, populate) {
const record = await this
.forge({

View File

@ -17,6 +17,8 @@ const { models: utilsModels } = require('strapi-utils');
// Local helpers.
const utils = require('./utils/');
const _utils = utils();
const relations = require('./relations');
/**
@ -488,16 +490,16 @@ module.exports = function (strapi) {
result.value = value;
break;
case '_sort':
result.key = `sort`;
result.key = 'sort';
result.value = (_.toLower(value) === 'desc') ? '-' : '';
result.value += key;
break;
case '_start':
result.key = `start`;
result.key = 'start';
result.value = parseFloat(value);
break;
case '_limit':
result.key = `limit`;
result.key = 'limit';
result.value = parseFloat(value);
break;
case '_contains':
@ -520,6 +522,13 @@ module.exports = function (strapi) {
}
return result;
},
postProcessValue: (value) => {
if (_.isArray(value)) {
return value.map(_utils.valueToId);
}
return _utils.valueToId(value);
}
}, relations);

View File

@ -10,11 +10,156 @@ const _ = require('lodash');
// Utils
const { models: { getValuePrimaryKey } } = require('strapi-utils');
const buildTempFieldPath = field => {
return `__${field}`;
};
const restoreRealFieldPath = (field, prefix) => {
return `${prefix}${field}`;
};
module.exports = {
getModel: function (model, plugin) {
return _.get(strapi.plugins, [plugin, 'models', model]) || _.get(strapi, ['models', model]) || undefined;
},
generateLookupStage: function (strapiModel, { whitelistedPopulate = null, prefixPath = '' } = {}) {
return strapiModel.associations
.filter(ast => {
if (whitelistedPopulate) {
return _.includes(whitelistedPopulate, ast.alias);
}
return ast.autoPopulate;
})
.reduce((acc, ast) => {
const model = ast.plugin
? strapi.plugins[ast.plugin].models[ast.collection || ast.model]
: strapi.models[ast.collection || ast.model];
const from = model.collectionName;
const isDominantAssociation =
(ast.dominant && ast.nature === 'manyToMany') || !!ast.model;
const _localField =
!isDominantAssociation || ast.via === 'related' ? '_id' : ast.alias;
const localField = `${prefixPath}${_localField}`;
const foreignField = ast.filter
? `${ast.via}.ref`
: isDominantAssociation
? '_id'
: ast.via;
// Add the juncture like the `.populate()` function
const asTempPath = buildTempFieldPath(ast.alias, prefixPath);
const asRealPath = restoreRealFieldPath(ast.alias, prefixPath);
acc.push({
$lookup: {
from,
localField,
foreignField,
as: asTempPath,
},
});
// Unwind the relation's result if only one is expected
if (ast.type === 'model') {
acc.push({
$unwind: {
path: `$${asTempPath}`,
preserveNullAndEmptyArrays: true,
},
});
}
// Preserve relation field if it is empty
acc.push({
$addFields: {
[asRealPath]: {
$ifNull: [`$${asTempPath}`, null],
},
},
});
// Remove temp field
acc.push({
$project: {
[asTempPath]: 0,
},
});
return acc;
}, []);
},
generateMatchStage: function (strapiModel, filters, { prefixPath = '' } = {}) {
if (!filters) {
return undefined;
}
let acc = [];
// 1st level deep filter
if (filters.where) {
acc.push(
...this.generateMatchStage(
strapiModel,
{ relations: filters.where },
{ prefixPath }
)
);
}
// 2nd+ level deep filter
_.forEach(filters.relations, (value, key) => {
if (key !== 'relations') {
const nextPrefixedPath = `${prefixPath}${key}.`;
const association = strapiModel.associations.find(a => a.alias === key);
if (!association) {
acc.push({
$match: { [`${prefixPath}${key}`]: value },
});
} else {
const model = association.plugin
? strapi.plugins[association.plugin].models[
association.collection || association.model
]
: strapi.models[association.collection || association.model];
// Generate lookup for this relation
acc.push(
...this.generateLookupStage(strapiModel, {
whitelistedPopulate: [key],
prefixPath,
})
);
// If it's an object re-run the same function with this new value until having either a primitive value or an array.
if (_.isPlainObject(value)) {
acc.push(
...this.generateMatchStage(
model,
{ relations: value },
{
prefixPath: nextPrefixedPath,
}
)
);
}
}
} else {
acc.push(
...this.generateMatchStage(strapiModel, { relations: value }, { prefixPath })
);
}
});
return acc;
},
update: async function (params) {
const virtualFields = [];
const response = await this

View File

@ -4,9 +4,23 @@
* Module dependencies
*/
module.exports = mongoose => {
var Decimal = require('mongoose-float').loadType(mongoose, 2);
var Float = require('mongoose-float').loadType(mongoose, 20);
// Public node modules.
const mongoose = require('mongoose');
const Mongoose = mongoose.Mongoose;
/**
* Convert MongoDB ID to the stringify version as GraphQL throws an error if not.
*
* Refer to: https://github.com/graphql/graphql-js/commit/3521e1429eec7eabeee4da65c93306b51308727b#diff-87c5e74dd1f7d923143e0eee611f598eR183
*/
mongoose.Types.ObjectId.prototype.valueOf = function () {
return this.toString();
};
module.exports = (mongoose = new Mongoose()) => {
const Decimal = require('mongoose-float').loadType(mongoose, 2);
const Float = require('mongoose-float').loadType(mongoose, 20);
return {
convertType: mongooseType => {
@ -42,5 +56,16 @@ module.exports = mongoose => {
default:
}
},
valueToId: function (value) {
return this.isMongoId(value)
? mongoose.Types.ObjectId(value)
: value;
},
isMongoId: function (value) {
// Here we don't use mongoose.Types.ObjectId.isValid method because it's a weird check,
// it returns for instance true for any integer value ¯\_(ツ)_/¯
const hexadecimal = /^[0-9A-F]+$/i;
return hexadecimal.test(value) && value.length === 24;
}
};
};

View File

@ -1,7 +1,7 @@
.containerFluid {
padding: 18px 30px;
> div:first-child {
max-height: 33px;
margin-bottom: 11px;
}
}
@ -192,4 +192,4 @@
.padded {
padding-bottom: 1px;
}
}

View File

@ -1,7 +1,7 @@
.containerFluid {
padding: 18px 30px;
> div:first-child {
max-height: 33px;
margin-bottom: 11px;
}
}
@ -35,4 +35,4 @@
border-bottom: none;
}
}
}
}

View File

@ -49,4 +49,4 @@
"npm": ">= 5.0.0"
},
"license": "MIT"
}
}

View File

@ -25,6 +25,23 @@ module.exports = {
}, {});
},
convertToQuery: function(params) {
const result = {};
_.forEach(params, (value, key) => {
if (_.isPlainObject(value)) {
const flatObject = this.convertToQuery(value);
_.forEach (flatObject, (_value, _key) => {
result[`${key}.${_key}`] = _value;
});
} else {
result[key] = value;
}
});
return result;
},
/**
* Security to avoid infinite limit.
*
@ -175,13 +192,15 @@ module.exports = {
// Plural.
return async (ctx, next) => {
ctx.params = this.amountLimiting(ctx.params);
ctx.query = Object.assign(
this.convertToParams(_.omit(ctx.params, 'where')),
ctx.params.where,
const queryOpts = {};
queryOpts.params = this.amountLimiting(ctx.params);
queryOpts.query = Object.assign(
{},
this.convertToParams(_.omit(queryOpts.params, 'where')),
this.convertToQuery(queryOpts.params.where)
);
return controller(ctx, next);
return controller(Object.assign({}, ctx, queryOpts, { send: ctx.send }), next, { populate: [] });
};
})();
@ -256,8 +275,12 @@ module.exports = {
// Resolver can be a function. Be also a native resolver or a controller's action.
if (_.isFunction(resolver)) {
context.query = this.convertToParams(options);
context.params = this.amountLimiting(options);
context.query = Object.assign(
{},
this.convertToParams(_.omit(options, 'where')),
this.convertToQuery(options.where)
);
if (isController) {
const values = await resolver.call(null, context);

View File

@ -398,11 +398,11 @@ module.exports = {
queryOpts.skip = convertedParams.start;
switch (association.nature) {
case 'manyToMany': {
case "manyToMany": {
const arrayOfIds = (obj[association.alias] || []).map(
related => {
return related[ref.primaryKey] || related;
},
}
);
// Where.
@ -413,7 +413,6 @@ module.exports = {
...where.where,
}).where;
break;
// falls through
}
default:
// Where.

View File

@ -47,4 +47,4 @@
"npm": ">= 3.0.0"
},
"license": "MIT"
}
}

View File

@ -5,7 +5,7 @@
.containerFluid {
padding: 18px 30px;
> div:first-child {
max-height: 33px;
margin-bottom: 11px;
}
}
@ -37,4 +37,4 @@
justify-content: center;
min-height: 260px;
margin: auto;
}
}

View File

@ -1,27 +1,16 @@
const _ = require('lodash');
module.exports = {
find: async function (params = {}, populate) {
const records = await this.query(function(qb) {
_.forEach(params.where, (where, key) => {
if (_.isArray(where.value)) {
for (const value in where.value) {
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]);
}
} else {
qb.where(key, where.symbol, where.value);
}
});
const hook = strapi.hook[this.orm];
const records = await this.query((qb) => {
// Generate match stage.
hook.load().generateMatchStage(qb)(this, params);
if (params.start) {
qb.offset(params.start);
}
if (params.limit) {
qb.limit(params.limit);
}
if (params.sort) {
if (_.has(params, 'start')) qb.offset(params.start);
if (_.has(params, 'limit')) qb.limit(params.limit);
if (!_.isEmpty(params.sort)) {
if (params.sort.key) {
qb.orderBy(params.sort.key, params.sort.order);
} else {
@ -33,7 +22,6 @@ module.exports = {
withRelated: populate || _.keys(_.groupBy(_.reject(this.associations, { autoPopulate: false }), 'alias'))
});
return records ? records.toJSON() : records;
},

View File

@ -1,14 +1,22 @@
const _ = require('lodash');
const { models: { mergeStages } } = require('strapi-utils');
module.exports = {
find: async function (params = {}, populate) {
return this
.find(params.where)
.limit(Number(params.limit))
.sort(params.sort)
.skip(Number(params.skip))
.populate(populate || this.associations.map(x => x.alias).join(' '))
.lean();
find: async function (filters = {}, populate) {
const hook = strapi.hook[this.orm];
// Generate stages.
const populateStage = hook.load().generateLookupStage(this, { whitelistedPopulate: populate }); // Nested-Population
const matchStage = hook.load().generateMatchStage(this, filters); // Nested relation filter
const aggregateStages = mergeStages(populateStage, matchStage);
const result = this.aggregate(aggregateStages);
if (_.has(filters, 'start')) result.skip(filters.start);
if (_.has(filters, 'limit')) result.limit(filters.limit);
if (_.has(filters, 'sort')) result.sort(filters.sort);
return result;
},
count: async function (params = {}) {

View File

@ -23,12 +23,13 @@
},
"dependencies": {
"bcryptjs": "^2.4.3",
"grant-koa": "^3.8.1",
"grant-koa": "^4.2.0",
"jsonwebtoken": "^8.1.0",
"koa": "^2.1.0",
"koa2-ratelimit": "^0.6.1",
"purest": "^2.0.1",
"request": "^2.83.0",
"strapi-utils": "3.0.0-alpha.14.5",
"uuid": "^3.1.0"
},
"devDependencies": {
@ -55,4 +56,4 @@
"npm": ">= 5.0.0"
},
"license": "MIT"
}
}

View File

@ -39,4 +39,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -42,4 +42,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -42,4 +42,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -41,4 +41,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -43,4 +43,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -43,4 +43,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -39,4 +39,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -13,4 +13,4 @@
"pkgcloud": "^1.5.0",
"streamifier": "^0.1.1"
}
}
}

View File

@ -9,17 +9,15 @@ const path = require('path');
// Public node modules.
const _ = require('lodash');
const pluralize = require('pluralize');
// Following this discussion https://stackoverflow.com/questions/18082/validate-decimal-numbers-in-javascript-isnumeric this function is the best implem to determine if a value is a valid number candidate
const isNumeric = (value) => {
return !_.isObject(value) && !isNaN(parseFloat(value)) && isFinite(value);
};
// Constants
const ORDERS = ['ASC', 'DESC'];
/* eslint-disable prefer-template */
/*
* Set of utils for models
*/
module.exports = {
/**
@ -37,7 +35,6 @@ module.exports = {
getPK: function (collectionIdentity, collection, models) {
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
try {
const GraphQLFunctions = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi-' + ORM, 'lib', 'utils'));
@ -311,6 +308,16 @@ module.exports = {
return _.get(strapi.models, collectionIdentity.toLowerCase() + '.orm');
},
/**
* Return table name for a collection many-to-many
*/
getCollectionName: (associationA, associationB) => {
return [associationA, associationB]
.sort((a, b) => a.collection < b.collection ? -1 : 1)
.map(table => _.snakeCase(`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`))
.join('__');
},
/**
* Define associations key to models
*/
@ -340,7 +347,7 @@ module.exports = {
// Build associations object
if (association.hasOwnProperty('collection') && association.collection !== '*') {
definition.associations.push({
const ast = {
alias: key,
type: 'collection',
collection: association.collection,
@ -350,7 +357,13 @@ module.exports = {
dominant: details.dominant !== true,
plugin: association.plugin || undefined,
filter: details.filter,
});
};
if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') {
ast.tableCollectionName = this.getCollectionName(association, details);
}
definition.associations.push(ast);
} else if (association.hasOwnProperty('model') && association.model !== '*') {
definition.associations.push({
alias: key,
@ -418,9 +431,32 @@ module.exports = {
return _.findKey(strapi.models[association.model || association.collection].attributes, {via: attribute});
},
convertParams: (entity, params) => {
mergeStages: (...stages) => {
return _.unionWith(...stages, _.isEqual);
},
convertParams: function (entity, params) {
const { model, models, convertor, postProcessValue } = this.prepareStage(
entity,
params
);
const _filter = this.splitPrimitiveAndRelationValues(params);
// Execute Steps in the given order
return _.flow([
this.processValues({ model, models, convertor, postProcessValue }),
this.processPredicates({ model, models, convertor }),
this.processGeneratedResults(),
this.mergeWhereAndRelationPayloads()
])(_filter);
},
prepareStage: function (entity, params) {
if (!entity) {
throw new Error('You can\'t call the convert params method without passing the model\'s name as a first argument.');
throw new Error(
'You can\'t call the convert params method without passing the model\'s name as a first argument.'
);
}
// Remove the source params (that can be sent from the ctm plugin) since it is not a filter
@ -428,86 +464,229 @@ module.exports = {
delete params.source;
}
const model = entity.toLowerCase();
const modelName = entity.toLowerCase();
const models = this.getStrapiModels();
const model = models[modelName];
const models = _.assign(_.clone(strapi.models), Object.keys(strapi.plugins).reduce((acc, current) => {
_.assign(acc, _.get(strapi.plugins[current], ['models'], {}));
return acc;
}, {}));
if (!models.hasOwnProperty(model)) {
return this.log.error(`The model ${model} can't be found.`);
if (!model) {
throw new Error(`The model ${modelName} can't be found.`);
}
const client = models[model].client;
const connector = models[model].orm;
if (!connector) {
throw new Error(`Impossible to determine the ORM used for the model ${model}.`);
if (!model.orm) {
throw new Error(
`Impossible to determine the ORM used for the model ${modelName}.`
);
}
const convertor = strapi.hook[connector].load().getQueryParams;
const convertParams = {
where: {},
sort: '',
start: 0,
limit: 100
const hook = strapi.hook[model.orm];
const convertor = hook.load().getQueryParams;
const postProcessValue = hook.load().postProcessValue || _.identity;
return {
models,
model,
hook,
convertor,
postProcessValue,
};
},
_.forEach(params, (value, key) => {
let result;
let formattedValue;
let modelAttributes = models[model]['attributes'];
let fieldType;
// Get the field type to later check if it's a string before number conversion
if (modelAttributes[key]) {
fieldType = modelAttributes[key]['type'];
} else {
// Remove the filter keyword at the end
let splitKey = key.split('_').slice(0,-1);
splitKey = splitKey.join('_');
getStrapiModels: function() {
return {
...strapi.models,
...Object.keys(strapi.plugins).reduce(
(acc, pluginName) => ({
...acc,
..._.get(strapi.plugins[pluginName], 'models', {}),
}),
{}
),
};
},
if (modelAttributes[splitKey]) {
fieldType = modelAttributes[splitKey]['type'];
}
}
// Check if the value is a valid candidate to be converted to a number value
if (fieldType !== 'string') {
formattedValue = isNumeric(value)
? _.toNumber(value)
: value;
} else {
formattedValue = value;
}
if (_.includes(['_start', '_limit'], key)) {
result = convertor(formattedValue, key);
} else if (key === '_sort') {
const [attr, order = 'ASC'] = formattedValue.split(':');
result = convertor(order, key, attr);
} else {
const suffix = key.split('_');
// Mysql stores boolean as 1 or 0
if (client === 'mysql' && _.get(models, [model, 'attributes', suffix, 'type']) === 'boolean') {
formattedValue = value === 'true' ? '1' : '0';
}
let type;
if (_.includes(['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss', 'in'], _.last(suffix))) {
type = `_${_.last(suffix)}`;
key = _.dropRight(suffix).join('_');
splitPrimitiveAndRelationValues: function(_query) {
const result = _.reduce(
_query,
(acc, value, key) => {
if (_.startsWith(key, '_')) {
acc[key] = value;
} else if (!_.includes(key, '.')) {
acc.where[key] = value;
} else {
type = '=';
_.set(acc.relations, this.injectRelationInKey(key), value);
}
return acc;
},
{
where: {},
relations: {},
sort: '',
start: 0,
limit: 100,
}
);
return result;
},
result = convertor(formattedValue, type, key);
injectRelationInKey: function (key) {
const numberOfRelations = key.match(/\./gi).length - 1;
const relationStrings = _.times(numberOfRelations, _.constant('relations'));
return _.chain(key)
.split('.')
.zip(relationStrings)
.flatten()
.compact()
.join('.')
.value();
},
transformFilter: function (filter, iteratee) {
if (!_.isArray(filter) && !_.isPlainObject(filter)) {
return filter;
}
return _.transform(filter, (updatedFilter, value, key) => {
const updatedValue = iteratee(value, key);
updatedFilter[key] = this.transformFilter(updatedValue, iteratee);
return updatedFilter;
});
},
processValues: function ({ model, models, convertor, postProcessValue }) {
return filter => {
let parentModel = model;
return this.transformFilter(filter, (value, key) => {
const field = this.getFieldFromKey(key, parentModel);
if (!field) {
return this.processMeta(value, key, {
field,
client: model.client,
model,
convertor,
});
}
if (field.collection || field.model) {
parentModel = models[field.collection || field.model];
}
return postProcessValue(
this.processValue(value, key, { field, client: model.client, model })
);
});
};
},
getFieldFromKey: function (key, model) {
let field;
// Primary key is a unique case because it doesn't belong to the model's attributes
if (key === model.primaryKey) {
field = {
type: 'ID', // Just in case
};
} else if (model.attributes[key]) {
field = model.attributes[key];
} else {
// Remove the filter keyword at the end
let splitKey = key.split('_').slice(0, -1);
splitKey = splitKey.join('_');
if (model.attributes[splitKey]) {
field = model.attributes[splitKey];
}
}
return field;
},
processValue: function (value, key, { field, client }) {
if (field.type === 'boolean' && client === 'mysql') {
return value === 'true' ? '1' : '0';
}
return value;
},
processMeta: function (value, key, { convertor, model }) {
if (_.includes(['_start', '_limit'], key)) {
return convertor(value, key);
} else if (key === '_sort') {
return this.processSortMeta(value, key, { convertor, model });
}
return value;
},
processSortMeta: function (value, key, { convertor, model }) {
const [attr, order = 'ASC'] = value.split(':');
if (!_.includes(ORDERS, order)) {
throw new Error(
`Unkown order value: "${order}", available values are: ${ORDERS.join(
', '
)}`
);
}
const field = this.getFieldFromKey(attr, model);
if (!field) {
throw new Error(`Unkown field: "${attr}"`);
}
return convertor(order, key, attr);
},
processPredicates: function ({ model, models, convertor }) {
return filter => {
let parentModel = model;
return this.transformFilter(filter, (value, key) => {
const field = this.getFieldFromKey(key, parentModel);
if (!field) {
return value;
}
if (field.collection || field.model) {
parentModel = models[field.collection || field.model];
}
return this.processCriteriaMeta(value, key, { convertor });
});
};
},
processCriteriaMeta: function (value, key, { convertor }) {
let type = '=';
if (key.match(/_{1}(?:ne|lte?|gte?|containss?|in)/)) {
type = key.match(/_{1}(?:ne|lte?|gte?|containss?|in)/)[0];
key = key.replace(type, '');
}
return convertor(value, type, key);
},
processGeneratedResults: function() {
return filter => {
if (!_.isArray(filter) && !_.isPlainObject(filter)) {
return filter;
}
_.set(convertParams, result.key, result.value);
});
return _.transform(filter, (updatedFilter, value, key) => {
// Only set results for object of shape { value, key }
if (_.has(value, 'value') && _.has(value, 'key')) {
const cleanKey = _.replace(value.key, 'where.', '');
_.set(updatedFilter, cleanKey, this.processGeneratedResults()(value.value));
} else {
updatedFilter[key] = this.processGeneratedResults()(value);
}
return convertParams;
return updatedFilter;
});
};
},
mergeWhereAndRelationPayloads: function() {
return filter => {
return {
...filter, // Normally here we need to omit where key
relations: {
...filter.where,
relations: filter.relations
}
};
};
}
};

View File

@ -23,6 +23,7 @@
"knex": "^0.13.0",
"lodash": "^4.17.5",
"pino": "^4.7.1",
"pluralize": "^7.0.0",
"shelljs": "^0.7.7"
},
"author": {

View File

@ -34,7 +34,6 @@ const watcher = (label, cmd, withSuccess = true) => {
shell.echo('✅ Success');
shell.echo('');
}
};
const asyncWatcher = (label, cmd, withSuccess = true, resolve) => {
@ -88,7 +87,6 @@ if (shell.test('-e', 'admin/src/config/plugins.json') === false) {
shell.cd('../../../');
}
watcher('📦 Linking strapi-admin', 'npm link --no-optional', false);
shell.cd('../strapi-generate-admin');
@ -112,18 +110,33 @@ watcher('', 'npm install ../strapi-hook-knex');
watcher('📦 Linking strapi-hook-bookshelf...', 'npm link');
shell.cd('../strapi');
watcher('', 'npm install ../strapi-generate ../strapi-generate-admin ../strapi-generate-api ../strapi-generate-new ../strapi-generate-plugin ../strapi-generate-policy ../strapi-generate-service ../strapi-utils');
watcher(
'',
'npm install ../strapi-generate ../strapi-generate-admin ../strapi-generate-api ../strapi-generate-new ../strapi-generate-plugin ../strapi-generate-policy ../strapi-generate-service ../strapi-utils'
);
watcher('📦 Linking strapi...', 'npm link');
shell.cd('../strapi-plugin-graphql');
watcher('📦 Linking strapi-plugin-graphql...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-plugin-graphql...',
'npm link --no-optional',
false
);
// Plugin services
shell.cd('../strapi-provider-upload-local');
watcher('📦 Linking strapi-provider-upload-local...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-provider-upload-local...',
'npm link --no-optional',
false
);
shell.cd('../strapi-provider-email-sendmail');
watcher('📦 Linking strapi-provider-email-sendmail...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-provider-email-sendmail...',
'npm link --no-optional',
false
);
// Plugins with admin
shell.cd('../strapi-plugin-email');
@ -134,19 +147,31 @@ watcher('📦 Linking strapi-plugin-email...', 'npm link --no-optional', false)
shell.cd('../strapi-plugin-users-permissions');
watcher('', 'npm install ../strapi-helper-plugin --no-optional');
watcher('', 'npm install ../strapi-utils --no-optional');
shell.rm('-f', 'package-lock.json');
watcher('📦 Linking strapi-plugin-users-permissions...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-plugin-users-permissions...',
'npm link --no-optional',
false
);
shell.cd('../strapi-plugin-content-manager');
watcher('', 'npm install ../strapi-helper-plugin --no-optional');
shell.rm('-f', 'package-lock.json');
watcher('📦 Linking strapi-plugin-content-manager...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-plugin-content-manager...',
'npm link --no-optional',
false
);
shell.cd('../strapi-plugin-settings-manager');
watcher('', 'npm install ../strapi-helper-plugin --no-optional');
shell.rm('-f', 'package-lock.json');
watcher('📦 Linking strapi-plugin-settings-manager...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-plugin-settings-manager...',
'npm link --no-optional',
false
);
// Plugins with admin and other plugin's dependencies
shell.cd('../strapi-plugin-upload');
@ -160,16 +185,32 @@ watcher('', 'npm install ../strapi-helper-plugin --no-optional');
watcher('', 'npm install ../strapi-generate --no-optional');
watcher('', 'npm install ../strapi-generate-api --no-optional');
shell.rm('-f', 'package-lock.json');
watcher('📦 Linking strapi-plugin-content-type-builder...', 'npm link --no-optional', false);
watcher(
'📦 Linking strapi-plugin-content-type-builder...',
'npm link --no-optional',
false
);
const pluginsToBuild = ['admin', 'content-manager', 'content-type-builder', 'upload', 'email', 'users-permissions', 'settings-manager'];
const pluginsToBuild = [
'admin',
'content-manager',
'content-type-builder',
'upload',
'email',
'users-permissions',
'settings-manager'
];
const buildPlugins = async () => {
const build = (pckgName) => {
const build = pckgName => {
return new Promise(resolve => {
const name = pckgName === 'admin' ? pckgName: `plugin-${pckgName}`;
asyncWatcher(`🏗 Building ${name}...`, `cd ../strapi-${name} && IS_MONOREPO=true npm run build`, false, resolve);
const name = pckgName === 'admin' ? pckgName : `plugin-${pckgName}`;
asyncWatcher(
`🏗 Building ${name}...`,
`cd ../strapi-${name} && IS_MONOREPO=true npm run build`,
false,
resolve
);
});
};
@ -178,23 +219,34 @@ const buildPlugins = async () => {
const setup = async () => {
if (process.env.npm_config_build) {
if (process.platform === 'darwin') { // Allow async build for darwin platform
if (process.platform === 'darwin') {
// Allow async build for darwin platform
await buildPlugins();
} else {
pluginsToBuild.map(name => {
const pluginName = name === 'admin' ? name : `plugin-${name}`;
shell.cd(`../strapi-${pluginName}`);
return watcher(`🏗 Building ${pluginName}...`, 'IS_MONOREPO=true npm run build');
return watcher(
`🏗 Building ${pluginName}...`,
'IS_MONOREPO=true npm run build'
);
});
}
}
// Log installation duration.
const installationEndDate = new Date();
const duration = (installationEndDate.getTime() - installationStartDate.getTime()) / 1000;
const duration =
(installationEndDate.getTime() - installationStartDate.getTime()) / 1000;
shell.echo('✅ Strapi has been succesfully installed.');
shell.echo(`⏳ The installation took ${Math.floor(duration / 60) > 0 ? `${Math.floor(duration / 60)} minutes and ` : ''}${Math.floor(duration % 60)} seconds.`);
shell.echo(
`⏳ The installation took ${
Math.floor(duration / 60) > 0
? `${Math.floor(duration / 60)} minutes and `
: ''
}${Math.floor(duration % 60)} seconds.`
);
};
setup();