mirror of
https://github.com/strapi/strapi.git
synced 2025-09-16 20:10:05 +00:00
Merge branch 'main' into fix/16170-graphql-pagination-count-filter
This commit is contained in:
commit
d21f34e436
@ -12,17 +12,64 @@ Async utils are grouping all function that interact with async stuff like Promis
|
|||||||
|
|
||||||
## Detailed design
|
## Detailed design
|
||||||
|
|
||||||
Available functions:
|
### mapAsync
|
||||||
|
|
||||||
- pipeAsync
|
The `mapAsync` function is an asynchronous version of the `Array.prototype.map` method.
|
||||||
- mapAsync
|
|
||||||
- reduceAsync
|
|
||||||
|
|
||||||
[See API reference](../../../api/api.mdx) (TODO)
|
Example usage:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const input = [1, 2, 3];
|
||||||
|
|
||||||
|
const output = await mapAsync(input, async (item) => {
|
||||||
|
return item * 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(output); // [2, 4, 6]
|
||||||
|
```
|
||||||
|
|
||||||
|
### reduceAsync
|
||||||
|
|
||||||
|
The `reduceAsync` function is an asynchronous version of the `Array.prototype.reduce` method.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const input = [1, 2, 3];
|
||||||
|
|
||||||
|
const reducer = reduceAsync(input);
|
||||||
|
const output = await reducer(async (accumulator, item) => {
|
||||||
|
return accumulator + item;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
console.log(output); // 6
|
||||||
|
```
|
||||||
|
|
||||||
|
### pipeAsync
|
||||||
|
|
||||||
|
The `pipeAsync` function is a utility function for composing asynchronous functions. It takes a list of functions as input, and returns a new function that applies each function in turn to the input.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function addOne(input: number): Promise<number> {
|
||||||
|
return input + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function double(input: number): Promise<number> {
|
||||||
|
return input * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addOneAndDouble = pipeAsync(addOne, double);
|
||||||
|
|
||||||
|
const output = await addOneAndDouble(3);
|
||||||
|
|
||||||
|
console.log(output); // 8
|
||||||
|
```
|
||||||
|
|
||||||
### When to use
|
### When to use
|
||||||
|
|
||||||
Everytime the code has to act with promises and iterate other them, an async utils function should be used.
|
Every time the code has to act with promises and iterate other them, an async utils function should be used.
|
||||||
|
|
||||||
### Should I add my function here ?
|
### Should I add my function here ?
|
||||||
|
|
||||||
@ -32,6 +79,12 @@ Please consider the next point if a lots of functions are available in the async
|
|||||||
|
|
||||||
## Potential improvements
|
## Potential improvements
|
||||||
|
|
||||||
|
Some ideas of functions that could be added:
|
||||||
|
|
||||||
|
- Other `Array.prototype` methods: `filterAsync`, `someAsync`, `everyAsync`, `findAsync`, `findIndexAsync`, `flatMapAsync`.
|
||||||
|
- `retryAsync`: A function that retries an asynchronous operation a specified number of times if it fails. It takes an asynchronous operation and a number of retries as input, and returns the result of the operation if it succeeds within the specified number of retries, or throws an error if it fails after all retries.
|
||||||
|
- `timeoutAsync`: A function that adds a timeout to an asynchronous operation. It takes an asynchronous operation and a timeout duration as input, and returns the result of the operation if it completes within the specified timeout, or throws an error if it takes longer than the timeout.
|
||||||
|
|
||||||
If we begin to use lots of async utils function, we may consider to migrate to a specialized library like [asyncjs](http://caolan.github.io/async/v3/)
|
If we begin to use lots of async utils function, we may consider to migrate to a specialized library like [asyncjs](http://caolan.github.io/async/v3/)
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
@ -38,7 +38,7 @@ const AuthenticatedApp = () => {
|
|||||||
const [
|
const [
|
||||||
{ data: appInfos, status },
|
{ data: appInfos, status },
|
||||||
{ data: tagName, isLoading },
|
{ data: tagName, isLoading },
|
||||||
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetched, isFetching },
|
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetching },
|
||||||
{ data: userRoles },
|
{ data: userRoles },
|
||||||
] = useQueries([
|
] = useQueries([
|
||||||
{ queryKey: 'app-infos', queryFn: fetchAppInfo },
|
{ queryKey: 'app-infos', queryFn: fetchAppInfo },
|
||||||
@ -86,7 +86,7 @@ const AuthenticatedApp = () => {
|
|||||||
// We don't need to wait for the release query to be fetched before rendering the plugins
|
// We don't need to wait for the release query to be fetched before rendering the plugins
|
||||||
// however, we need the appInfos and the permissions
|
// however, we need the appInfos and the permissions
|
||||||
const shouldShowNotDependentQueriesLoader =
|
const shouldShowNotDependentQueriesLoader =
|
||||||
(isFetching && isFetched) || status === 'loading' || fetchPermissionsStatus === 'loading';
|
isFetching || status === 'loading' || fetchPermissionsStatus === 'loading';
|
||||||
|
|
||||||
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
|
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
} from '@strapi/helper-plugin';
|
} from '@strapi/helper-plugin';
|
||||||
import { ArrowLeft, Cog, Plus } from '@strapi/icons';
|
import { ArrowLeft, Cog, Plus } from '@strapi/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import getReviewWorkflowsColumn from 'ee_else_ce/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { stringify } from 'qs';
|
import { stringify } from 'qs';
|
||||||
@ -391,6 +392,17 @@ function ListView({
|
|||||||
return formattedHeaders;
|
return formattedHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this should not exist. Ideally we would use registerHook() similar to what has been done
|
||||||
|
// in the i18n plugin. In order to do that review-workflows should have been a plugin. In
|
||||||
|
// a future iteration we need to find a better pattern.
|
||||||
|
|
||||||
|
// In CE this will return null - in EE a column definition including the custom formatting component.
|
||||||
|
const reviewWorkflowColumn = getReviewWorkflowsColumn(layout);
|
||||||
|
|
||||||
|
if (reviewWorkflowColumn) {
|
||||||
|
formattedHeaders.push(reviewWorkflowColumn);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...formattedHeaders,
|
...formattedHeaders,
|
||||||
{
|
{
|
||||||
|
@ -183,13 +183,13 @@ const EditPage = ({ canUpdate }) => {
|
|||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validationSchema={editValidation}
|
validationSchema={editValidation}
|
||||||
>
|
>
|
||||||
{({ errors, values, handleChange, isSubmitting }) => {
|
{({ errors, values, handleChange, isSubmitting, dirty }) => {
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<HeaderLayout
|
<HeaderLayout
|
||||||
primaryAction={
|
primaryAction={
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting || !canUpdate}
|
disabled={(isSubmitting || !canUpdate) ? true : !dirty}
|
||||||
startIcon={<Check />}
|
startIcon={<Check />}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -59,7 +59,10 @@ export const CrumbSimpleMenuAsync = ({ parentsToOmit, currentFolderId, onChangeF
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getFolderURL(pathname, query, ascendant?.id);
|
const url = getFolderURL(pathname, query, {
|
||||||
|
folder: ascendant?.id,
|
||||||
|
folderPath: ascendant?.path,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem isLink as={NavLink} to={url} key={ascendant.id}>
|
<MenuItem isLink as={NavLink} to={url} key={ascendant.id}>
|
||||||
|
@ -82,10 +82,8 @@ describe('useAssets', () => {
|
|||||||
filters: {
|
filters: {
|
||||||
$and: [
|
$and: [
|
||||||
{
|
{
|
||||||
folder: {
|
folderPath: {
|
||||||
id: {
|
$eq: '/',
|
||||||
$null: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -100,7 +98,7 @@ describe('useAssets', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('fetches data from the right URL if a query was set', async () => {
|
test('fetches data from the right URL if a query was set', async () => {
|
||||||
const { result } = await setup({ query: { folder: 1 } });
|
const { result } = await setup({ query: { folderPath: '/1/2' } });
|
||||||
|
|
||||||
await waitFor(() => result.current.isSuccess);
|
await waitFor(() => result.current.isSuccess);
|
||||||
const { get } = useFetchClient();
|
const { get } = useFetchClient();
|
||||||
@ -109,8 +107,8 @@ describe('useAssets', () => {
|
|||||||
filters: {
|
filters: {
|
||||||
$and: [
|
$and: [
|
||||||
{
|
{
|
||||||
folder: {
|
folderPath: {
|
||||||
id: 1,
|
$eq: '/1/2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -126,7 +124,7 @@ describe('useAssets', () => {
|
|||||||
|
|
||||||
test('allows to merge filter query params using filters.$and', async () => {
|
test('allows to merge filter query params using filters.$and', async () => {
|
||||||
const { result } = await setup({
|
const { result } = await setup({
|
||||||
query: { folder: 5, filters: { $and: [{ something: 'true' }] } },
|
query: { folderPath: '/1/2', filters: { $and: [{ something: 'true' }] } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => result.current.isSuccess);
|
await waitFor(() => result.current.isSuccess);
|
||||||
@ -139,8 +137,8 @@ describe('useAssets', () => {
|
|||||||
something: true,
|
something: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
folder: {
|
folderPath: {
|
||||||
id: 5,
|
$eq: '/1/2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -154,9 +152,9 @@ describe('useAssets', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not use folder filter in params if _q', async () => {
|
test('does not use folderPath filter in params if _q', async () => {
|
||||||
const { result } = await setup({
|
const { result } = await setup({
|
||||||
query: { folder: 5, _q: 'something', filters: { $and: [{ something: 'true' }] } },
|
query: { folderPath: '/1/2', _q: 'something', filters: { $and: [{ something: 'true' }] } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => result.current.isSuccess);
|
await waitFor(() => result.current.isSuccess);
|
||||||
@ -183,7 +181,7 @@ describe('useAssets', () => {
|
|||||||
test('correctly encodes the search query _q', async () => {
|
test('correctly encodes the search query _q', async () => {
|
||||||
const _q = 'something&else';
|
const _q = 'something&else';
|
||||||
const { result } = await setup({
|
const { result } = await setup({
|
||||||
query: { folder: 5, _q, filters: { $and: [{ something: 'true' }] } },
|
query: { folderPath: '/1/2', _q, filters: { $and: [{ something: 'true' }] } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => result.current.isSuccess);
|
await waitFor(() => result.current.isSuccess);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useNotifyAT } from '@strapi/design-system';
|
import { useNotifyAT } from '@strapi/design-system';
|
||||||
import { useFetchClient, useNotification } from '@strapi/helper-plugin';
|
import { useFetchClient, useNotification } from '@strapi/helper-plugin';
|
||||||
import { stringify } from 'qs';
|
import { stringify } from 'qs';
|
||||||
@ -13,7 +15,7 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => {
|
|||||||
const { notifyStatus } = useNotifyAT();
|
const { notifyStatus } = useNotifyAT();
|
||||||
const { get } = useFetchClient();
|
const { get } = useFetchClient();
|
||||||
const dataRequestURL = getRequestUrl('files');
|
const dataRequestURL = getRequestUrl('files');
|
||||||
const { folder, _q, ...paramsExceptFolderAndQ } = query;
|
const { folderPath, _q, ...paramsExceptFolderAndQ } = query;
|
||||||
|
|
||||||
let params;
|
let params;
|
||||||
|
|
||||||
@ -29,19 +31,16 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => {
|
|||||||
$and: [
|
$and: [
|
||||||
...(paramsExceptFolderAndQ?.filters?.$and ?? []),
|
...(paramsExceptFolderAndQ?.filters?.$and ?? []),
|
||||||
{
|
{
|
||||||
folder: {
|
folderPath: { $eq: folderPath ?? '/' },
|
||||||
id: folder ?? {
|
|
||||||
$null: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAssets = async () => {
|
const { data, error, isLoading } = useQuery(
|
||||||
try {
|
[pluginId, 'assets', stringify(params)],
|
||||||
|
async () => {
|
||||||
const { data } = await get(
|
const { data } = await get(
|
||||||
`${dataRequestURL}${stringify(params, {
|
`${dataRequestURL}${stringify(params, {
|
||||||
encode: false,
|
encode: false,
|
||||||
@ -49,54 +48,59 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => {
|
|||||||
})}`
|
})}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !skipWhen,
|
||||||
|
staleTime: 0,
|
||||||
|
cacheTime: 0,
|
||||||
|
select(data) {
|
||||||
|
if (data?.results && Array.isArray(data.results)) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
results: data.results
|
||||||
|
/**
|
||||||
|
* Filter out assets that don't have a name.
|
||||||
|
* So we don't try to render them as assets
|
||||||
|
* and get errors.
|
||||||
|
*/
|
||||||
|
.filter((asset) => asset.name)
|
||||||
|
.map((asset) => ({
|
||||||
|
...asset,
|
||||||
|
/**
|
||||||
|
* Mime and ext cannot be null in the front-end because
|
||||||
|
* we expect them to be strings and use the `includes` method.
|
||||||
|
*/
|
||||||
|
mime: asset.mime ?? '',
|
||||||
|
ext: asset.ext ?? '',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
notifyStatus(
|
notifyStatus(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
id: 'list.asset.at.finished',
|
id: 'list.asset.at.finished',
|
||||||
defaultMessage: 'The assets have finished loading.',
|
defaultMessage: 'The assets have finished loading.',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}, [data, formatMessage, notifyStatus]);
|
||||||
|
|
||||||
return data;
|
useEffect(() => {
|
||||||
} catch (err) {
|
if (error) {
|
||||||
toggleNotification({
|
toggleNotification({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: { id: 'notification.error' },
|
message: { id: 'notification.error' },
|
||||||
});
|
});
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
};
|
}, [error, toggleNotification]);
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery([pluginId, 'assets', stringify(params)], getAssets, {
|
|
||||||
enabled: !skipWhen,
|
|
||||||
staleTime: 0,
|
|
||||||
cacheTime: 0,
|
|
||||||
select(data) {
|
|
||||||
if (data?.results && Array.isArray(data.results)) {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
results: data.results
|
|
||||||
/**
|
|
||||||
* Filter out assets that don't have a name.
|
|
||||||
* So we don't try to render them as assets
|
|
||||||
* and get errors.
|
|
||||||
*/
|
|
||||||
.filter((asset) => asset.name)
|
|
||||||
.map((asset) => ({
|
|
||||||
...asset,
|
|
||||||
/**
|
|
||||||
* Mime and ext cannot be null in the front-end because
|
|
||||||
* we expect them to be strings and use the `includes` method.
|
|
||||||
*/
|
|
||||||
mime: asset.mime ?? '',
|
|
||||||
ext: asset.ext ?? '',
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data, error, isLoading };
|
return { data, error, isLoading };
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@ export const Header = ({
|
|||||||
const backQuery = {
|
const backQuery = {
|
||||||
...query,
|
...query,
|
||||||
folder: folder?.parent?.id ?? undefined,
|
folder: folder?.parent?.id ?? undefined,
|
||||||
|
folderPath: folder?.parent?.path ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -366,7 +366,7 @@ exports[`Header renders 1`] = `
|
|||||||
<a
|
<a
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
class="c2 active"
|
class="c2 active"
|
||||||
href="/?folder=2"
|
href="/?folder=2&folderPath=/1"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -367,7 +367,10 @@ export const MediaLibrary = () => {
|
|||||||
(currentFolder) => currentFolder.id === folder.id
|
(currentFolder) => currentFolder.id === folder.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = getFolderURL(pathname, query, folder?.id);
|
const url = getFolderURL(pathname, query, {
|
||||||
|
folder: folder?.id,
|
||||||
|
folderPath: folder?.path,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridItem col={3} key={`folder-${folder.id}`}>
|
<GridItem col={3} key={`folder-${folder.id}`}>
|
||||||
|
@ -18,7 +18,10 @@ const getBreadcrumbDataML = (folder, { pathname, query }) => {
|
|||||||
data.push({
|
data.push({
|
||||||
id: folder.parent.id,
|
id: folder.parent.id,
|
||||||
label: folder.parent.name,
|
label: folder.parent.name,
|
||||||
href: getFolderURL(pathname, query, folder.parent?.id),
|
href: getFolderURL(pathname, query, {
|
||||||
|
folder: folder.parent.id,
|
||||||
|
folderPath: folder.parent.path,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* @param {string} pathname
|
* @param {string} pathname
|
||||||
* @param {object} query
|
* @param {object} currentQuery
|
||||||
* @param {string} query._q Search value of the query
|
* @param {string} query._q Search value of the query
|
||||||
* @param {number|undefined} folderID
|
* @param {object} newQuery
|
||||||
|
* @param {string} newQuery.folder
|
||||||
|
* @param {string} newQuery.folderPath
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { stringify } from 'qs';
|
import { stringify } from 'qs';
|
||||||
|
|
||||||
const getFolderURL = (pathname, query, folderID) => {
|
const getFolderURL = (pathname, currentQuery, { folder, folderPath } = {}) => {
|
||||||
const { _q, ...queryParamsWithoutQ } = query;
|
const { _q, ...queryParamsWithoutQ } = currentQuery;
|
||||||
const queryParamsString = stringify(
|
const queryParamsString = stringify(
|
||||||
{
|
{
|
||||||
...queryParamsWithoutQ,
|
...queryParamsWithoutQ,
|
||||||
folder: folderID,
|
folder,
|
||||||
|
folderPath,
|
||||||
},
|
},
|
||||||
{ encode: false }
|
{ encode: false }
|
||||||
);
|
);
|
||||||
|
@ -3,27 +3,47 @@ import { getFolderURL } from '..';
|
|||||||
const FIXTURE_PATHNAME = '/media-library';
|
const FIXTURE_PATHNAME = '/media-library';
|
||||||
const FIXTURE_QUERY = {};
|
const FIXTURE_QUERY = {};
|
||||||
const FIXTURE_FOLDER = 1;
|
const FIXTURE_FOLDER = 1;
|
||||||
|
const FIXTURE_FOLDER_PATH = '/1/2/3';
|
||||||
|
|
||||||
describe('getFolderURL', () => {
|
describe('getFolderURL', () => {
|
||||||
test('returns a path for the root of the media library', () => {
|
test('returns a path for the root of the media library', () => {
|
||||||
expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY)).toStrictEqual(FIXTURE_PATHNAME);
|
expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY)).toMatchInlineSnapshot(`"/media-library"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns a path for a folder', () => {
|
test('returns a path for a folder', () => {
|
||||||
expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, FIXTURE_FOLDER)).toStrictEqual(
|
expect(
|
||||||
`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER}`
|
getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, { folder: FIXTURE_FOLDER })
|
||||||
);
|
).toMatchInlineSnapshot(`"/media-library?folder=1"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('removes _q query parameter', () => {
|
test('removes _q query parameter', () => {
|
||||||
expect(
|
expect(
|
||||||
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, _q: 'search' }, FIXTURE_FOLDER)
|
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, _q: 'search' }, { folder: FIXTURE_FOLDER })
|
||||||
).toStrictEqual(`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER}`);
|
).toMatchInlineSnapshot(`"/media-library?folder=1"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps and stringifies query parameter', () => {
|
test('keeps and stringifies query parameter', () => {
|
||||||
expect(
|
expect(
|
||||||
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, some: 'thing' }, FIXTURE_FOLDER)
|
getFolderURL(
|
||||||
).toStrictEqual(`${FIXTURE_PATHNAME}?some=thing&folder=${FIXTURE_FOLDER}`);
|
FIXTURE_PATHNAME,
|
||||||
|
{ ...FIXTURE_QUERY, some: 'thing' },
|
||||||
|
{ folder: FIXTURE_FOLDER }
|
||||||
|
)
|
||||||
|
).toMatchInlineSnapshot(`"/media-library?some=thing&folder=1"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includes folderPath if provided', () => {
|
||||||
|
expect(
|
||||||
|
getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, {
|
||||||
|
folder: FIXTURE_FOLDER,
|
||||||
|
folderPath: FIXTURE_FOLDER_PATH,
|
||||||
|
})
|
||||||
|
).toMatchInlineSnapshot(`"/media-library?folder=1&folderPath=/1/2/3"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includes fodlerPath if provided and folder is undefined', () => {
|
||||||
|
expect(
|
||||||
|
getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, { folderPath: FIXTURE_FOLDER_PATH })
|
||||||
|
).toMatchInlineSnapshot(`"/media-library?folderPath=/1/2/3"`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import cqp from '../convert-query-params';
|
import convertQueryParams from '../convert-query-params';
|
||||||
import { Model } from '../types';
|
import { Model } from '../types';
|
||||||
|
|
||||||
const schema: Model = {
|
const schema: Model = {
|
||||||
@ -40,7 +40,7 @@ describe('convert-query-params', () => {
|
|||||||
])('keeps: %s', (key, input) => {
|
])('keeps: %s', (key, input) => {
|
||||||
const expectedOutput = { ...input };
|
const expectedOutput = { ...input };
|
||||||
|
|
||||||
const res = cqp.convertFiltersQueryParams(input, schema);
|
const res = convertQueryParams.convertFiltersQueryParams(input, schema);
|
||||||
expect(res).toEqual(expectedOutput);
|
expect(res).toEqual(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ describe('convert-query-params', () => {
|
|||||||
['invalid operator', { $nope: 'test' }],
|
['invalid operator', { $nope: 'test' }],
|
||||||
['uppercase operator', { $GT: new Date() }],
|
['uppercase operator', { $GT: new Date() }],
|
||||||
])('removes: %s', (key, input) => {
|
])('removes: %s', (key, input) => {
|
||||||
const res = cqp.convertFiltersQueryParams(input, schema);
|
const res = convertQueryParams.convertFiltersQueryParams(input, schema);
|
||||||
expect(res).toEqual({});
|
expect(res).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ const singleTypeModel = {
|
|||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
attributes: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const models = {
|
const models = {
|
||||||
@ -355,6 +356,7 @@ describe('Entity service decorator', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultService = {
|
const defaultService = {
|
||||||
|
wrapResult: jest.fn((input) => Promise.resolve(input)),
|
||||||
wrapParams: jest.fn(() => Promise.resolve(entry)),
|
wrapParams: jest.fn(() => Promise.resolve(entry)),
|
||||||
findMany: jest.fn(() => Promise.resolve(entry)),
|
findMany: jest.fn(() => Promise.resolve(entry)),
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const { has, get, omit, isArray } = require('lodash/fp');
|
const { has, get, omit, isArray } = require('lodash/fp');
|
||||||
const { ApplicationError } = require('@strapi/utils').errors;
|
const { ApplicationError } = require('@strapi/utils').errors;
|
||||||
|
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
|
||||||
|
|
||||||
const { getService } = require('../utils');
|
const { getService } = require('../utils');
|
||||||
|
|
||||||
@ -79,8 +80,19 @@ const assignValidLocale = async (data) => {
|
|||||||
*/
|
*/
|
||||||
const decorator = (service) => ({
|
const decorator = (service) => ({
|
||||||
/**
|
/**
|
||||||
|
* Wraps result
|
||||||
|
* @param {object} result - result object of query
|
||||||
|
* @param {object} ctx - Query context
|
||||||
|
* @param {object} ctx.model - Model that is being used
|
||||||
|
*/
|
||||||
|
async wrapResult(result = {}, ctx = {}) {
|
||||||
|
return service.wrapResult.call(this, result, ctx);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
* Wraps query options. In particular will add default locale to query params
|
* Wraps query options. In particular will add default locale to query params
|
||||||
* @param {object} opts - Query options object (params, data, files, populate)
|
* @param {object} params - Query options object (params, data, files, populate)
|
||||||
* @param {object} ctx - Query context
|
* @param {object} ctx - Query context
|
||||||
* @param {object} ctx.model - Model that is being used
|
* @param {object} ctx.model - Model that is being used
|
||||||
*/
|
*/
|
||||||
@ -164,13 +176,15 @@ const decorator = (service) => ({
|
|||||||
return service.findMany.call(this, uid, opts);
|
return service.findMany.call(this, uid, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { kind } = strapi.getModel(uid);
|
const { kind } = model;
|
||||||
|
|
||||||
if (kind === 'singleType') {
|
if (kind === 'singleType') {
|
||||||
if (opts[LOCALE_QUERY_FILTER] === 'all') {
|
if (opts[LOCALE_QUERY_FILTER] === 'all') {
|
||||||
// TODO Fix so this won't break lower lying find many wrappers
|
// TODO Fix so this won't break lower lying find many wrappers
|
||||||
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findMany' });
|
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findMany' });
|
||||||
return strapi.db.query(uid).findMany(wrappedParams);
|
const query = transformParamsToQuery(uid, wrappedParams);
|
||||||
|
const entities = await strapi.db.query(uid).findMany(query);
|
||||||
|
return this.wrapResult(entities, { uid, action: 'findMany' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// This one gets transformed into a findOne on a lower layer
|
// This one gets transformed into a findOne on a lower layer
|
||||||
|
@ -70,9 +70,11 @@ module.exports = ({ env }) => ({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration for a private S3 bucket
|
### Configuration for a private S3 bucket and signed URLs
|
||||||
|
|
||||||
If your bucket is configured to be private, you will need to set the `ACL` option to `private` in the `params` object. This will ensure that the signed URL is generated with the correct permissions.
|
If your bucket is configured to be private, you will need to set the `ACL` option to `private` in the `params` object. This will ensure file URLs are signed.
|
||||||
|
|
||||||
|
**Note:** If you are using a CDN, the URLs will not be signed.
|
||||||
|
|
||||||
You can also define the expiration time of the signed URL by setting the `signedUrlExpires` option in the `params` object. The default value is 15 minutes.
|
You can also define the expiration time of the signed URL by setting the `signedUrlExpires` option in the `params` object. The default value is 15 minutes.
|
||||||
|
|
||||||
|
@ -73,6 +73,6 @@ describe('Test for URLs', () => {
|
|||||||
test('CDN', async () => {
|
test('CDN', async () => {
|
||||||
const url = 'https://cdn.example.com/v1/img.png';
|
const url = 'https://cdn.example.com/v1/img.png';
|
||||||
const isFromBucket = isUrlFromBucket(url, 'bucket', 'https://cdn.example.com/v1/');
|
const isFromBucket = isUrlFromBucket(url, 'bucket', 'https://cdn.example.com/v1/');
|
||||||
expect(isFromBucket).toEqual(true);
|
expect(isFromBucket).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -143,4 +143,36 @@ describe('AWS-S3 provider', () => {
|
|||||||
expect(file.url).toEqual('https://cdn.test/dir/dir2/tmp/test/test.json');
|
expect(file.url).toEqual('https://cdn.test/dir/dir2/tmp/test/test.json');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isPrivate', () => {
|
||||||
|
test('Should sign files if ACL is private', async () => {
|
||||||
|
const providerInstance = awsProvider.init({
|
||||||
|
s3Options: {
|
||||||
|
params: {
|
||||||
|
Bucket: 'test',
|
||||||
|
ACL: 'private',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrivate = providerInstance.isPrivate();
|
||||||
|
|
||||||
|
expect(isPrivate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not sign files if ACL is public', async () => {
|
||||||
|
const providerInstance = awsProvider.init({
|
||||||
|
s3Options: {
|
||||||
|
params: {
|
||||||
|
Bucket: 'test',
|
||||||
|
ACL: 'public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrivate = providerInstance.isPrivate();
|
||||||
|
|
||||||
|
expect(isPrivate).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -69,14 +69,6 @@ export = {
|
|||||||
|
|
||||||
const ACL = getOr('public-read', ['params', 'ACL'], config);
|
const ACL = getOr('public-read', ['params', 'ACL'], config);
|
||||||
|
|
||||||
// if ACL is private and baseUrl is set, we need to warn the user
|
|
||||||
// signed url's will not have the baseUrl prefix
|
|
||||||
if (ACL === 'private' && baseUrl) {
|
|
||||||
process.emitWarning(
|
|
||||||
'You are using a private ACL with a baseUrl. This is not recommended as the files will be accessible without the baseUrl prefix.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const upload = (file: File, customParams = {}): Promise<void> =>
|
const upload = (file: File, customParams = {}): Promise<void> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const fileKey = getFileKey(file);
|
const fileKey = getFileKey(file);
|
||||||
|
@ -5,14 +5,13 @@ interface BucketInfo {
|
|||||||
err?: string;
|
err?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUrlFromBucket(fileUrl: string, bucketName: string, bucketBaseUrl = ''): boolean {
|
export function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {
|
||||||
const url = new URL(fileUrl);
|
const url = new URL(fileUrl);
|
||||||
|
|
||||||
// Check if the file URL is using a base URL (e.g. a CDN).
|
// Check if the file URL is using a base URL (e.g. a CDN).
|
||||||
// In this case, check if the file URL starts with the same base URL as the bucket URL.
|
// In this case do not sign the URL.
|
||||||
if (bucketBaseUrl) {
|
if (baseUrl) {
|
||||||
const baseUrl = new URL(bucketBaseUrl);
|
return false;
|
||||||
return url.href.startsWith(baseUrl.href);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { bucket } = getBucketFromAwsUrl(fileUrl);
|
const { bucket } = getBucketFromAwsUrl(fileUrl);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user