mirror of
https://github.com/strapi/strapi.git
synced 2025-09-15 19:39:06 +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
|
||||
|
||||
Available functions:
|
||||
### mapAsync
|
||||
|
||||
- pipeAsync
|
||||
- mapAsync
|
||||
- reduceAsync
|
||||
The `mapAsync` function is an asynchronous version of the `Array.prototype.map` method.
|
||||
|
||||
[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
|
||||
|
||||
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 ?
|
||||
|
||||
@ -32,6 +79,12 @@ Please consider the next point if a lots of functions are available in the async
|
||||
|
||||
## 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/)
|
||||
|
||||
## Resources
|
||||
|
@ -38,7 +38,7 @@ const AuthenticatedApp = () => {
|
||||
const [
|
||||
{ data: appInfos, status },
|
||||
{ data: tagName, isLoading },
|
||||
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetched, isFetching },
|
||||
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetching },
|
||||
{ data: userRoles },
|
||||
] = useQueries([
|
||||
{ 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
|
||||
// however, we need the appInfos and the permissions
|
||||
const shouldShowNotDependentQueriesLoader =
|
||||
(isFetching && isFetched) || status === 'loading' || fetchPermissionsStatus === 'loading';
|
||||
isFetching || status === 'loading' || fetchPermissionsStatus === 'loading';
|
||||
|
||||
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
|
||||
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
} from '@strapi/helper-plugin';
|
||||
import { ArrowLeft, Cog, Plus } from '@strapi/icons';
|
||||
import axios from 'axios';
|
||||
import getReviewWorkflowsColumn from 'ee_else_ce/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringify } from 'qs';
|
||||
@ -391,6 +392,17 @@ function ListView({
|
||||
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 [
|
||||
...formattedHeaders,
|
||||
{
|
||||
|
@ -183,13 +183,13 @@ const EditPage = ({ canUpdate }) => {
|
||||
validateOnChange={false}
|
||||
validationSchema={editValidation}
|
||||
>
|
||||
{({ errors, values, handleChange, isSubmitting }) => {
|
||||
{({ errors, values, handleChange, isSubmitting, dirty }) => {
|
||||
return (
|
||||
<Form>
|
||||
<HeaderLayout
|
||||
primaryAction={
|
||||
<Button
|
||||
disabled={isSubmitting || !canUpdate}
|
||||
disabled={(isSubmitting || !canUpdate) ? true : !dirty}
|
||||
startIcon={<Check />}
|
||||
loading={isSubmitting}
|
||||
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 (
|
||||
<MenuItem isLink as={NavLink} to={url} key={ascendant.id}>
|
||||
|
@ -82,10 +82,8 @@ describe('useAssets', () => {
|
||||
filters: {
|
||||
$and: [
|
||||
{
|
||||
folder: {
|
||||
id: {
|
||||
$null: true,
|
||||
},
|
||||
folderPath: {
|
||||
$eq: '/',
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -100,7 +98,7 @@ describe('useAssets', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
const { get } = useFetchClient();
|
||||
@ -109,8 +107,8 @@ describe('useAssets', () => {
|
||||
filters: {
|
||||
$and: [
|
||||
{
|
||||
folder: {
|
||||
id: 1,
|
||||
folderPath: {
|
||||
$eq: '/1/2',
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -126,7 +124,7 @@ describe('useAssets', () => {
|
||||
|
||||
test('allows to merge filter query params using filters.$and', async () => {
|
||||
const { result } = await setup({
|
||||
query: { folder: 5, filters: { $and: [{ something: 'true' }] } },
|
||||
query: { folderPath: '/1/2', filters: { $and: [{ something: 'true' }] } },
|
||||
});
|
||||
|
||||
await waitFor(() => result.current.isSuccess);
|
||||
@ -139,8 +137,8 @@ describe('useAssets', () => {
|
||||
something: true,
|
||||
},
|
||||
{
|
||||
folder: {
|
||||
id: 5,
|
||||
folderPath: {
|
||||
$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({
|
||||
query: { folder: 5, _q: 'something', filters: { $and: [{ something: 'true' }] } },
|
||||
query: { folderPath: '/1/2', _q: 'something', filters: { $and: [{ something: 'true' }] } },
|
||||
});
|
||||
|
||||
await waitFor(() => result.current.isSuccess);
|
||||
@ -183,7 +181,7 @@ describe('useAssets', () => {
|
||||
test('correctly encodes the search query _q', async () => {
|
||||
const _q = 'something&else';
|
||||
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);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useNotifyAT } from '@strapi/design-system';
|
||||
import { useFetchClient, useNotification } from '@strapi/helper-plugin';
|
||||
import { stringify } from 'qs';
|
||||
@ -13,7 +15,7 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => {
|
||||
const { notifyStatus } = useNotifyAT();
|
||||
const { get } = useFetchClient();
|
||||
const dataRequestURL = getRequestUrl('files');
|
||||
const { folder, _q, ...paramsExceptFolderAndQ } = query;
|
||||
const { folderPath, _q, ...paramsExceptFolderAndQ } = query;
|
||||
|
||||
let params;
|
||||
|
||||
@ -29,19 +31,16 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => {
|
||||
$and: [
|
||||
...(paramsExceptFolderAndQ?.filters?.$and ?? []),
|
||||
{
|
||||
folder: {
|
||||
id: folder ?? {
|
||||
$null: true,
|
||||
},
|
||||
},
|
||||
folderPath: { $eq: folderPath ?? '/' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const getAssets = async () => {
|
||||
try {
|
||||
const { data, error, isLoading } = useQuery(
|
||||
[pluginId, 'assets', stringify(params)],
|
||||
async () => {
|
||||
const { data } = await get(
|
||||
`${dataRequestURL}${stringify(params, {
|
||||
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(
|
||||
formatMessage({
|
||||
id: 'list.asset.at.finished',
|
||||
defaultMessage: 'The assets have finished loading.',
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [data, formatMessage, notifyStatus]);
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: { id: 'notification.error' },
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
}, [error, toggleNotification]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
};
|
||||
|
@ -25,6 +25,7 @@ export const Header = ({
|
||||
const backQuery = {
|
||||
...query,
|
||||
folder: folder?.parent?.id ?? undefined,
|
||||
folderPath: folder?.parent?.path ?? undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -366,7 +366,7 @@ exports[`Header renders 1`] = `
|
||||
<a
|
||||
aria-current="page"
|
||||
class="c2 active"
|
||||
href="/?folder=2"
|
||||
href="/?folder=2&folderPath=/1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
|
@ -367,7 +367,10 @@ export const MediaLibrary = () => {
|
||||
(currentFolder) => currentFolder.id === folder.id
|
||||
);
|
||||
|
||||
const url = getFolderURL(pathname, query, folder?.id);
|
||||
const url = getFolderURL(pathname, query, {
|
||||
folder: folder?.id,
|
||||
folderPath: folder?.path,
|
||||
});
|
||||
|
||||
return (
|
||||
<GridItem col={3} key={`folder-${folder.id}`}>
|
||||
|
@ -18,7 +18,10 @@ const getBreadcrumbDataML = (folder, { pathname, query }) => {
|
||||
data.push({
|
||||
id: folder.parent.id,
|
||||
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 {object} query
|
||||
* @param {object} currentQuery
|
||||
* @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}
|
||||
*/
|
||||
|
||||
import { stringify } from 'qs';
|
||||
|
||||
const getFolderURL = (pathname, query, folderID) => {
|
||||
const { _q, ...queryParamsWithoutQ } = query;
|
||||
const getFolderURL = (pathname, currentQuery, { folder, folderPath } = {}) => {
|
||||
const { _q, ...queryParamsWithoutQ } = currentQuery;
|
||||
const queryParamsString = stringify(
|
||||
{
|
||||
...queryParamsWithoutQ,
|
||||
folder: folderID,
|
||||
folder,
|
||||
folderPath,
|
||||
},
|
||||
{ encode: false }
|
||||
);
|
||||
|
@ -3,27 +3,47 @@ import { getFolderURL } from '..';
|
||||
const FIXTURE_PATHNAME = '/media-library';
|
||||
const FIXTURE_QUERY = {};
|
||||
const FIXTURE_FOLDER = 1;
|
||||
const FIXTURE_FOLDER_PATH = '/1/2/3';
|
||||
|
||||
describe('getFolderURL', () => {
|
||||
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', () => {
|
||||
expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, FIXTURE_FOLDER)).toStrictEqual(
|
||||
`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER}`
|
||||
);
|
||||
expect(
|
||||
getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, { folder: FIXTURE_FOLDER })
|
||||
).toMatchInlineSnapshot(`"/media-library?folder=1"`);
|
||||
});
|
||||
|
||||
test('removes _q query parameter', () => {
|
||||
expect(
|
||||
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, _q: 'search' }, FIXTURE_FOLDER)
|
||||
).toStrictEqual(`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER}`);
|
||||
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, _q: 'search' }, { folder: FIXTURE_FOLDER })
|
||||
).toMatchInlineSnapshot(`"/media-library?folder=1"`);
|
||||
});
|
||||
|
||||
test('keeps and stringifies query parameter', () => {
|
||||
expect(
|
||||
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, some: 'thing' }, FIXTURE_FOLDER)
|
||||
).toStrictEqual(`${FIXTURE_PATHNAME}?some=thing&folder=${FIXTURE_FOLDER}`);
|
||||
getFolderURL(
|
||||
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';
|
||||
|
||||
const schema: Model = {
|
||||
@ -40,7 +40,7 @@ describe('convert-query-params', () => {
|
||||
])('keeps: %s', (key, input) => {
|
||||
const expectedOutput = { ...input };
|
||||
|
||||
const res = cqp.convertFiltersQueryParams(input, schema);
|
||||
const res = convertQueryParams.convertFiltersQueryParams(input, schema);
|
||||
expect(res).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
@ -50,7 +50,7 @@ describe('convert-query-params', () => {
|
||||
['invalid operator', { $nope: 'test' }],
|
||||
['uppercase operator', { $GT: new Date() }],
|
||||
])('removes: %s', (key, input) => {
|
||||
const res = cqp.convertFiltersQueryParams(input, schema);
|
||||
const res = convertQueryParams.convertFiltersQueryParams(input, schema);
|
||||
expect(res).toEqual({});
|
||||
});
|
||||
|
||||
|
@ -37,6 +37,7 @@ const singleTypeModel = {
|
||||
localized: true,
|
||||
},
|
||||
},
|
||||
attributes: {}
|
||||
};
|
||||
|
||||
const models = {
|
||||
@ -355,6 +356,7 @@ describe('Entity service decorator', () => {
|
||||
};
|
||||
|
||||
const defaultService = {
|
||||
wrapResult: jest.fn((input) => Promise.resolve(input)),
|
||||
wrapParams: jest.fn(() => Promise.resolve(entry)),
|
||||
findMany: jest.fn(() => Promise.resolve(entry)),
|
||||
};
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
const { has, get, omit, isArray } = require('lodash/fp');
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
|
||||
|
||||
const { getService } = require('../utils');
|
||||
|
||||
@ -79,8 +80,19 @@ const assignValidLocale = async (data) => {
|
||||
*/
|
||||
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
|
||||
* @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.model - Model that is being used
|
||||
*/
|
||||
@ -164,13 +176,15 @@ const decorator = (service) => ({
|
||||
return service.findMany.call(this, uid, opts);
|
||||
}
|
||||
|
||||
const { kind } = strapi.getModel(uid);
|
||||
const { kind } = model;
|
||||
|
||||
if (kind === 'singleType') {
|
||||
if (opts[LOCALE_QUERY_FILTER] === 'all') {
|
||||
// TODO Fix so this won't break lower lying find many wrappers
|
||||
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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -73,6 +73,6 @@ describe('Test for URLs', () => {
|
||||
test('CDN', async () => {
|
||||
const url = 'https://cdn.example.com/v1/img.png';
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// 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> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileKey = getFileKey(file);
|
||||
|
@ -5,14 +5,13 @@ interface BucketInfo {
|
||||
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);
|
||||
|
||||
// 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.
|
||||
if (bucketBaseUrl) {
|
||||
const baseUrl = new URL(bucketBaseUrl);
|
||||
return url.href.startsWith(baseUrl.href);
|
||||
// In this case do not sign the URL.
|
||||
if (baseUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { bucket } = getBucketFromAwsUrl(fileUrl);
|
||||
|
Loading…
x
Reference in New Issue
Block a user