Merge branch 'master' into features/typescript

This commit is contained in:
Convly 2022-05-24 08:02:38 +02:00
commit 66e3aa5dcb
90 changed files with 7104 additions and 561 deletions

View File

@ -1,6 +1,6 @@
{
"name": "check-pr-status",
"version": "4.1.9",
"version": "4.1.11",
"main": "dist/index.js",
"license": "MIT",
"private": true,
@ -9,7 +9,7 @@
"watch": "NODE_ENV=production ncc build index.js -w -o dist --minify"
},
"devDependencies": {
"@actions/core": "1.8.0",
"@actions/core": "1.8.1",
"@actions/github": "5.0.0",
"@vercel/ncc": "0.33.3"
}

View File

@ -2,10 +2,10 @@
## Supported Versions
As of January 2022 (and until this document is updated), only the v3.0.0 and v4.0.0 _stable_ releases of Strapi are supported for updates. Any previous versions are currently not supported and users are advised to use them "at their own risk".
As of May 2022 (and until this document is updated), only the v4.x.x _stable_ releases of Strapi are supported for updates and bug fixes. Any previous versions are currently not supported and users are advised to use them "at their own risk".
- v3 support is limited to Critical/High priority security updates only until September 2022
- v4 is considered LTS until further notice
- v3.x.x support is limited to Critical/High severity security updates only until December 2022
- v4.x.x is considered LTS until further notice
## Reporting a Vulnerability

View File

