Merge branch 'master' into fix/hidden-fields-schema

This commit is contained in:
Jim LAURIE 2018-11-27 12:19:04 +01:00 committed by GitHub
commit 70e5816f46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1094 additions and 323 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

@ -74,7 +74,7 @@ To use the providers authentication, set your credentials in
Redirect your user to: `GET /connect/:provider`.
After his approval, he will be redirected to `/auth/:provider/callback`. The jwt and user will be available in the querystring.
After their approval, they will be redirected to `/auth/:provider/callback`. The jwt and user will be available in the querystring.
Response payload:

View File

@ -1,6 +1,6 @@
# Quick start
This section explains how to handle Strapi for the first time, ([check out our tutorial video](https://www.youtube.com/watch?v=yMl5IcFHA74)).
This section explains how to handle Strapi for the first time, ([check out our video tutorial](https://www.youtube.com/watch?v=yMl5IcFHA74)).
**Table of contents:**
- [1. Create your first project](#_1-create-a-project)
@ -170,7 +170,7 @@ axios
If you want to get a specific entry, add the `id` of the wanted post at the end of the url.
Examble with Axios:
Example with Axios:
```js
import axios from 'axios';
@ -194,7 +194,7 @@ axios
Use the `POST` route to create a new entry.
Examble with Axios:
Example with Axios:
```js
import axios from 'axios';
@ -221,7 +221,7 @@ axios
Use the `PUT` route to update an existing entry.
Examble with Axios:
Example with Axios:
```js
import axios from 'axios';
@ -250,7 +250,7 @@ axios
Use the `DELETE` route to delete an existing entry.
Examble with Axios:
Example with Axios:
```js
import axios from 'axios';

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

@ -296,7 +296,7 @@ A `product` can be related to many `categories`, so a `category` can have many `
```
::: note
The `dominant` key allows you to define in which table/collection (only for NoSQL databases) should be stored the array that defines the relationship. Because there is no join table in NoSQL, this key is required for NoSQL databases (ex: MongoDB).
(NoSQL databases only) The `dominant` key defines which table/collection should store the array that defines the relationship. Because there are no join tables in NoSQL, this key is required for NoSQL databases (ex: MongoDB).
:::
**Path —** `./api/category/models/Category.settings.json`.

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

@ -1,3 +1,3 @@
{
"languages": ["en", "ar", "es", "fr", "de", "it", "ko", "nl", "pl", "pt", "pt-BR", "ru", "tr", "zh", "zh-Hans", "ja"]
"languages": ["en", "ar", "es", "fa", "fr", "de", "it", "ko", "nl", "pl", "pt", "pt-BR", "ru", "tr", "zh", "zh-Hans", "ja"]
}

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

@ -37,6 +37,8 @@ export class LocaleToggle extends React.Component { // eslint-disable-line
return 'https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/flags/4x3/kr.svg';
case 'ja':
return 'https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/flags/4x3/jp.svg';
case 'fa':
return 'https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/flags/4x3/ir.svg';
default:
return `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/flags/4x3/${locale}.svg`;
}

View File

@ -0,0 +1,141 @@
{
"Analytics": "آنالیز",
"Content Manager": "مدیریت محتوا",
"Content Type Builder": "سازنده ی الگوی محتوایی",
"Email": "ایمیل",
"Files Upload": "آپلود فایل",
"HomePage.notification.newsLetter.success": "موفقیت در اشتراک خبر نامه",
"New entry": "رکورد جدید",
"Password": "رمز عبور",
"Provider": "سرویس دهنده",
"ResetPasswordToken": "بازنشانی رمز عبور",
"Role": "نقش",
"Roles & Permissions": "نقش ها و دسترسی ها",
"Settings Manager": "مدیریت تنظیمات",
"Username": "نام کاربری",
"Users": "کاربران",
"Users & Permissions": "کاربران و دسترسی ها",
"app.components.BlockLink.code": "نمونه کد",
"app.components.BlockLink.code.content": "با بررسی و آزمایش پروژه های واقعی توسط جامعه توسعه دهنده Strapi را یاد بگیرید.",
"app.components.BlockLink.documentation": "خواندن مستندات",
"app.components.BlockLink.documentation.content": "مفاهیم، راهنمای مرجع و آموزش ها را فرا بگیرید.",
"app.components.Button.cancel": "کنسل",
"app.components.Button.save": "ذخیره",
"app.components.ComingSoonPage.comingSoon": "بزودی",
"app.components.ComingSoonPage.featuresNotAvailable": "این ویژگی هنوز در حال توسعه است.",
"app.components.DownloadInfo.download": "در حال دانلود...",
"app.components.DownloadInfo.text": "ممکن است دقایقی طول بکشد، با تشکر از شکیبایی شما.",
"app.components.EmptyAttributes.title": "هیچ فیلدی وجود ندارد",
"app.components.HomePage.button.blog": "مشاهده بیشتر در بلاگ",
"app.components.HomePage.button.quickStart": "شروع آموزش سریع",
"app.components.HomePage.community": "جستجوی جامعه توسعه دهنده در اینترنت",
"app.components.HomePage.community.content": "گفتگو با اعضای تیم و توسعه دهندگان در کانال های مختلف ارتباطی",
"app.components.HomePage.create": "ساخت اولین الگوی محتوا",
"app.components.HomePage.createBlock.content.first": "این ",
"app.components.HomePage.createBlock.content.second": " افزونه به شما کمک میکند مدل ساختاری داده های خود را تعریف کنید،اگر شما جدیدا به اینجا مراجعه کردید، پس حتما مارا دنبال کنید ",
"app.components.HomePage.createBlock.content.tutorial": " آموزش.",
"app.components.HomePage.cta": "تایید",
"app.components.HomePage.newsLetter": "برای آگاهی از آخرین اخبار Strapi مشترک خبرنامه شوید.",
"app.components.HomePage.support": "مارا حمایت کنید",
"app.components.HomePage.support.content": "با خرید تی شرت ما میتوانیم به توسعه Strapi ادامه داده و بهترین تجربه را برای شما فراهم کنیم!",
"app.components.HomePage.support.link": "هم اکنون تی شرت خود را دریافت کنید",
"app.components.HomePage.welcome": "خوش آمدید!",
"app.components.HomePage.welcome.again": "خوش آمدید ",
"app.components.HomePage.welcomeBlock.content": "خرسندیم که شما را به عنوان عضوی از جامعه توسعه دهنده بشناسیم، ما هر لحظه به دنبال دریافت بازخورد شما هستیم، در ارسال بازخورد تردید نکنید ",
"app.components.HomePage.welcomeBlock.content.again": "امیدواریم در پروژه خود پیشرفت کنید... میتوانید در مورد آخرین های Strapi مطالعه کنید. بر اساس بازخورد شما ما موثر ترین اقدامات را برای بهبود محصول انجام خواهیم داد.",
"app.components.HomePage.welcomeBlock.content.issues": "مشکلات.",
"app.components.HomePage.welcomeBlock.content.raise": " یا ارتقاء دهید ",
"app.components.ImgPreview.hint": "برای آپلود، فایل خود را بکشید و در این نقطه رها کنید یا {browse}",
"app.components.ImgPreview.hint.browse": "مرور کردن",
"app.components.InputFile.newFile": "افزودن فایل جدید",
"app.components.InputFileDetails.open": "باز کردن در تب جدید",
"app.components.InputFileDetails.originalName": "نام اصلی:",
"app.components.InputFileDetails.remove": "حذف این فایل",
"app.components.InputFileDetails.size": "حجم:",
"app.components.InstallPluginPage.InputSearch.label": " ",
"app.components.InstallPluginPage.InputSearch.placeholder": "جستجوی افزونه... (برای مثال: authentication)",
"app.components.InstallPluginPage.description": "به راحتی برنامه خود را توسعه دهید.",
"app.components.InstallPluginPage.helmet": "فروشگاه - افزونه ها",
"app.components.InstallPluginPage.plugin.support-us.description": "با خرید تی شرت ما میتوانیم به توسعه Strapi ادامه داده و بهترین تجربه را برای شما فراهم کنیم!",
"app.components.InstallPluginPage.title": "فروشگاه - افزونه ها",
"app.components.InstallPluginPopup.downloads": "دانلود",
"app.components.InstallPluginPopup.navLink.avis": "مشاهده",
"app.components.InstallPluginPopup.navLink.changelog": "تغییرات",
"app.components.InstallPluginPopup.navLink.description": "توضیحات",
"app.components.InstallPluginPopup.navLink.faq": "پرسش و پاسخ",
"app.components.InstallPluginPopup.navLink.screenshots": "تصاویر",
"app.components.InstallPluginPopup.noDescription": "توضیحاتی وجود ندارد",
"app.components.LeftMenuFooter.poweredBy": "قدرت گرفته از ",
"app.components.LeftMenuLinkContainer.configuration": "پیکربندی",
"app.components.LeftMenuLinkContainer.general": "عمومی",
"app.components.LeftMenuLinkContainer.installNewPlugin": "فروشگاه",
"app.components.LeftMenuLinkContainer.listPlugins": "افزونه ها",
"app.components.LeftMenuLinkContainer.noPluginsInstalled": "هیچ افزونه ای نصب نشده است",
"app.components.LeftMenuLinkContainer.plugins": "افزونه ها",
"app.components.ListPluginsPage.description": "فهرست افزونه های نصب شده در پروژه.",
"app.components.ListPluginsPage.helmet.title": "فهرست افزونه ها",
"app.components.ListPluginsPage.title": "افزونه ها",
"app.components.NotFoundPage.back": "بازگشت به صفحه اصلی",
"app.components.NotFoundPage.description": "یافت نشد",
"app.components.Official": "رسمی",
"app.components.PluginCard.Button.label.download": "دانلود",
"app.components.PluginCard.Button.label.install": "قبلا نصب شده است",
"app.components.PluginCard.Button.label.support": "حمایت از ما",
"app.components.PluginCard.compatible": "سازگار با برنامه ی شما",
"app.components.PluginCard.compatibleCommunity": "سازگار با جامعه ی توسعه دهنده",
"app.components.PluginCard.more-details": "اطلاعات بیشتر",
"app.components.PluginCard.price.free": "ریگان",
"app.components.listPlugins.button": "افزودن افزونه جدید",
"app.components.listPlugins.title.none": "هیچ افزونه ای نصب نشده است",
"app.components.listPlugins.title.plural": "{number} افزونه نصب شده",
"app.components.listPlugins.title.singular": "{number} افزونه نصب شده",
"app.components.listPluginsPage.deletePlugin.error": "خطا در هنگام حذف این افزونه",
"app.utils.SelectOption.defaultMessage": " ",
"app.utils.defaultMessage": " ",
"app.utils.placeholder.defaultMessage": " ",
"components.AutoReloadBlocker.description": "فایل را باز کرده و این ویژگی را فعال کنید.",
"components.AutoReloadBlocker.header": "بارگذاری مجدد برای این افزونه ضروری است.",
"components.ErrorBoundary.title": "خطایی رخ داده است...",
"components.Input.error.attribute.key.taken": "این مقدار در حال حاضر وجود دارد",
"components.Input.error.attribute.sameKeyAndName": "نمیتواند برابر باشد",
"components.Input.error.attribute.taken": "نام فیلد در حال حاضر وجود دارد",
"components.Input.error.contentTypeName.taken": "این نام هم اکنون وجود دارد",
"components.Input.error.custom-error": "{errorMessage} ",
"components.Input.error.validation.email": "این مقدار پست الکترونیک صحیح نیست",
"components.Input.error.validation.json": "این مقدار با فرمت استاندارد JSON مطابقت ندارد",
"components.Input.error.validation.max": "این مقدار زیاد است.",
"components.Input.error.validation.maxLength": "این مقدار طولانی است.",
"components.Input.error.validation.min": "این مقدار کوچک است.",
"components.Input.error.validation.minLength": "طول این مقدار کم است.",
"components.Input.error.validation.minSupMax": "نمی تواند بالاتر باشد",
"components.Input.error.validation.regex": "این مقدار با عبارت منظم مطابقت ندارد.",
"components.Input.error.validation.required": "این مقدار ضروری است.",
"components.ListRow.empty": "داده ای جهت نمایش وجود ندارد.",
"components.OverlayBlocker.description": "اقدام مورد نظر نیازمند راه اندازی مجدد سرور است. لطفا منتظر بمانید.",
"components.OverlayBlocker.title": "در انتظار راه اندازی مجدد...",
"components.PageFooter.select": "تعداد سطر در هر صفحه",
"components.ProductionBlocker.description": "به جهت موارد امنیتی ما باید این افزونه را در محیط های دیگر غیرفعال کنیم.",
"components.ProductionBlocker.header": "این افزونه فقط در حالت توسعه در دسترس است.",
"components.Wysiwyg.ToggleMode.markdown": "تبدیل به حالت markdown",
"components.Wysiwyg.ToggleMode.preview": "تبدیل به حالت نمایشی",
"components.Wysiwyg.collapse": "کوچک کردن",
"components.Wysiwyg.selectOptions.H1": "عنوان H1",
"components.Wysiwyg.selectOptions.H2": "عنوان H2",
"components.Wysiwyg.selectOptions.H3": "عنوان H3",
"components.Wysiwyg.selectOptions.H4": "عنوان H4",
"components.Wysiwyg.selectOptions.H5": "عنوان H5",
"components.Wysiwyg.selectOptions.H6": "عنوان H6",
"components.Wysiwyg.selectOptions.title": "افزدون عنوان",
"components.WysiwygBottomControls.charactersIndicators": "حروف",
"components.WysiwygBottomControls.fullscreen": "تمام صفحه",
"components.WysiwygBottomControls.uploadFiles": "کشیدن و رها کردن فایل، خواندن از حافظه یا {browse}.",
"components.WysiwygBottomControls.uploadFiles.browse": "انتخاب کنید",
"components.popUpWarning.button.cancel": "کنسل",
"components.popUpWarning.button.confirm": "تایید",
"components.popUpWarning.message": "آیا از حذف این مقدار اطمینان دارید؟",
"components.popUpWarning.title": "لطفا تایید کنید",
"notification.error": "خطایی رخ داده است",
"notification.error.layout": "خطا در بازیابی طرح",
"request.error.model.unknown": "این مدل وجود ندارد",
"app.utils.delete": "حذف"
}

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

@ -12,7 +12,15 @@ module.exports = {
// After saving a value.
// Fired after an `insert` or `update` query.
// afterSave: async (model, response, options) => {},
// Before fetching all values.
// Fired before a `fetchAll` operation.
// beforeFetchCollection: async (model, columns, options) => {},
// After fetching all values.
// Fired after a `fetchAll` operation.
// afterFetchCollection: async (model, columns, options) => {},
// Before fetching a value.
// Fired before a `fetch` operation.
// beforeFetch: async (model, columns, options) => {},

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

@ -28,22 +28,22 @@
],
"dependencies": {
"add-asset-html-webpack-plugin": "^2.1.2",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.1",
"babel-plugin-istanbul": "^4.1.5",
"babel-plugin-styled-components": "^1.5.1",
"babel-plugin-transform-es2015-destructuring": "^6.23.0",
"babel-plugin-transform-es2015-parameters": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-react-inline-elements": "^6.22.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.18",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-react-hmre": "^1.1.1",
"babel-preset-stage-0": "^6.24.1",
"babel-cli": "6.26.0",
"babel-core": "6.26.0",
"babel-loader": "7.1.1",
"babel-plugin-istanbul": "4.1.5",
"babel-plugin-styled-components": "1.5.1",
"babel-plugin-transform-es2015-destructuring": "6.23.0",
"babel-plugin-transform-es2015-parameters": "6.24.1",
"babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-plugin-transform-react-constant-elements": "6.23.0",
"babel-plugin-transform-react-inline-elements": "6.22.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.18",
"babel-polyfill": "6.26.0",
"babel-preset-env": "1.6.1",
"babel-preset-react": "6.24.1",
"babel-preset-react-hmre": "1.1.1",
"babel-preset-stage-0": "6.24.1",
"bootstrap": "^4.0.0-alpha.6",
"chalk": "^2.1.0",
"classnames": "^2.2.5",
@ -106,4 +106,4 @@
"webpack-hot-middleware": "^2.18.2",
"whatwg-fetch": "^2.0.3"
}
}
}

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,89 @@ 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 (let value in value.value) {
if (typeof value === 'string') {
value = {
value,
symbol: '='
};
}
qb[value ? 'where' : 'orWhere'](fieldKey, value.symbol, value.value[value]);
}
} else {
if (typeof value === 'string') {
value = {
value,
symbol: '='
};
}
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
@ -97,13 +242,13 @@ module.exports = {
acc[current] = params.values[current];
} else if (response[current] && _.isArray(response[current]) && current !== 'id') {
// Records to add in the relation.
const toAdd = _.differenceWith(params.values[current], response[current], (a, b) =>
a[this.primaryKey].toString() === b[this.primaryKey].toString()
const toAdd = _.differenceWith(params.values[current], response[current], (a, b) =>
(a[this.primaryKey] || a).toString() === (b[this.primaryKey] || b).toString()
);
// Records to remove in the relation.
const toRemove = _.differenceWith(response[current], params.values[current], (a, b) =>
a[this.primaryKey].toString() === b[this.primaryKey].toString()
(a[this.primaryKey] || a).toString() === (b[this.primaryKey] || b).toString()
)
.filter(x => toAdd.find(y => x.id === y.id) === undefined);

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

@ -8,7 +8,7 @@ import React from 'react';
import Select from 'react-select';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import { cloneDeep, isArray, isNull, isUndefined, get, findIndex, isEmpty } from 'lodash';
import { cloneDeep, includes, isArray, isNull, isUndefined, get, findIndex, isEmpty } from 'lodash';
// Utils.
import request from 'utils/request';
@ -103,6 +103,15 @@ class SelectMany extends React.PureComponent {
});
};
handleInputChange = (value) => {
const clonedOptions = this.state.options;
const filteredValues = clonedOptions.filter(data => includes(data.label, value));
if (filteredValues.length === 0) {
return this.getOptions(value);
}
}
handleChange = value => {
// Remove new added value from available option;
this.state.options = this.state.options.filter(el =>
@ -169,6 +178,7 @@ class SelectMany extends React.PureComponent {
id={this.props.relation.alias}
isLoading={this.state.isLoading}
onChange={this.handleChange}
onInputChange={this.handleInputChange}
onMenuScrollToBottom={this.handleBottomScroll}
options={this.state.options}
placeholder={<FormattedMessage id='content-manager.containers.Edit.addAnItem' />}

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

@ -357,16 +357,16 @@ module.exports = async cb => {
});
// Update other keys
sameApis.map(apiPath => {
sameApis.forEach(apiPath => {
// This doesn't keep the prevSettings for the relations, the user will have to reset it.
// We might have to improve this if we want the order of the relations to be kept
const keysToUpdate = ['relations', 'loadedModel', 'associations', 'attributes', ['editDisplay', 'relations']].map(key => apiPath.concat(key));
['relations', 'loadedModel', 'associations', 'attributes', ['editDisplay', 'relations']]
.map(key => apiPath.concat(key))
.forEach(keyPath => {
const newValue = _.get(schema.models, keyPath);
keysToUpdate.map(keyPath => {
const newValue = _.get(schema.models, keyPath);
_.set(prevSchema.models, keyPath, newValue);
});
_.set(prevSchema.models, keyPath, newValue);
});
});
// Special handler for the upload relations
@ -381,6 +381,7 @@ module.exports = async cb => {
});
await pluginStore.set({ key: 'schema', value: prevSchema });
} catch(err) {
console.log('error', err);
}

View File

@ -1,7 +1,8 @@
const _ = require('lodash');
/**
* Retrieve the path of each API
* @param {Object}} data
* @param {Object}} data
* @returns {Array} Array of API path ['plugins.upload.file', 'plugins.users-permissions.user', ...]
*/
const getApis = (data) => Object.keys(data).reduce((acc, curr) => {
@ -10,8 +11,8 @@ const getApis = (data) => Object.keys(data).reduce((acc, curr) => {
}
if (curr === 'plugins') {
Object.keys(data[curr]).map(plugin => {
Object.keys(data[curr][plugin]).map(api => {
Object.keys(data[curr]).forEach(plugin => {
Object.keys(data[curr][plugin]).forEach(api => {
acc = acc.concat([`${curr}.${plugin}.${api}`]);
});
});
@ -23,8 +24,8 @@ const getApis = (data) => Object.keys(data).reduce((acc, curr) => {
/**
* Retrieve all the fields from an api
* @param {Object} data
* @param {Array} apis
* @param {Object} data
* @param {Array} apis
* @returns {Array} Array composed of fields path for instance : [['plugins.users-permissions.user.fields.username', 'plugins.users-permissions.user.fields.email', 'plugins.users-permissions.user.fields.password'], [...]]
*/
const getApisKeys = (data, apis) => apis.map(apiPath => {
@ -43,12 +44,12 @@ const getApisUploadRelations = (data, sameArray) => sameArray.map(apiPath => {
.filter(relationName => {
return _.get(data.models, [...relationPath, relationName, 'plugin' ]) === 'upload';
});
return relations.map(relation => `${apiPath.join('.')}.editDisplay.availableFields.${relation}`);
});
/**
*
*
* @param {String} attrPath
* @returns {Array}
*/

View File

@ -105,9 +105,9 @@ module.exports = {
_.set(schema, [...schemaPath, 'editDisplay', 'fields'], newList.toJS());
}
Object.keys(attributes).map(attribute => {
Object.keys(attributes).forEach(attribute => {
const appearances = _.get(attributes, [attribute, 'appearance'], {});
Object.keys(appearances).map(appearance => {
Object.keys(appearances).forEach(appearance => {
_.set(layout, ['attributes', attribute, 'appearance'], appearances[appearance] ? appearance : '' );
});

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

@ -373,14 +373,14 @@ module.exports = {
source: association.plugin,
};
if (association.type === 'model') {
params.id = obj[association.alias];
} else {
// Get refering model.
const ref = association.plugin
? strapi.plugins[association.plugin].models[params.model]
: strapi.models[params.model];
// Get refering model.
const ref = association.plugin
? strapi.plugins[association.plugin].models[params.model]
: strapi.models[params.model];
if (association.type === 'model') {
params.id = _.get(obj, [association.alias, ref.primaryKey], obj[association.alias]);
} else {
// Apply optional arguments to make more precise nested request.
const convertedParams = strapi.utils.models.convertParams(
name,
@ -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

@ -46,6 +46,10 @@
"configurable": false,
"required": true
},
"public_id": {
"type": "string",
"configurable": false
},
"related": {
"collection": "*",
"filter": "field",

View File

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

View File

@ -53,6 +53,8 @@ class PopUpForm extends React.Component { // eslint-disable-line react/prefer-st
return get(this.props.values, 'redirect_uri', '');
case 'microsoft':
return `${strapi.backendURL}/connect/microsoft/callback`;
case 'twitter':
return `${strapi.backendURL}/connect/twitter/callback`;
default: {
const value = get(this.props.values, 'callback', '');

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

@ -8,7 +8,7 @@ export default function checkFormValidity(settingType, data, providerToEdit = ''
const isProviderEnabled = get(data, 'enabled');
const keys = providerToEdit === 'email' ? [] : ['key', 'secret'];
keys.map(key => {
keys.forEach(key => {
if (isProviderEnabled && isEmpty(get(data, key))) {
formErrors.push({ name: key, errors: [{ id: 'components.Input.error.validation.required' }] });
}
@ -16,9 +16,9 @@ export default function checkFormValidity(settingType, data, providerToEdit = ''
break;
}
case 'email-templates': {
Object.keys(data.options).map((value) => {
Object.keys(data.options).forEach((value) => {
if (isObject(data.options[value])) {
Object.keys(data.options[value]).map(subValue => {
Object.keys(data.options[value]).forEach(subValue => {
if (isEmpty(get(data, ['options', value, subValue]))) {
formErrors.push({ name: `options.${value}.${subValue}`, errors: [{ id: 'components.Input.error.validation.required' }] });
}

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

@ -147,12 +147,8 @@ module.exports = {
key: 'grant'
}).get();
_.defaultsDeep(grantConfig, {
server: {
protocol: 'http',
host: `${strapi.config.currentEnvironment.server.host}:${strapi.config.currentEnvironment.server.port}`
}
});
const [ protocol, host ] = strapi.config.url.split('://');
_.defaultsDeep(grantConfig, { server: { protocol, host } });
const provider = process.platform === 'win32' ? ctx.request.url.split('\\')[2] : ctx.request.url.split('/')[2];
const config = grantConfig[provider];
@ -306,7 +302,7 @@ module.exports = {
const settings = storeEmail['email_confirmation'] ? storeEmail['email_confirmation'].options : {};
settings.message = await strapi.plugins['users-permissions'].services.userspermissions.template(settings.message, {
URL: `http://${strapi.config.currentEnvironment.server.host}:${strapi.config.currentEnvironment.server.port}/auth/email-confirmation`,
URL: (new URL('/auth/email-confirmation', strapi.config.url)).toString(),
USER: _.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken', 'role', 'provider']),
CODE: jwt
});

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

@ -199,9 +199,9 @@ module.exports = {
const prefix = curr.config.prefix;
const path = prefix !== undefined ? `${prefix}${curr.path}` : `/${current}${curr.path}`;
_.set(curr, 'path', path);
return acc.concat(curr);
}, []);
}, []);
acc[current] = routes;
@ -221,7 +221,7 @@ module.exports = {
// Aggregate first level actions.
const appActions = Object.keys(strapi.api || {}).reduce((acc, api) => {
Object.keys(_.get(strapi.api[api], 'controllers', {}))
.map(controller => {
.forEach(controller => {
const actions = Object.keys(strapi.api[api].controllers[controller])
.filter(action => _.isFunction(strapi.api[api].controllers[controller][action]))
.map(action => `application.${controller}.${action.toLowerCase()}`);
@ -235,7 +235,7 @@ module.exports = {
// Aggregate plugins' actions.
const pluginsActions = Object.keys(strapi.plugins).reduce((acc, plugin) => {
Object.keys(strapi.plugins[plugin].controllers)
.map(controller => {
.forEach(controller => {
const actions = Object.keys(strapi.plugins[plugin].controllers[controller])
.filter(action => _.isFunction(strapi.plugins[plugin].controllers[controller][action]))
.map(action => `${plugin}.${controller}.${action.toLowerCase()}`);

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

@ -1,5 +1,12 @@
# strapi-provider-upload-cloudinary
## ⏳ Installation
```bash
cd plugins/upload
npm i --save strapi-provider-upload-cloudinary
````
## Resources
- [MIT License](LICENSE.md)

View File

@ -50,7 +50,7 @@ module.exports = {
},
async delete (file) {
try {
const response = await cloudinary.uploader.destroy(file.public_id + '3', {
const response = await cloudinary.uploader.destroy(file.public_id, {
invalidate: true
});
if (response.result !== 'ok') {

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

@ -323,26 +323,21 @@ module.exports.app = async function() {
// default settings
this.config.port = get(this.config.currentEnvironment, 'server.port') || this.config.port;
this.config.host = get(this.config.currentEnvironment, 'server.host') || this.config.host;
this.config.url = `http://${this.config.host}:${this.config.port}`;
// Admin.
const url = getURLFromSegments({ hostname: this.config.host, port: this.config.port });
const adminPath = get(this.config.currentEnvironment.server, 'admin.path', 'admin');
this.config.admin.devMode = isAdminInDevMode.call(this);
this.config.admin.url = this.config.admin.devMode ?
`http://${this.config.host}:4000/${get(this.config.currentEnvironment.server, 'admin.path', 'admin')}`:
`${this.config.url}/${get(this.config.currentEnvironment.server, 'admin.path', 'admin')}`;
(new URL(adminPath, `http://${this.config.host}:4000`)).toString():
(new URL(adminPath, url)).toString();
// proxy settings
this.config.proxy = get(this.config.currentEnvironment, 'server.proxy', {});
// check if SSL enabled and construct proxy url
function getProxyUrl(ssl, url) {
return `http${ssl ? 's' : ''}://${url}`;
}
const proxy = get(this.config.currentEnvironment, 'server.proxy', {});
this.config.proxy = proxy;
// check if proxy is enabled and construct url
if (get(this.config, 'proxy.enabled')) {
this.config.url = getProxyUrl(this.config.proxy.ssl, `${this.config.proxy.host}:${this.config.proxy.port}`);
}
this.config.url = proxy.enabled ? getURLFromSegments({ hostname: proxy.host, port: proxy.port, ssl: proxy.ssl }) : url;
};
const enableHookNestedDependencies = function (name, flattenHooksConfig, force = false) {
@ -368,7 +363,7 @@ const enableHookNestedDependencies = function (name, flattenHooksConfig, force =
});
return apiModelsUsed.length !== 0;
}) || 0; // Filter model with the right connector
}); // Filter model with the right connector
flattenHooksConfig[name] = {
enabled: force || modelsUsed.length > 0 // Will return false if there is no model, else true.
@ -392,3 +387,11 @@ const isAdminInDevMode = function () {
return true;
}
};
const getURLFromSegments = function ({ hostname, port, ssl = false }) {
const protocol = ssl ? 'https' : 'http';
const defaultPort = ssl ? 443 : 80;
const portString = (port === undefined || parseInt(port) === defaultPort) ? '' : `:${port}`;
return `${protocol}://${hostname}${portString}`;
};

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();