@ -1,7 +1,7 @@
{
"name": "getstarted",
"private": true,
"version": "4.1.9",
"version": "4.1.11",
"description": "A Strapi application.",
"scripts": {
"develop": "strapi develop",
@ -12,17 +12,17 @@
"strapi": "strapi"
},
"dependencies": {
"@strapi/admin": "4.1.9",
"@strapi/plugin-documentation": "4.1.9",
"@strapi/plugin-graphql": "4.1.9",
"@strapi/plugin-i18n": "4.1.9",
"@strapi/plugin-sentry": "4.1.9",
"@strapi/plugin-users-permissions": "4.1.9",
"@strapi/provider-email-mailgun": "4.1.9",
"@strapi/provider-upload-aws-s3": "4.1.9",
"@strapi/provider-upload-cloudinary": "4.1.9",
"@strapi/strapi": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/admin": "4.1.11",
"@strapi/plugin-documentation": "4.1.11",
"@strapi/plugin-graphql": "4.1.11",
"@strapi/plugin-i18n": "4.1.11",
"@strapi/plugin-sentry": "4.1.11",
"@strapi/plugin-users-permissions": "4.1.11",
"@strapi/provider-email-mailgun": "4.1.11",
"@strapi/provider-upload-aws-s3": "4.1.11",
"@strapi/provider-upload-cloudinary": "4.1.11",
"@strapi/strapi": "4.1.11",
"@strapi/utils": "4.1.11",
"@vscode/sqlite3": "5.0.8",
"better-sqlite3": "7.4.6",
"lodash": "4.17.21",

View File

@ -1,7 +1,7 @@
{
"name": "kitchensink",
"private": true,
"version": "4.1.9",
"version": "4.1.11",
"description": "A Strapi application.",
"scripts": {
"develop": "strapi develop",
@ -12,12 +12,12 @@
"strapi": "strapi"
},
"dependencies": {
"@strapi/admin": "4.1.9",
"@strapi/provider-email-mailgun": "4.1.9",
"@strapi/provider-upload-aws-s3": "4.1.9",
"@strapi/provider-upload-cloudinary": "4.1.9",
"@strapi/strapi": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/admin": "4.1.11",
"@strapi/provider-email-mailgun": "4.1.11",
"@strapi/provider-upload-aws-s3": "4.1.11",
"@strapi/provider-upload-cloudinary": "4.1.11",
"@strapi/strapi": "4.1.11",
"@strapi/utils": "4.1.11",
"lodash": "4.17.21",
"mysql": "2.18.1",
"passport-google-oauth2": "0.2.0",

View File

@ -1,5 +1,5 @@
{
"version": "4.1.9",
"version": "4.1.11",
"packages": [
"packages/*",
"examples/*"

View File

@ -95,7 +95,7 @@
"execa": "1.0.0",
"fs-extra": "10.1.0",
"get-port": "5.1.1",
"glob": "7.2.0",
"glob": "7.2.3",
"husky": "3.1.0",
"istanbul": "~0.4.2",
"jest": "26.6.3",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/admin-test-utils",
"version": "4.1.9",
"version": "4.1.11",
"private": true,
"description": "Test utilities for the Strapi administration panel",
"license": "MIT",

View File

@ -81,8 +81,7 @@ async function initProject(projectName, program) {
return generateApp(projectName, program);
}
const prompt = await promptUser(projectName, program);
const prompt = await promptUser(projectName, program, hasDatabaseOptions);
const directory = prompt.directory || projectName;
await checkInstallPath(resolve(directory));

View File

@ -1,6 +1,6 @@
{
"name": "create-strapi-app",
"version": "4.1.9",
"version": "4.1.11",
"description": "Generate a new Strapi application.",
"keywords": [
"create-strapi-app",
@ -38,7 +38,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/generate-new": "4.1.9",
"@strapi/generate-new": "4.1.11",
"commander": "6.1.0",
"inquirer": "8.2.0"
},

View File

@ -7,8 +7,8 @@ const inquirer = require('inquirer');
* @param {string|null} template - The Github repo of the template
* @returns Object containting prompt answers
*/
module.exports = async function promptUser(projectName, program) {
const questions = await getPromptQuestions(projectName, program);
module.exports = async function promptUser(projectName, program, hasDatabaseOptions) {
const questions = await getPromptQuestions(projectName, program, hasDatabaseOptions);
return inquirer.prompt(questions);
};
@ -17,7 +17,7 @@ module.exports = async function promptUser(projectName, program) {
* @param {string|null} template - The template the project should use
* @returns Array of prompt question objects
*/
async function getPromptQuestions(projectName, program) {
async function getPromptQuestions(projectName, program, hasDatabaseOptions) {
return [
{
type: 'input',
@ -30,7 +30,7 @@ async function getPromptQuestions(projectName, program) {
type: 'list',
name: 'quick',
message: 'Choose your installation type',
when: !program.quickstart,
when: !program.quickstart && !hasDatabaseOptions,
choices: [
{
name: 'Quickstart (recommended)',

View File

@ -1,6 +1,6 @@
{
"name": "create-strapi-starter",
"version": "4.1.9",
"version": "4.1.11",
"description": "Generate a new Strapi application.",
"keywords": [
"create-strapi-starter",
@ -38,7 +38,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/generate-new": "4.1.9",
"@strapi/generate-new": "4.1.11",
"chalk": "4.1.1",
"ci-info": "3.1.1",
"commander": "7.1.0",

View File

@ -1,43 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Wrapper from './Wrapper';
const DynamicComponentCard = ({ children, componentUid, friendlyName, icon, onClick }) => {
return (
<Wrapper
onClick={e => {
e.preventDefault();
e.stopPropagation();
onClick(componentUid);
}}
>
<button className="component-icon" type="button">
<FontAwesomeIcon icon={icon} />
</button>
<div className="component-uid">
<span>{friendlyName}</span>
</div>
{children}
</Wrapper>
);
};
DynamicComponentCard.defaultProps = {
children: null,
friendlyName: '',
onClick: () => {},
icon: 'smile',
};
DynamicComponentCard.propTypes = {
children: PropTypes.node,
componentUid: PropTypes.string.isRequired,
friendlyName: PropTypes.string,
icon: PropTypes.string,
onClick: PropTypes.func,
};
export default DynamicComponentCard;

View File

@ -74,7 +74,7 @@ function ComponentCard({ componentUid, intlLabel, icon, onClick }) {
}
ComponentCard.defaultProps = {
icon: 'smile',
icon: 'dice-d6',
onClick: () => {},
};

View File

@ -43,7 +43,7 @@ const getSelectStyles = theme => {
};
},
indicatorContainer: base => ({ ...base, padding: 0, paddingRight: theme.spaces[3] }),
input: base => ({ ...base, margin: 0, padding: 0 }),
input: base => ({ ...base, margin: 0, padding: 0, color: theme.colors.neutral800 }),
menu: base => {
return {
...base,

View File

@ -1,12 +1,12 @@
import React from 'react';
import styled from 'styled-components';
import { useIntl } from 'react-intl';
import { ContentBox, useTracking } from '@strapi/helper-plugin';
import { Stack } from '@strapi/design-system/Stack';
import InformationSquare from '@strapi/icons/InformationSquare';
import CodeSquare from '@strapi/icons/CodeSquare';
import PlaySquare from '@strapi/icons/PlaySquare';
import FeatherSquare from '@strapi/icons/FeatherSquare';
import { ContentBox } from '@strapi/helper-plugin';
const BlockLink = styled.a`
text-decoration: none;
@ -14,6 +14,11 @@ const BlockLink = styled.a`
const ContentBlocks = () => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const handleClick = eventName => {
trackUsage(eventName);
};
return (
<Stack spacing={5}>
@ -21,6 +26,7 @@ const ContentBlocks = () => {
href="https://strapi.io/resource-center"
target="_blank"
rel="noopener noreferrer nofollow"
onClick={() => handleClick('didClickonReadTheDocumentationSection')}
>
<ContentBox
title={formatMessage({
@ -39,6 +45,7 @@ const ContentBlocks = () => {
href="https://strapi.io/starters"
target="_blank"
rel="noopener noreferrer nofollow"
onClick={() => handleClick('didClickonCodeExampleSection')}
>
<ContentBox
title={formatMessage({
@ -57,6 +64,7 @@ const ContentBlocks = () => {
href="https://strapi.io/blog/categories/tutorials"
target="_blank"
rel="noopener noreferrer nofollow"
onClick={() => handleClick('didClickonTutorialSection')}
>
<ContentBox
title={formatMessage({
@ -71,7 +79,12 @@ const ContentBlocks = () => {
iconBackground="secondary100"
/>
</BlockLink>
<BlockLink href="https://strapi.io/blog" target="_blank" rel="noopener noreferrer nofollow">
<BlockLink
href="https://strapi.io/blog"
target="_blank"
rel="noopener noreferrer nofollow"
onClick={() => handleClick('didClickonBlogSection')}
>
<ContentBox
title={formatMessage({
id: 'app.components.BlockLink.blog',

View File

@ -29,6 +29,10 @@ module.exports = {
const createdUser = await getService('user').create(attributes);
const userInfo = getService('user').sanitizeUser(createdUser);
// Note: We need to assign manually the registrationToken to the
// final user payload so that it's not removed in the sanitation process.
Object.assign(userInfo, { registrationToken: createdUser.registrationToken });
ctx.created({ data: userInfo });
},
};

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/admin",
"version": "4.1.9",
"version": "4.1.11",
"description": "Strapi Admin",
"repository": {
"type": "git",
@ -50,12 +50,12 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@strapi/babel-plugin-switch-ee-ce": "4.1.9",
"@strapi/babel-plugin-switch-ee-ce": "4.1.11",
"@strapi/design-system": "1.1.0",
"@strapi/helper-plugin": "4.1.9",
"@strapi/helper-plugin": "4.1.11",
"@strapi/icons": "1.1.0",
"@strapi/typescript-utils": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/typescript-utils": "4.1.11",
"@strapi/utils": "4.1.11",
"axios": "0.24.0",
"babel-loader": "8.2.3",
"babel-plugin-styled-components": "2.0.2",

View File

@ -36,6 +36,10 @@ module.exports = {
const userInfo = getService('user').sanitizeUser(createdUser);
// Note: We need to assign manually the registrationToken to the
// final user payload so that it's not removed in the sanitation process.
Object.assign(userInfo, { registrationToken: createdUser.registrationToken });
// Send 201 created
ctx.created({ data: userInfo });
},

View File

@ -105,13 +105,22 @@ describe('Auth', () => {
});
describe('validatePassword', () => {
test('Compares password with hash', async () => {
test('Compares password with hash (matching passwords)', async () => {
const password = 'pcw123';
const hash = await hashPassword(password);
const isValid = await validatePassword(password, hash);
expect(isValid).toBe(true);
});
test('Compares password with hash (not matching passwords)', async () => {
const password = 'pcw123';
const password2 = 'pcs1234';
const hash = await hashPassword(password2);
const isValid = await validatePassword(password, hash);
expect(isValid).toBe(false);
});
});
describe('forgotPassword', () => {

View File

@ -1,13 +1,14 @@
'use strict';
const _ = require('lodash');
const { omit } = require('lodash/fp');
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
const { createAuthRequest } = require('../../../../../test/helpers/request');
const { createUtils } = require('../../../../../test/helpers/utils');
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
const omitTimestamps = obj => _.omit(obj, ['updatedAt', 'createdAt']);
const omitTimestamps = omit(['updatedAt', 'createdAt']);
const omitRegistrationToken = omit(['registrationToken']);
/**
* == Test Suite Overview ==
@ -128,9 +129,10 @@ describe('Admin User CRUD (e2e)', () => {
expect(res.statusCode).toBe(201);
expect(res.body.data).not.toBeNull();
expect(res.body.data).toHaveProperty('registrationToken');
// Using the created user as an example for the rest of the tests
testData.user = res.body.data;
testData.user = omitRegistrationToken(res.body.data);
});
test('3. Creates users with superAdmin role (success)', async () => {
@ -153,7 +155,7 @@ describe('Admin User CRUD (e2e)', () => {
expect(res.statusCode).toBe(201);
expect(res.body.data).not.toBeNull();
testData.otherSuperAdminUsers.push(res.body.data);
testData.otherSuperAdminUsers.push(omitRegistrationToken(res.body.data));
}
});

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-content-manager",
"version": "4.1.9",
"version": "4.1.11",
"description": "A powerful UI to easily manage your data.",
"repository": {
"type": "git",
@ -24,7 +24,7 @@
},
"dependencies": {
"@sindresorhus/slugify": "1.1.0",
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"lodash": "4.17.21"
},
"engines": {

View File

@ -106,7 +106,7 @@ function ComponentCard({ component, dzName, index, isActive, isInDevelopmentMode
paddingRight={4}
>
<StackCentered spacing={1}>
<StyledFontAwesomeIcon icon={icon} />
<StyledFontAwesomeIcon icon={icon || 'dice-d6'} />
<Box maxWidth={`calc(${pxToRem(140)} - 32px)`}>
<Typography variant="pi" fontWeight="bold" ellipsis>
{displayName}

View File

@ -16,23 +16,14 @@ import Cell from './Cell';
const CELL_WIDTH = 44;
const ComponentIconPicker = ({ error, isCreating, intlLabel, name, onChange, value }) => {
const { allIcons, allComponentsIconAlreadyTaken } = useDataManager();
const ComponentIconPicker = ({ error, intlLabel, name, onChange, value }) => {
const { allIcons } = useDataManager();
const { formatMessage } = useIntl();
const [originalIcon] = useState(value);
const initialIcons = allIcons.filter(ico => {
if (isCreating) {
return !allComponentsIconAlreadyTaken.includes(ico);
}
// Edition
return !allComponentsIconAlreadyTaken.filter(icon => icon !== originalIcon).includes(ico);
});
const searchWrapperRef = useRef();
const [showSearch, setShowSearch] = useState(false);
const [search, setSearch] = useState('');
const [icons, setIcons] = useState(initialIcons);
const [icons, setIcons] = useState(allIcons);
const toggleSearch = () => setShowSearch(prev => !prev);
useEffect(() => {
@ -43,7 +34,7 @@ const ComponentIconPicker = ({ error, isCreating, intlLabel, name, onChange, val
const handleChangeSearch = ({ target: { value } }) => {
setSearch(value);
setIcons(() => initialIcons.filter(icon => icon.includes(value)));
setIcons(() => allIcons.filter(icon => icon.includes(value)));
};
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
@ -91,7 +82,7 @@ const ComponentIconPicker = ({ error, isCreating, intlLabel, name, onChange, val
}}
onClear={() => {
setSearch('');
setIcons(initialIcons);
setIcons(allIcons);
toggleSearch();
}}
value={search}
@ -175,7 +166,6 @@ ComponentIconPicker.defaultProps = {
ComponentIconPicker.propTypes = {
error: PropTypes.string,
isCreating: PropTypes.bool.isRequired,
intlLabel: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,

View File

@ -554,10 +554,6 @@ const DataManagerProvider = ({
addAttribute,
addCreatedComponentToDynamicZone,
allComponentsCategories: retrieveSpecificInfoFromComponents(components, ['category']),
allComponentsIconAlreadyTaken: retrieveSpecificInfoFromComponents(components, [
'schema',
'icon',
]),
allIcons,
changeDynamicZoneComponents,
components,

View File

@ -251,7 +251,7 @@ const advancedForm = {
id: 'global.settings',
defaultMessage: 'Settings',
},
items: [options.private, options.required],
items: [options.required, options.private],
},
],
};

View File

@ -39,7 +39,7 @@ const createComponentSchema = (usedComponentNames, reservedNames, category) => {
.matches(CATEGORY_NAME_REGEX, errorsTrads.regex)
.required(errorsTrads.required),
icon: yup.string().required(errorsTrads.required),
icon: yup.string(),
};
return yup.object(shape);

View File

@ -67,11 +67,10 @@ const formsAPI = {
],
},
};
formType[field].validators.push(validator);
formType[field].form.advanced.push(advanced);
formType[field].form.base.push(base);
}
formType[field].validators.push(validator);
formType[field].form.advanced.push(advanced);
formType[field].form.base.push(base);
});
},
getAdvancedForm(target, props = null) {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-content-type-builder",
"version": "4.1.9",
"version": "4.1.11",
"description": "Strapi plugin to create content type",
"repository": {
"type": "git",
@ -28,9 +28,9 @@
},
"dependencies": {
"@sindresorhus/slugify": "1.1.0",
"@strapi/generators": "4.1.9",
"@strapi/helper-plugin": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/generators": "4.1.11",
"@strapi/helper-plugin": "4.1.11",
"@strapi/utils": "4.1.11",
"fs-extra": "10.0.0",
"lodash": "4.17.21",
"pluralize": "^8.0.0",

View File

@ -22,8 +22,7 @@ const componentSchema = createSchema(VALID_TYPES, VALID_RELATIONS, {
icon: yup
.string()
.nullable()
.test(isValidIcon)
.required('icon.required'),
.test(isValidIcon),
category: yup
.string()
.nullable()

View File

@ -47,11 +47,6 @@ describe('Content Type Builder - Components', () => {
name: 'ValidationError',
path: ['component', 'displayName'],
},
{
message: 'icon.required',
name: 'ValidationError',
path: ['component', 'icon'],
},
{
message: 'category.required',
name: 'ValidationError',
@ -59,7 +54,7 @@ describe('Content Type Builder - Components', () => {
},
],
},
message: '4 errors occurred',
message: '3 errors occurred',
name: 'ValidationError',
},
});
@ -242,11 +237,6 @@ describe('Content Type Builder - Components', () => {
name: 'ValidationError',
path: ['component', 'displayName'],
},
{
message: 'icon.required',
name: 'ValidationError',
path: ['component', 'icon'],
},
{
message: 'category.required',
name: 'ValidationError',
@ -254,7 +244,7 @@ describe('Content Type Builder - Components', () => {
},
],
},
message: '3 errors occurred',
message: '2 errors occurred',
name: 'ValidationError',
},
});

View File

@ -0,0 +1,55 @@
'use strict';
const { createLifecyclesProvider } = require('../lifecycles');
describe('LifecycleProvider', () => {
describe('run', () => {
/** @type {import("../lifecycles").LifecycleProvider} */
let provider;
let dbMetadataGetStub = jest.fn(uid => ({ uid, name: 'TestModel' }));
beforeEach(() => {
const db = {
metadata: {
get: dbMetadataGetStub,
},
};
provider = createLifecyclesProvider(db);
provider.clear();
});
it('store state', async () => {
const expectedState = new Date().toISOString();
const subscriber = {
async beforeEvent(event) {
event.state = expectedState;
},
};
provider.subscribe(subscriber);
const stateBefore = await provider.run('beforeEvent', 'test-model', { id: 'instance-id' });
expect(stateBefore.get(subscriber)).toEqual(expectedState);
});
it('use shared state', async () => {
const expectedState = { value: new Date().toISOString() };
let receivedState;
provider.subscribe({
async beforeEvent(event) {
event.state.value = expectedState.value;
},
async afterEvent(event) {
receivedState = event.state;
},
});
const stateBefore = await provider.run('beforeEvent', 'test-model', { id: 'instance-id' });
await provider.run('afterEvent', 'test-model', { id: 'instance-id' }, stateBefore);
expect(receivedState).toEqual(expectedState);
});
});
});

View File

@ -113,33 +113,33 @@ const createEntityManager = db => {
return {
async findOne(uid, params) {
await db.lifecycles.run('beforeFindOne', uid, { params });
const states = await db.lifecycles.run('beforeFindOne', uid, { params });
const result = await this.createQueryBuilder(uid)
.init(params)
.first()
.execute();
await db.lifecycles.run('afterFindOne', uid, { params, result });
await db.lifecycles.run('afterFindOne', uid, { params, result }, states);
return result;
},
// should we name it findOne because people are used to it ?
async findMany(uid, params) {
await db.lifecycles.run('beforeFindMany', uid, { params });
const states = await db.lifecycles.run('beforeFindMany', uid, { params });
const result = await this.createQueryBuilder(uid)
.init(params)
.execute();
await db.lifecycles.run('afterFindMany', uid, { params, result });
await db.lifecycles.run('afterFindMany', uid, { params, result }, states);
return result;
},
async count(uid, params = {}) {
await db.lifecycles.run('beforeCount', uid, { params });
async count(uid, params) {
const states = await db.lifecycles.run('beforeCount', uid, { params });
const res = await this.createQueryBuilder(uid)
.init(_.pick(['_q', 'where', 'filters'], params))
@ -149,13 +149,13 @@ const createEntityManager = db => {
const result = Number(res.count);
await db.lifecycles.run('afterCount', uid, { params, result });
await db.lifecycles.run('afterCount', uid, { params, result }, states);
return result;
},
async create(uid, params = {}) {
await db.lifecycles.run('beforeCreate', uid, { params });
const states = await db.lifecycles.run('beforeCreate', uid, { params });
const metadata = db.metadata.get(uid);
const { data } = params;
@ -182,14 +182,14 @@ const createEntityManager = db => {
populate: params.populate,
});
await db.lifecycles.run('afterCreate', uid, { params, result });
await db.lifecycles.run('afterCreate', uid, { params, result }, states);
return result;
},
// TODO: where do we handle relation processing for many queries ?
async createMany(uid, params = {}) {
await db.lifecycles.run('beforeCreateMany', uid, { params });
const states = await db.lifecycles.run('beforeCreateMany', uid, { params });
const metadata = db.metadata.get(uid);
const { data } = params;
@ -210,13 +210,13 @@ const createEntityManager = db => {
const result = { count: data.length };
await db.lifecycles.run('afterCreateMany', uid, { params, result });
await db.lifecycles.run('afterCreateMany', uid, { params, result }, states);
return result;
},
async update(uid, params = {}) {
await db.lifecycles.run('beforeUpdate', uid, { params });
const states = await db.lifecycles.run('beforeUpdate', uid, { params });
const metadata = db.metadata.get(uid);
const { where, data } = params;
@ -259,14 +259,14 @@ const createEntityManager = db => {
populate: params.populate,
});
await db.lifecycles.run('afterUpdate', uid, { params, result });
await db.lifecycles.run('afterUpdate', uid, { params, result }, states);
return result;
},
// TODO: where do we handle relation processing for many queries ?
async updateMany(uid, params = {}) {
await db.lifecycles.run('beforeUpdateMany', uid, { params });
const states = await db.lifecycles.run('beforeUpdateMany', uid, { params });
const metadata = db.metadata.get(uid);
const { where, data } = params;
@ -284,13 +284,13 @@ const createEntityManager = db => {
const result = { count: updatedRows };
await db.lifecycles.run('afterUpdateMany', uid, { params, result });
await db.lifecycles.run('afterUpdateMany', uid, { params, result }, states);
return result;
},
async delete(uid, params = {}) {
await db.lifecycles.run('beforeDelete', uid, { params });
const states = await db.lifecycles.run('beforeDelete', uid, { params });
const { where, select, populate } = params;
@ -318,14 +318,14 @@ const createEntityManager = db => {
await this.deleteRelations(uid, id);
await db.lifecycles.run('afterDelete', uid, { params, result: entity });
await db.lifecycles.run('afterDelete', uid, { params, result: entity }, states);
return entity;
},
// TODO: where do we handle relation processing for many queries ?
async deleteMany(uid, params = {}) {
await db.lifecycles.run('beforeDeleteMany', uid, { params });
const states = await db.lifecycles.run('beforeDeleteMany', uid, { params });
const { where } = params;
@ -336,7 +336,7 @@ const createEntityManager = db => {
const result = { count: deletedRows };
await db.lifecycles.run('afterDelete', uid, { params, result });
await db.lifecycles.run('afterDeleteMany', uid, { params, result }, states);
return result;
},

View File

@ -43,7 +43,8 @@ export interface Event {
export interface LifecycleProvider {
subscribe(subscriber: Subscriber): () => void;
clear(): void;
run(action: Action, uid: string, properties: any): Promise<void>;
run(action: Action, uid: string, properties: any): Promise<Map<any, any>>;
run(action: Action, uid: string, properties: any, states: Map<any, any>): Promise<Map<any, any>>;
createEvent(action: Action, uid: string, properties: any): Event;
}

View File

@ -30,21 +30,39 @@ const createLifecyclesProvider = db => {
subscribers = [];
},
createEvent(action, uid, properties) {
/**
* @param {string} action
* @param {string} uid
* @param {{ params?: any, result?: any }} properties
* @param {Map<any, any>} state
*/
createEvent(action, uid, properties, state) {
const model = db.metadata.get(uid);
return {
action,
model,
state,
...properties,
};
},
async run(action, uid, properties) {
for (const subscriber of subscribers) {
/**
* @param {string} action
* @param {string} uid
* @param {{ params?: any, result?: any }} properties
* @param {Map<any, any>} states
*/
async run(action, uid, properties, states = new Map()) {
for (let i = 0; i < subscribers.length; i++) {
const subscriber = subscribers[i];
if (typeof subscriber === 'function') {
const event = this.createEvent(action, uid, properties);
const state = states.get(subscriber) || {};
const event = this.createEvent(action, uid, properties, state);
await subscriber(event);
if (event.state) {
states.set(subscriber, event.state || state);
}
continue;
}
@ -52,11 +70,17 @@ const createLifecyclesProvider = db => {
const hasModel = !subscriber.models || subscriber.models.includes(uid);
if (hasAction && hasModel) {
const event = this.createEvent(action, uid, properties);
const state = states.get(subscriber) || {};
const event = this.createEvent(action, uid, properties, state);
await subscriber[action](event);
if (event.state) {
states.set(subscriber, event.state);
}
}
}
return states;
},
};
};

View File

@ -2,24 +2,35 @@
const path = require('path');
const fse = require('fs-extra');
const Umzug = require('umzug');
const { Umzug } = require('umzug');
const createStorage = require('./storage');
const wrapTransaction = db => fn => () =>
db.getConnection().transaction(trx => Promise.resolve(fn(trx)));
// TODO: check multiple commands in one sql statement
const migrationResolver = path => {
const migrationResolver = ({ name, path, context }) => {
const { db } = context;
// if sql file run with knex raw
if (path.match(/\.sql$/)) {
const sql = fse.readFileSync(path, 'utf8');
return {
up: knex => knex.raw(sql),
name,
up: wrapTransaction(db)(knex => knex.raw(sql)),
down() {},
};
}
// NOTE: we can add some ts register if we want to handle ts migration files at some point
return require(path);
const migration = require(path);
return {
name,
up: wrapTransaction(db)(migration.up),
down: wrapTransaction(db)(migration.down),
};
};
const createUmzugProvider = db => {
@ -27,17 +38,12 @@ const createUmzugProvider = db => {
fse.ensureDirSync(migrationDir);
const wrapFn = fn => db => db.getConnection().transaction(trx => Promise.resolve(fn(trx)));
const storage = createStorage({ db, tableName: 'strapi_migrations' });
return new Umzug({
storage,
storage: createStorage({ db, tableName: 'strapi_migrations' }),
context: { db },
migrations: {
path: migrationDir,
pattern: /\.(js|sql)$/,
params: [db],
wrap: wrapFn,
customResolver: migrationResolver,
glob: ['*.{js,sql}', { cwd: migrationDir }],
resolve: migrationResolver,
},
});
};

View File

@ -14,21 +14,21 @@ const createStorage = (opts = {}) => {
};
return {
async logMigration(migrationName) {
async logMigration({ name }) {
await db
.getConnection()
.insert({
name: migrationName,
name,
time: new Date(),
})
.into(tableName);
},
async unlogMigration(migrationName) {
async unlogMigration({ name }) {
await db
.getConnection(tableName)
.del()
.where({ name: migrationName });
.where({ name });
},
async executed() {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/database",
"version": "4.1.9",
"version": "4.1.11",
"description": "Strapi's database layer",
"homepage": "https://strapi.io",
"bugs": {
@ -36,7 +36,7 @@
"fs-extra": "10.0.0",
"knex": "1.0.4",
"lodash": "4.17.21",
"umzug": "2.3.0"
"umzug": "3.1.1"
},
"engines": {
"node": ">=12.22.0 <=16.x.x",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-email",
"version": "4.1.9",
"version": "4.1.11",
"description": "Easily configure your Strapi application to send emails.",
"repository": {
"type": "git",
@ -26,12 +26,12 @@
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
},
"dependencies": {
"@strapi/provider-email-sendmail": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/provider-email-sendmail": "4.1.11",
"@strapi/utils": "4.1.11",
"lodash": "4.17.21"
},
"devDependencies": {
"@strapi/helper-plugin": "4.1.9"
"@strapi/helper-plugin": "4.1.11"
},
"engines": {
"node": ">=12.22.0 <=16.x.x",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/helper-plugin",
"version": "4.1.9",
"version": "4.1.11",
"description": "Helper for Strapi plugins development",
"repository": {
"type": "git",

View File

@ -7,31 +7,35 @@ const fse = require('fs-extra');
const { isKebabCase } = require('@strapi/utils');
const { importDefault } = require('../../utils');
// to handle names with numbers in it we first check if it is already in kebabCase
const normalizeName = name => (isKebabCase(name) ? name : _.kebabCase(name));
const DEFAULT_CONTENT_TYPE = {
schema: {},
actions: {},
lifecycles: {},
};
// to handle names with numbers in it we first check if it is already in kebabCase
const normalizeName = name => (isKebabCase(name) ? name : _.kebabCase(name));
const isDirectory = fd => fd.isDirectory();
const isDotFile = fd => fd.name.startsWith('.');
module.exports = async strapi => {
if (!existsSync(strapi.dirs.dist.api)) {
return;
}
const apisFDs = await fse.readdir(strapi.dirs.dist.api, { withFileTypes: true });
const apisFDs = await (await fse.readdir(strapi.dirs.dist.api, { withFileTypes: true }))
.filter(isDirectory)
.filter(_.negate(isDotFile));
const apis = {};
// only load folders
for (const apiFD of apisFDs) {
if (apiFD.isDirectory()) {
const apiName = normalizeName(apiFD.name);
const api = await loadAPI(join(strapi.dirs.dist.api, apiFD.name));
const apiName = normalizeName(apiFD.name);
const api = await loadAPI(join(strapi.dirs.dist.api, apiFD.name));
apis[apiName] = api;
}
apis[apiName] = api;
}
validateContentTypesUnicity(apis);

View File

@ -200,4 +200,19 @@ describe('BigInteger validator', () => {
});
});
});
describe('min', () => {
test('it does not validate the min constraint if the attribute min is not a number', async () => {
const validator = strapiUtils.validateYupSchema(
validators.biginteger(
{
attr: { type: 'biginteger', minLength: '123' },
},
{ isDraft: false }
)
);
expect(await validator(1)).toBe(1);
});
});
});

View File

@ -12,7 +12,7 @@ describe('Email validator', () => {
const validator = strapiUtils.validateYupSchema(
validators.email(
{
attr: { type: 'string' },
attr: { type: 'email' },
},
{ isDraft: false }
)
@ -29,7 +29,7 @@ describe('Email validator', () => {
const validator = strapiUtils.validateYupSchema(
validators.email(
{
attr: { type: 'string' },
attr: { type: 'email' },
},
{ isDraft: false }
)
@ -37,5 +37,18 @@ describe('Email validator', () => {
expect(await validator('valid@email.com')).toBe('valid@email.com');
});
test('it validates non-empty email required field', async () => {
const validator = strapiUtils.validateYupSchema(
validators.email({ attr: { type: 'email' } }, { isDraft: false })
);
try {
await validator('');
} catch (err) {
expect(err).toBeInstanceOf(YupValidationError);
expect(err.message).toBe('this cannot be empty');
}
});
});
});

View File

@ -166,7 +166,9 @@ const stringValidator = composeValidators(
addUniqueValidator
);
const emailValidator = composeValidators(stringValidator, validator => validator.email());
const emailValidator = composeValidators(stringValidator, validator =>
validator.email().min(1, '${path} cannot be empty')
);
const uidValidator = composeValidators(stringValidator, validator =>
validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$'))

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/strapi",
"version": "4.1.9",
"version": "4.1.11",
"description": "An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite",
"keywords": [
"strapi",
@ -80,17 +80,17 @@
"dependencies": {
"@koa/cors": "3.1.0",
"@koa/router": "10.1.1",
"@strapi/admin": "4.1.9",
"@strapi/database": "4.1.9",
"@strapi/generate-new": "4.1.9",
"@strapi/generators": "4.1.9",
"@strapi/logger": "4.1.9",
"@strapi/plugin-content-manager": "4.1.9",
"@strapi/plugin-content-type-builder": "4.1.9",
"@strapi/plugin-email": "4.1.9",
"@strapi/plugin-upload": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/typescript-utils": "4.1.9",
"@strapi/admin": "4.1.11",
"@strapi/database": "4.1.11",
"@strapi/generate-new": "4.1.11",
"@strapi/generators": "4.1.11",
"@strapi/logger": "4.1.11",
"@strapi/plugin-content-manager": "4.1.11",
"@strapi/plugin-content-type-builder": "4.1.11",
"@strapi/plugin-email": "4.1.11",
"@strapi/plugin-upload": "4.1.11",
"@strapi/utils": "4.1.11",
"@strapi/typescript-utils": "4.1.11",
"bcryptjs": "2.4.3",
"boxen": "5.1.2",
"chalk": "4.1.2",

View File

@ -0,0 +1,104 @@
{
"bulk.select.label": "Seleccioneu tots els fitxers",
"button.next": "Següent",
"checkControl.crop-duplicate": "Duplica i retalla l'imatge",
"checkControl.crop-original": "Retalla l'imatge original",
"control-card.add": "Afegeix",
"control-card.cancel": "Cancel·lar",
"control-card.copy-link": "Copia l'enllaç",
"control-card.crop": "Retalla",
"control-card.download": "Descarregar",
"control-card.edit": "Edita",
"control-card.replace-media": "Substitueix els fitxers",
"control-card.save": "Desa",
"control-card.stop-crop": "Deixa de retallar",
"filter.add": "Afegeix un filtre",
"form.button.replace-media": "Substituïu els fitxers",
"form.input.decription.file-alt": "Aquest text es mostrarà si el recurs no es pot mostrar.",
"form.input.label.file-alt": "Text alternatiu",
"form.input.label.file-caption": "Subtítol",
"form.input.label.file-name": "Nom de l'arxiu",
"form.upload-url.error.url.invalid": "Un URL no és vàlid",
"form.upload-url.error.url.invalids": "{number} URL no són vàlids",
"header.actions.add-assets": "Afegeix nous fitxers",
"header.actions.upload-assets": "Carregueu fitxers",
"header.actions.upload-new-asset": "Penja un fitxer nou",
"header.content.assets-empty": "Sense fitxers",
"header.content.assets-multiple": "{nombre} fitxers",
"header.content.assets-single": "1 fitxer",
"input.button.label": "Exploreu fitxers",
"input.label": "Arrossega i deixa anar aquí o",
"input.label-bold": "Arrossegar i deixar anar",
"input.label-normal": "per carregar o",
"input.placeholder": "Feu clic per afegir un fitxer o arrossegueu i deixeu anar un fitxer en aquesta àrea",
"input.placeholder.icon": "Deixeu el fitxer en aquesta zona",
"input.url.description": "Separeu els vostres enllaços URL mitjançant un retorn de carro.",
"input.url.label": "URL",
"list.asset.at.finished": "Els fitxers s'han acabat de carregar.",
"list.asset.load": "S'està carregant la llista de fitxers.",
"list.assets-empty.search": "No s'ha trobat cap resultat",
"list.assets-empty.subtitle": "Afegiu-ne un a la llista.",
"list.assets-empty.title": "Encara no hi ha fitxers",
"list.assets-empty.title-withSearch": "No hi ha fitxers amb els filtres aplicats",
"list.assets.empty": "Penja els teus primers fitxers...",
"list.assets.empty.no-permissions": "La llista de fitxers està buida.",
"list.assets.loading-asset": "S'està carregant la previsualització dels mitjans: {path}",
"list.assets.not-supported-content": "No hi ha vista prèvia disponible",
"list.assets.preview-asset": "Vista prèvia del vídeo al camí {path}",
"list.assets.selected": "{number, plural, =0 {Cap fitxer} un {1 fitxer} altres {# fitxers}} a punt per penjar",
"list.assets.type-not-allowed": "Aquest tipus de fitxer no està permès.",
"mediaLibraryInput.actions.nextSlide": "Següent diapositiva",
"mediaLibraryInput.actions.previousSlide": "Diapositiva anterior",
"mediaLibraryInput.placeholder": "Feu clic per afegir un fitxer o arrossegueu-ne un en aquesta àrea",
"mediaLibraryInput.slideCount": "{n} de {m} diapositives",
"modal.file-details.date": "Data",
"modal.file-details.dimensions": "Dimensions",
"modal.file-details.extension": "Extensió",
"modal.file-details.size": "Mida",
"modal.header.browse": "Carregueu fitxers",
"modal.header.file-detail": "Detalls",
"modal.header.pending-assets": "Fitxers pendents",
"modal.header.select-files": "Fitxers seleccionats",
"modal.nav.browse": "navegar",
"modal.nav.computer": "Des de l'ordinador",
"modal.nav.selected": "seleccionat",
"modal.nav.url": "Des de l'URL",
"modal.remove.success-label": "El fitxer s'ha eliminat correctament.",
"modal.selected-list.sub-header-subtitle": "Arrossegueu i deixeu anar per reordenar els fitxers al camp",
"modal.upload-list.footer.button": "Carregueu {nombre, plural, un {# fitxer} altres {# fitxer}} a la biblioteca",
"modal.upload-list.sub-header-subtitle": "Gestioneu els fitxers abans d'afegir-los a la Mediateca",
"modal.upload-list.sub-header.button": "Afegeix més actius",
"modal.upload.cancelled": "La càrrega s'ha avortat manualment.",
"page.title": "Configuració - Mediateca",
"permissions.not-allowed.update": "No teniu permís per editar aquest fitxer.",
"plugin.description.long": "Gestió de fitxers multimèdia.",
"plugin.description.short": "Gestió de fitxers multimèdia.",
"plugin.name": "Mediateca",
"search.clear.label": "Esborra la cerca",
"search.label": "Cerca un actiu",
"search.placeholder": "ex.: el primer gos a la lluna",
"settings.blockTitle": "Gestió de fitxers",
"settings.form.autoOrientation.description": "Si activeu aquesta opció, la imatge girarà automàticament segons l'etiqueta d'orientació EXIF.",
"settings.form.autoOrientation.label": "Orientació automàtica",
"settings.form.responsiveDimensions.description": "Si activeu aquesta opció, es generaran diversos formats (petit, mitjà i gran) del recurs penjat.",
"settings.form.responsiveDimensions.label": "Càrrega amigable responsiva",
"settings.form.sizeOptimization.description": "Activar aquesta opció reduirà la mida de la imatge i la qualitat lleugerament.",
"settings.form.sizeOptimization.label": "Optimització de la mida",
"settings.form.videoPreview.description": "Generarà una previsualització de sis segons del vídeo (GIF)",
"settings.form.videoPreview.label": "Vista prèvia",
"settings.header.label": "Mediateca",
"settings.section.doc.label": "Doc",
"settings.section.image.label": "Imatge",
"settings.section.video.label": "Vídeo",
"settings.sub-header.label": "Configureu la configuració de la mediateca",
"sort.created_at_asc": "Càrregues més antigues",
"sort.created_at_desc": "Càrregues més recents",
"sort.label": "Ordenar per",
"sort.name_asc": "Ordre alfabètic (A a Z)",
"sort.name_desc": "Ordre alfabètic invers (de la Z a la A)",
"sort.updated_at_asc": "Actualitzacions més antigues",
"sort.updated_at_desc": "Actualitzacions més recents",
"tabs.title": "Com vols pujar els teus actius?",
"window.confirm.close-modal.file": "Estàs segur? Els vostres canvis es perdran.",
"window.confirm.close-modal.files": "Estàs segur? Teniu alguns fitxers que encara no s'han penjat."
}

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-upload",
"version": "4.1.9",
"version": "4.1.11",
"description": "Makes it easy to upload images and files to your Strapi Application.",
"license": "SEE LICENSE IN LICENSE",
"author": {
@ -23,9 +23,9 @@
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
},
"dependencies": {
"@strapi/helper-plugin": "4.1.9",
"@strapi/provider-upload-local": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/helper-plugin": "4.1.11",
"@strapi/provider-upload-local": "4.1.11",
"@strapi/utils": "4.1.11",
"byte-size": "7.0.1",
"cropperjs": "1.5.11",
"fs-extra": "10.0.0",

View File

@ -0,0 +1,73 @@
'use strict';
const { join } = require('path');
const bootstrap = require('../register');
const exampleMiddlewaresConfig = [
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': ["'self'", 'data:', 'blob:', 'https://exampledomain.global.strapi.io'],
'media-src': ["'self'", 'data:', 'blob:', 'https://exampledomain.global.strapi.io'],
upgradeInsecureRequests: null,
},
},
},
},
];
jest.mock('@strapi/provider-upload-local', () => ({
init() {
global.strapi.config.set('middlewares', exampleMiddlewaresConfig);
return {
uploadStream: jest.fn(),
upload: jest.fn(),
delete: jest.fn(),
};
},
}));
describe('Upload plugin register function', () => {
test('The upload plugin registers the /upload route', async () => {
const registerRoute = jest.fn();
global.strapi = {
dirs: { root: process.cwd(), public: join(process.cwd(), 'public') },
plugins: { upload: {} },
server: { app: { on: jest.fn() }, routes: registerRoute },
admin: { services: { permission: { actionProvider: { registerMany: jest.fn() } } } },
config: {
get: jest.fn().mockReturnValueOnce({ provider: 'local' }),
set: jest.fn(),
},
};
await bootstrap({ strapi });
expect(registerRoute).toHaveBeenCalledTimes(1);
});
test('Strapi config can programatically be extended by providers', async () => {
const setConfig = jest.fn();
global.strapi = {
dirs: { root: process.cwd(), public: join(process.cwd(), 'public') },
plugins: { upload: {} },
server: { app: { on: jest.fn() }, routes: jest.fn() },
admin: { services: { permission: { actionProvider: { registerMany: jest.fn() } } } },
config: {
get: jest.fn().mockReturnValueOnce({ provider: 'local' }),
set: setConfig,
},
};
await bootstrap({ strapi });
expect(setConfig).toHaveBeenCalledWith('middlewares', exampleMiddlewaresConfig);
});
});

View File

@ -1,13 +1,9 @@
'use strict';
const _ = require('lodash');
module.exports = async ({ strapi }) => {
// set plugin store
const configurator = strapi.store({ type: 'plugin', name: 'upload', key: 'settings' });
strapi.plugin('upload').provider = createProvider(strapi.config.get('plugin.upload', {}));
// if provider config does not exist set one by default
const config = await configurator.get();
@ -24,64 +20,6 @@ module.exports = async ({ strapi }) => {
await registerPermissionActions();
};
const createProvider = config => {
const { providerOptions, actionOptions = {} } = config;
const providerName = _.toLower(config.provider);
let provider;
let modulePath;
try {
modulePath = require.resolve(`@strapi/provider-upload-${providerName}`);
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
modulePath = providerName;
} else {
throw error;
}
}
try {
provider = require(modulePath);
} catch (err) {
const newError = new Error(`Could not load upload provider "${providerName}".`);
newError.stack = err.stack;
throw newError;
}
const providerInstance = provider.init(providerOptions);
if (!providerInstance.delete) {
throw new Error(`The upload provider "${providerName}" doesn't implement the delete method.`);
}
if (!providerInstance.upload && !providerInstance.uploadStream) {
throw new Error(
`The upload provider "${providerName}" doesn't implement the uploadStream nor the upload method.`
);
}
if (!providerInstance.uploadStream) {
process.emitWarning(
`The upload provider "${providerName}" doesn't implement the uploadStream function. Strapi will fallback on the upload method. Some performance issues may occur.`
);
}
const wrappedProvider = _.mapValues(providerInstance, (method, methodName) => {
return async function(file, options = actionOptions[methodName]) {
return providerInstance[methodName](file, options);
};
});
return Object.assign(Object.create(baseProvider), wrappedProvider);
};
const baseProvider = {
extend(obj) {
Object.assign(this, obj);
},
};
const registerPermissionActions = async () => {
const actions = [
{

View File

@ -1,5 +1,6 @@
'use strict';
const _ = require('lodash');
const registerUploadMiddleware = require('./middlewares/upload');
/**
@ -7,9 +8,69 @@ const registerUploadMiddleware = require('./middlewares/upload');
* @param {{ strapi: import('@strapi/strapi').Strapi }}
*/
module.exports = async ({ strapi }) => {
strapi.plugin('upload').provider = createProvider(strapi.config.get('plugin.upload', {}));
await registerUploadMiddleware({ strapi });
if (strapi.plugin('graphql')) {
require('./graphql')({ strapi });
}
};
const createProvider = config => {
const { providerOptions, actionOptions = {} } = config;
const providerName = _.toLower(config.provider);
let provider;
let modulePath;
try {
modulePath = require.resolve(`@strapi/provider-upload-${providerName}`);
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
modulePath = providerName;
} else {
throw error;
}
}
try {
provider = require(modulePath);
} catch (err) {
const newError = new Error(`Could not load upload provider "${providerName}".`);
newError.stack = err.stack;
throw newError;
}
const providerInstance = provider.init(providerOptions);
if (!providerInstance.delete) {
throw new Error(`The upload provider "${providerName}" doesn't implement the delete method.`);
}
if (!providerInstance.upload && !providerInstance.uploadStream) {
throw new Error(
`The upload provider "${providerName}" doesn't implement the uploadStream nor the upload method.`
);
}
if (!providerInstance.uploadStream) {
process.emitWarning(
`The upload provider "${providerName}" doesn't implement the uploadStream function. Strapi will fallback on the upload method. Some performance issues may occur.`
);
}
const wrappedProvider = _.mapValues(providerInstance, (method, methodName) => {
return async function(file, options = actionOptions[methodName]) {
return providerInstance[methodName](file, options);
};
});
return Object.assign(Object.create(baseProvider), wrappedProvider);
};
const baseProvider = {
extend(obj) {
Object.assign(this, obj);
},
};

View File

@ -60,10 +60,18 @@ describe('parseType', () => {
});
it('Throws on invalid formator dates', () => {
expect(() => parseType({ type: 'date', value: '-1029-11-02' })).toThrow();
expect(() => parseType({ type: 'date', value: '2019-13-02' })).toThrow();
expect(() => parseType({ type: 'date', value: '2019-12-32' })).toThrow();
expect(() => parseType({ type: 'date', value: '2019-02-31' })).toThrow();
expect(() => parseType({ type: 'date', value: '-1029-11-02' })).toThrow(
'Invalid format, expected an ISO compatible date'
);
expect(() => parseType({ type: 'date', value: '2019-13-02' })).toThrow(
'Invalid format, expected an ISO compatible date'
);
expect(() => parseType({ type: 'date', value: '2019-12-32' })).toThrow(
'Invalid format, expected an ISO compatible date'
);
expect(() => parseType({ type: 'date', value: '2019-02-31' })).toThrow(
'Invalid format, expected an ISO compatible date'
);
});
});

View File

@ -22,6 +22,7 @@ const {
toRegressedEnumValue,
startsWithANumber,
joinBy,
toKebabCase,
} = require('./string-formatting');
const { removeUndefined } = require('./object-formatting');
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
@ -65,6 +66,7 @@ module.exports = {
stringEquals,
isKebabCase,
isCamelCase,
toKebabCase,
contentTypes,
webhook,
env,

View File

@ -2,6 +2,7 @@
const _ = require('lodash');
const { trimChars, trimCharsEnd, trimCharsStart } = require('lodash/fp');
const slugify = require('@sindresorhus/slugify');
const { kebabCase } = require('lodash');
const nameToSlug = (name, options = { separator: '-' }) => slugify(name, options);
@ -58,6 +59,8 @@ const joinBy = (joint, ...args) => {
}, '');
};
const toKebabCase = value => kebabCase(value);
module.exports = {
nameToSlug,
nameToCollectionName,
@ -68,6 +71,7 @@ module.exports = {
stringEquals,
isCamelCase,
isKebabCase,
toKebabCase,
toRegressedEnumValue,
startsWithANumber,
joinBy,

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/utils",
"version": "4.1.9",
"version": "4.1.11",
"description": "Shared utilities for the Strapi packages",
"keywords": [
"strapi",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/generate-new",
"version": "4.1.9",
"version": "4.1.11",
"description": "Generate a new Strapi application.",
"keywords": [
"generate",

View File

@ -3,6 +3,7 @@
const tsUtils = require('@strapi/typescript-utils');
const getDestinationPrompts = require('./prompts/get-destination-prompts');
const validateInput = require('./utils/validate-input');
const getFilePath = require('./utils/get-file-path');
module.exports = plop => {
@ -14,6 +15,7 @@ module.exports = plop => {
type: 'input',
name: 'name',
message: 'Middleware name',
validate: input => validateInput(input),
},
...getDestinationPrompts('middleware', plop.getDestBasePath(), { rootFolder: true }),
],

View File

@ -2,6 +2,9 @@
const chalk = require('chalk');
const { isUsingTypeScriptSync } = require('@strapi/typescript-utils');
const { isKebabCase, toKebabCase } = require('@strapi/utils');
const validateInput = require('./utils/validate-input');
const logInstructions = (pluginName, { language }) => {
const maxLength = ` resolve: './src/plugins/${pluginName}'`.length;
@ -37,6 +40,7 @@ module.exports = plop => {
type: 'input',
name: 'pluginName',
message: 'Plugin name',
validate: input => validateInput(input),
},
{
type: 'list',
@ -51,7 +55,14 @@ module.exports = plop => {
const language = isTypescript ? 'ts' : 'js';
const projectLanguage = isUsingTypeScriptSync(process.cwd()) ? 'ts' : 'js';
// TODO: Adds tsconfig & build command for TS plugins?
if (!isKebabCase(answers.pluginName)) {
answers.pluginName = toKebabCase(answers.pluginName);
console.log(
chalk.yellow(
`Strapi only supports kebab-cased names for plugins.\nYour plugin has been automatically renamed to "${answers.pluginName}".`
)
);
}
return [
{

View File

@ -3,6 +3,7 @@
const tsUtils = require('@strapi/typescript-utils');
const getDestinationPrompts = require('./prompts/get-destination-prompts');
const validateInput = require('./utils/validate-input');
const getFilePath = require('./utils/get-file-path');
module.exports = plop => {
@ -14,6 +15,7 @@ module.exports = plop => {
type: 'input',
name: 'id',
message: 'Policy name',
validate: input => validateInput(input),
},
...getDestinationPrompts('policy', plop.getDestBasePath(), { rootFolder: true }),
],

View File

@ -0,0 +1,25 @@
'use strict';
const getFilePath = require('../get-file-path');
describe('Get-File-Path util', () => {
test('with destination set as api', () => {
const filePath = getFilePath('api');
expect(filePath).toBe(`api/{{ api }}`);
});
test('with destination set as plugin', () => {
const filePath = getFilePath('plugin');
expect(filePath).toBe(`plugins/{{ plugin }}/server`);
});
test('with destination set as root', () => {
const filePath = getFilePath('root');
expect(filePath).toBe(`./`);
});
test('with empty destination string', () => {
const filePath = getFilePath('');
expect(filePath).toBe(`api/{{ id }}`);
});
});

View File

@ -4,8 +4,9 @@
"description": "This is the description of the plugin.",
"strapi": {
"name": "{{ pluginName }}",
"description": "Description of {{ pluginName }} plugin",
"kind": "plugin"
"description": "Description of {{titleCase pluginName }} plugin",
"kind": "plugin",
"displayName": "{{titleCase pluginName }}"
},
"dependencies": {},
"author": {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/generators",
"version": "4.1.9",
"version": "4.1.11",
"description": "Interactive API generator.",
"keywords": [
"strapi",
@ -29,9 +29,9 @@
],
"main": "lib/index.js",
"dependencies": {
"@strapi/typescript-utils": "4.1.9",
"@strapi/typescript-utils": "4.1.11",
"@sindresorhus/slugify": "1.1.0",
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"chalk": "4.1.2",
"fs-extra": "10.0.0",
"node-plop": "0.26.3",

View File

@ -147,8 +147,10 @@ describe('Build Component Schema', () => {
const expectedShape = {
type: 'object',
required: ['data'],
properties: {
data: {
required: [],
type: 'object',
properties: { test: { type: 'string' } },
},
@ -237,6 +239,7 @@ describe('Build Component Schema', () => {
const expectedShape = {
type: 'object',
required: ['locale'],
properties: { test: { type: 'string' } },
};

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-documentation",
"version": "4.1.9",
"version": "4.1.11",
"description": "Create an OpenAPI Document and visualize your API with SWAGGER UI.",
"repository": {
"type": "git",
@ -24,8 +24,8 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/helper-plugin": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/helper-plugin": "4.1.11",
"@strapi/utils": "4.1.11",
"bcryptjs": "2.4.3",
"cheerio": "^1.0.0-rc.5",
"fs-extra": "10.0.0",

View File

@ -48,7 +48,7 @@ module.exports = {
properties: {
data: {
nullable: true,
oneOf: [{ type: 'object' }, { type: 'array' }],
oneOf: [{ type: 'object' }, { type: 'array', items: [] }],
},
error: {
type: 'object',

View File

@ -36,23 +36,24 @@ const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
];
const attributesForRequest = _.omit(attributes, attributesToOmit);
const requiredAttributes = Object.entries(attributesForRequest)
.filter(([, attribute]) => attribute.required)
.map(([attributeName, attribute]) => {
return { [attributeName]: attribute };
});
// Get a list of required attribute names
const requiredAttributes = Object.entries(attributesForRequest).reduce((acc, attribute) => {
const [attributeKey, attributeValue] = attribute;
const requestAttributes =
routeMethods.includes('POST') && requiredAttributes.length
? Object.assign({}, ...requiredAttributes)
: attributesForRequest;
if (attributeValue.required) {
acc.push(attributeKey);
}
return acc;
}, []);
if (hasLocalizationPath) {
schemas = {
...schemas,
[`${pascalCase(uniqueName)}LocalizationRequest`]: {
required: [...requiredAttributes, 'locale'],
type: 'object',
properties: cleanSchemaAttributes(requestAttributes, { isRequest: true }),
properties: cleanSchemaAttributes(attributesForRequest, { isRequest: true }),
},
};
}
@ -62,10 +63,12 @@ const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
...schemas,
[`${pascalCase(uniqueName)}Request`]: {
type: 'object',
required: ['data'],
properties: {
data: {
required: requiredAttributes,
type: 'object',
properties: cleanSchemaAttributes(requestAttributes, { isRequest: true }),
properties: cleanSchemaAttributes(attributesForRequest, { isRequest: true }),
},
},
},

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-graphql",
"version": "4.1.9",
"version": "4.1.11",
"description": "Adds GraphQL endpoint with default API methods.",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"@apollo/federation": "^0.28.0",
"@graphql-tools/schema": "8.1.2",
"@graphql-tools/utils": "^8.0.2",
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"apollo-server-core": "3.1.2",
"apollo-server-koa": "3.1.2",
"glob": "^7.1.7",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-i18n",
"version": "4.1.9",
"version": "4.1.11",
"description": "This plugin enables to create, to read and to update content in different languages, both from the Admin Panel and from the API",
"repository": {
"type": "git",
@ -24,7 +24,7 @@
"test:unit": "jest --verbose"
},
"dependencies": {
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"lodash": "4.17.21"
},
"engines": {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-sentry",
"version": "4.1.9",
"version": "4.1.11",
"description": "Send Strapi error events to Sentry",
"repository": {
"type": "git",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-users-permissions",
"version": "4.1.9",
"version": "4.1.11",
"description": "Protect your API with a full-authentication process based on JWT",
"repository": {
"type": "git",
@ -27,8 +27,8 @@
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
},
"dependencies": {
"@strapi/helper-plugin": "4.1.9",
"@strapi/utils": "4.1.9",
"@strapi/helper-plugin": "4.1.11",
"@strapi/utils": "4.1.11",
"bcryptjs": "2.4.3",
"grant-koa": "5.4.8",
"jsonwebtoken": "^8.1.0",

View File

@ -378,7 +378,7 @@ module.exports = {
throw new ValidationError('token.invalid');
}
const user = await userService.fetch({ confirmationToken }, []);
const [user] = await userService.fetchAll({ filters: { confirmationToken } });
if (!user) {
throw new ValidationError('token.invalid');

View File

@ -12,7 +12,7 @@ const { getService } = require('../utils');
const { validateCreateUserBody, validateUpdateUserBody } = require('./validation/user');
const { sanitize } = utils;
const { ApplicationError, ValidationError } = utils.errors;
const { ApplicationError, ValidationError, NotFoundError } = utils.errors;
const sanitizeOutput = (user, ctx) => {
const schema = strapi.getModel('plugin::users-permissions.user');
@ -91,6 +91,9 @@ module.exports = {
const { email, username, password } = ctx.request.body;
const user = await getService('user').fetch(id);
if (!user) {
throw new NotFoundError(`User not found`);
}
await validateUpdateUserBody(ctx.request.body);

View File

@ -19,7 +19,7 @@ module.exports = ({ nexus, strapi }) => {
async resolve(parent, args, context) {
const { koaContext } = context;
koaContext.request.body = toPlainObject(args);
koaContext.query = toPlainObject(args);
await strapi
.plugin('users-permissions')

View File

@ -19,7 +19,7 @@ module.exports = ({ nexus, strapi }) => {
id: nonNull('ID'),
},
description: 'Update an existing user',
description: 'Delete an existing user',
async resolve(parent, args, context) {
const { koaContext } = context;

View File

@ -26,12 +26,12 @@ module.exports = ({ strapi }) => {
// Scoped auth for replaced CRUD operations
// Role
[`Mutation.${createRole}`]: { auth: { scope: [`${roleUID}.create`] } },
[`Mutation.${updateRole}`]: { auth: { scope: [`${roleUID}.update`] } },
[`Mutation.${deleteRole}`]: { auth: { scope: [`${roleUID}.delete`] } },
[`Mutation.${createRole}`]: { auth: { scope: [`${roleUID}.createRole`] } },
[`Mutation.${updateRole}`]: { auth: { scope: [`${roleUID}.updateRole`] } },
[`Mutation.${deleteRole}`]: { auth: { scope: [`${roleUID}.deleteRole`] } },
// User
[`Mutation.${createUser}`]: { auth: { scope: [`${userUID}.create`] } },
[`Mutation.${updateUser}`]: { auth: { scope: [`${userUID}.update`] } },
[`Mutation.${deleteUser}`]: { auth: { scope: [`${userUID}.delete`] } },
[`Mutation.${deleteUser}`]: { auth: { scope: [`${userUID}.destroy`] } },
};
};

View File

@ -80,6 +80,22 @@ describe('Users API', () => {
data.user = res.body;
});
test('Updating unknown user returns 404', async () => {
const res = await rq({
method: 'PUT',
url: '/users/99999999',
});
expect(res.statusCode).toBe(404);
expect(res.body).toMatchObject({
error: {
message: 'User not found',
name: 'NotFoundError',
status: 404,
},
});
});
describe('Read users', () => {
test('without filter', async () => {
const res = await rq({

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-email-amazon-ses",
"version": "4.1.9",
"version": "4.1.11",
"description": "Amazon SES provider for strapi email",
"keywords": [
"email",
@ -36,7 +36,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"node-ses": "^3.0.3"
},
"engines": {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-email-mailgun",
"version": "4.1.9",
"version": "4.1.11",
"description": "Mailgun provider for strapi email plugin",
"keywords": [
"email",
@ -36,7 +36,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"mailgun-js": "0.22.0"
},
"engines": {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-email-nodemailer",
"version": "4.1.9",
"version": "4.1.11",
"description": "Nodemailer provider for Strapi 3",
"keywords": [
"strapi",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-email-sendgrid",
"version": "4.1.9",
"version": "4.1.11",
"description": "Sendgrid provider for strapi email",
"keywords": [
"email",
@ -37,7 +37,7 @@
},
"dependencies": {
"@sendgrid/mail": "7.4.7",
"@strapi/utils": "4.1.9"
"@strapi/utils": "4.1.11"
},
"engines": {
"node": ">=12.22.0 <=16.x.x",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-email-sendmail",
"version": "4.1.9",
"version": "4.1.11",
"description": "Sendmail provider for strapi email",
"keywords": [
"email",
@ -35,7 +35,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"sendmail": "^1.6.1"
},
"engines": {

View File

@ -21,11 +21,13 @@ yarn add @strapi/provider-upload-aws-s3
npm install @strapi/provider-upload-aws-s3 --save
```
## Configurations
## Configuration
Your configuration is passed down to the provider. (e.g: `new AWS.S3(config)`). You can see the complete list of options [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property)
- `provider` defines the name of the provider
- `providerOptions` is passed down during the construction of the provider. (ex: `new AWS.S3(config)`). [Complete list of options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property)
- `actionOptions` is passed directly to the parameters to each method respectively. You can find the complete list of [upload/ uploadStream options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property) and [delete options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#deleteObject-property)
See the [using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) documentation for information on installing and using a provider. And see the [environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables) for setting and using environment variables in your configs.
See the [documentation about using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) for information on installing and using a provider. To understand how environment variables are used in Strapi, please refer to the [documentation about environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables).
### Provider Configuration
@ -45,6 +47,11 @@ module.exports = ({ env }) => ({
Bucket: env('AWS_BUCKET'),
},
},
actionOptions: {
upload: {},
uploadStream: {},
delete: {},
},
},
},
// ...
@ -90,6 +97,8 @@ module.exports = [
];
```
If you use dots in your bucket name, the url of the ressource is in directory style (`s3.yourRegion.amazonaws.com/your.bucket.name/image.jpg`) instead of `yourBucketName.s3.yourRegion.amazonaws.com/image.jpg`. Then only add `s3.yourRegion.amazonaws.com` to img-src and media-src directives.
## Required AWS Policy Actions
These are the minimum amount of permissions needed for this provider to work.

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-upload-aws-s3",
"version": "4.1.9",
"version": "4.1.11",
"description": "AWS S3 provider for strapi upload",
"keywords": [
"upload",

View File

@ -21,13 +21,13 @@ yarn add @strapi/provider-upload-cloudinary
npm install @strapi/provider-upload-cloudinary --save
```
## Configurations
## Configuration
Your configuration is passed down to the cloudinary configuration. (e.g: `cloudinary.config(config)`). You can see the complete list of options [here](https://cloudinary.com/documentation/cloudinary_sdks#configuration_parameters)
- `provider` defines the name of the provider
- `providerOptions` is passed down during the construction of the provider. (ex: `cloudinary.config`). [Complete list of options](https://cloudinary.com/documentation/cloudinary_sdks#configuration_parameters)
- `actionOptions` is passed directly to each method respectively allowing for custom options. You can find the complete list of [upload/ uploadStream options](https://cloudinary.com/documentation/image_upload_api_reference#upload_optional_parameters) and [delete options](https://cloudinary.com/documentation/image_upload_api_reference#destroy_optional_parameters)
`actionOptions` are passed directly to the upload and delete functions respectively allowing for custom options such as folder, type, etc. You can see the complete list of upload options [here](https://cloudinary.com/documentation/image_upload_api_reference#upload_optional_parameters) and delete options [here](https://cloudinary.com/documentation/image_upload_api_reference#destroy_optional_parameters)
See the [using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) documentation for information on installing and using a provider. And see the [environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables) for setting and using environment variables in your configs.
See the [documentation about using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) for information on installing and using a provider. To understand how environment variables are used in Strapi, please refer to the [documentation about environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables).
### Provider Configuration
@ -46,6 +46,7 @@ module.exports = ({ env }) => ({
},
actionOptions: {
upload: {},
uploadStream: {},
delete: {},
},
},

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-upload-cloudinary",
"version": "4.1.9",
"version": "4.1.11",
"description": "Cloudinary provider for strapi upload",
"keywords": [
"upload",
@ -36,7 +36,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"cloudinary": "^1.25.1",
"into-stream": "^5.1.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-upload-local",
"version": "4.1.9",
"version": "4.1.11",
"description": "Local provider for strapi upload",
"keywords": [
"upload",
@ -35,7 +35,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/utils": "4.1.9",
"@strapi/utils": "4.1.11",
"fs-extra": "10.0.0"
},
"engines": {

View File

@ -21,11 +21,13 @@ yarn add @strapi/provider-upload-rackspace
npm install @strapi/provider-upload-rackspace --save
```
## Configurations
## Configuration
Your configuration is passed down to the client initialization. (e.g: `createClient(config)`). The implementation is based on the package `pkgcloud`. You can read the docs [here](https://github.com/pkgcloud/pkgcloud#storage).
- `provider` defines the name of the provider
- `providerOptions` is passed down during the construction of the provider. (ex: `createClient(config)`). [Complete list of options](https://github.com/pkgcloud/pkgcloud/blob/master/docs/providers/rackspace/README.md). The implementation is based on the package `pkgcloud`. [Documentation](https://github.com/pkgcloud/pkgcloud#storage)
- `actionOptions` is passed directly to each method respectively allowing for custom options. You can find the complete list of [upload/ uploadStream options](https://github.com/pkgcloud/pkgcloud#upload-a-file) and [delete options](https://github.com/pkgcloud/pkgcloud/blob/master/docs/providers/rackspace/storage.md#clientremovefilecontainer-file-functionerr-result--)
See the [using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) documentation for information on installing and using a provider. And see the [environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables) for setting and using environment variables in your configs.
See the [documentation about using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) for information on installing and using a provider. To understand how environment variables are used in Strapi, please refer to the [documentation about environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables).
### Provider Configuration
@ -43,6 +45,11 @@ module.exports = ({ env }) => ({
region: env('RACKSPACE_REGION'),
container: env('RACKSPACE_CONTAINER'),
},
actionOptions: {
upload: {},
uploadStream: {},
delete: {},
},
},
},
// ...

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/provider-upload-rackspace",
"version": "4.1.9",
"version": "4.1.11",
"description": "Rackspace provider for strapi upload",
"repository": {
"type": "git",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/babel-plugin-switch-ee-ce",
"version": "4.1.9",
"version": "4.1.11",
"private": false,
"description": "Babel plugin to switch from CE to EE at runtime",
"repository": "git://github.com/strapi/strapi.git",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/logger",
"version": "4.1.9",
"version": "4.1.11",
"description": "Strapi's logger",
"homepage": "https://strapi.io",
"bugs": {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/typescript-utils",
"version": "4.1.9",
"version": "4.1.11",
"description": "Typescript support for Strapi",
"keywords": [
"strapi",

6608
yarn.lock

File diff suppressed because it is too large Load Diff