superdesk/superdesk-client-core

View on GitHub
scripts/extensions/sams/src/api/assets.ts

Summary

Maintainability
F
1 wk
Test Coverage
// Types
import {
    IAttachment,
    IElasticRangeQueryParams,
    IRestApiResponse,
    IRootElasticQuery,
} from 'superdesk-api';
import {
    ASSET_LIST_STYLE,
    ASSET_SORT_FIELD,
    ASSET_STATE,
    ASSET_TYPE_FILTER,
    IAssetItem,
    IAssetSearchParams,
    SORT_ORDER,
    IUploadAssetModalProps,
    ISetItem,
    SET_STATE,
    IAutoTaggingSearchResult,
} from '../interfaces';
import {superdeskApi} from '../apis';

// Redux Actions & Selectors
import {getStoreSync} from '../store';
import {loadSets} from '../store/sets/actions';
import {getSetsById, getAvailableSetsForDesk} from '../store/sets/selectors';

// Utils
import {fixItemResponseVersionDates, fixItemVersionDates} from './common';
import {getApiErrorMessage, isSamsApiError} from '../utils/api';

// UI
import {showModalConnectedToStore} from '../utils/ui';
import {UploadAssetModal} from '../components/assets/uploadAssetModal';

const RESOURCE = 'sams/assets';
const COUNT_RESOURCE = `${RESOURCE}/counts/`;
const BINARY_RESOURCE = `${RESOURCE}/binary/`;
const COMPRESSED_BINARY_RESOURCE = `${RESOURCE}/compressed_binary/`;
const LOCK_ASSET = `${RESOURCE}/lock`;
const UNLOCK_ASSET = `${RESOURCE}/unlock`;

import {downloadBlob} from '@superdesk/common';

export function uploadAsset(
    data: FormData,
    onProgress: (event: ProgressEvent) => void,
): Promise<IAssetItem> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.uploadFileWithProgress<IAssetItem>(
        '/' + RESOURCE,
        data,
        onProgress,
    )
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to upload file.'));
            }

            return Promise.reject(error);
        });
}

const GRID_PAGE_SIZE = 25;
const LIST_PAGE_SIZE = 50;

function querySearchString(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.textSearch != null && params.textSearch.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.queryString({
                query: params.textSearch,
                lenient: true,
                default_operator: 'AND',
            }),
        );
    }
}

function querySetId(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.setId != null && params.setId.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.term({
                field: 'set_id',
                value: params.setId,
            }),
        );
    }
}

function querySetIds(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.setIds != null && params.setIds.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.terms({
                field: 'set_id',
                value: params.setIds,
            }),
        );
    }
}

function queryAllAvailableSets(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (!params.setId?.length && !params.setIds?.length) {
        // Construct the list of Set IDs to use (based on the currently selected Desk)
        const store = getStoreSync();
        const setIds = getAvailableSetsForDesk(store.getState());

        source.query.bool.must.push(
            superdeskApi.elasticsearch.terms({
                field: 'set_id',
                value: setIds,
            }),
        );
    }
}

function queryExcludedAssetIds(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.excludedAssetIds != null && params.excludedAssetIds.length > 0) {
        source.query.bool.must_not.push(
            superdeskApi.elasticsearch.terms({
                field: '_id',
                value: params.excludedAssetIds,
            }),
        );
    }
}

function queryState(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.state != null && params.state.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.term({
                field: 'state',
                value: params.state,
            }),
        );
    }
}

function queryStates(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.states != null && params.states.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.terms({
                field: 'state',
                value: params.states,
            }),
        );
    }
}

function queryName(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.name != null && params.name.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.queryString({
                query: `name:(${params.name})`,
                lenient: true,
                default_operator: 'AND',
            }),
        );
    }
}

function queryFilename(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.filename != null && params.filename.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.queryString({
                query: `filename:(${params.filename})`,
                lenient: true,
                default_operator: 'AND',
            }),
        );
    }
}

function queryDescription(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.description != null && params.description.length > 0) {
        source.query.bool.must.push(
            superdeskApi.elasticsearch.queryString({
                query: `description:(${params.description})`,
                lenient: false,
                default_operator: 'AND',
            }),
        );
    }
}

function queryTags(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.tags != null && params.tags.length > 0) {
        let taglist: Array<string> = [];

        params.tags.forEach((tag) => {
            taglist.push(tag.code);
        });
        source.query.bool.must.push(
            superdeskApi.elasticsearch.terms({
                field: 'tags.code',
                value: taglist,
            }),
        );
    }
}

function queryMimetypes(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.mimetypes === ASSET_TYPE_FILTER.DOCUMENTS) {
        source.query.bool.must_not.push(
            superdeskApi.elasticsearch.queryString({
                query: 'mimetype:(image\\/*) OR mimetype:(video\\/*) OR mimetype:(audio\\/*)',
                lenient: true,
                default_operator: 'OR',
            }),
        );
    } else if (params.mimetypes !== ASSET_TYPE_FILTER.ALL) {
        let typeString: string;

        switch (params.mimetypes) {
        case ASSET_TYPE_FILTER.IMAGES:
            typeString = 'image';
            break;
        case ASSET_TYPE_FILTER.VIDEOS:
            typeString = 'video';
            break;
        case ASSET_TYPE_FILTER.AUDIO:
            typeString = 'audio';
            break;
        }

        if (typeString != null) {
            source.query.bool.must.push(
                superdeskApi.elasticsearch.queryString({
                    query: `mimetype:(${typeString}\\/*)`,
                    lenient: true,
                    default_operator: 'OR',
                }),
            );
        }
    }
}

function queryDateRange(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.dateFrom != null || params.dateTo != null) {
        const args: IElasticRangeQueryParams = {field: '_updated'};

        if (params.dateFrom != null) {
            args.gte = params.dateFrom.toISOString();
        }

        if (params.dateTo != null) {
            args.lte = params.dateTo.toISOString();
        }

        source.query.bool.must.push(
            superdeskApi.elasticsearch.range(args),
        );
    }
}

function querySizeRange(source: IRootElasticQuery, params: IAssetSearchParams) {
    if (params.sizeFrom != null || params.sizeTo != null) {
        const args: IElasticRangeQueryParams = {field: 'length'};

        if (params.sizeFrom != null) {
            args.gte = params.sizeFrom * 1048576; // MB -> bytes
        }

        if (params.sizeTo != null) {
            args.lte = params.sizeTo * 1048576; // MB -> bytes
        }

        source.query.bool.must.push(
            superdeskApi.elasticsearch.range(args),
        );
    }
}

export function queryAssets(
    params: IAssetSearchParams,
    listStyle: ASSET_LIST_STYLE,
): Promise<IRestApiResponse<IAssetItem>> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;
    const pageSize = listStyle === ASSET_LIST_STYLE.GRID ?
        GRID_PAGE_SIZE :
        LIST_PAGE_SIZE;

    const source: IRootElasticQuery = {
        query: {
            bool: {
                must: [],
                must_not: [],
            },
        },
        size: pageSize,
        from: (params.page - 1) * pageSize,
    };

    [
        querySearchString,
        querySetId,
        querySetIds,
        queryAllAvailableSets,
        queryExcludedAssetIds,
        queryName,
        queryDescription,
        queryTags,
        queryFilename,
        queryMimetypes,
        queryState,
        queryStates,
        queryDateRange,
        querySizeRange,
    ].forEach((func) => func(source, params));

    const sortOrder = params.sortOrder === SORT_ORDER.ASCENDING ? 1 : 0;
    const sort = `[("${params.sortField}",${sortOrder})]`;

    return superdeskApi.dataApi.queryRawJson<IRestApiResponse<IAssetItem>>(
        RESOURCE,
        {
            source: JSON.stringify(source),
            sort: sort,
        },
    )
        .then(fixItemResponseVersionDates)
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to query Assets'));
            }

            return Promise.reject(error);
        });
}

export function getAssetSearchUrlParams(): Partial<IAssetSearchParams> {
    const {urlParams} = superdeskApi.browser.location;
    const {filterUndefined} = superdeskApi.helpers;

    return filterUndefined<IAssetSearchParams>({
        textSearch: urlParams.getString('textSearch'),
        setId: urlParams.getString('setId'),
        name: urlParams.getString('name'),
        description: urlParams.getString('description'),
        tags: urlParams.getStringArray('tags')?.map((tag) => ({'code': tag, 'name': tag})),
        state: urlParams.getString('state') as ASSET_STATE,
        filename: urlParams.getString('filename'),
        mimetypes: urlParams.getString('mimetypes', ASSET_TYPE_FILTER.ALL) as ASSET_TYPE_FILTER,
        dateFrom: urlParams.getDate('dateFrom'),
        dateTo: urlParams.getDate('dateTo'),
        sizeFrom: urlParams.getNumber('sizeFrom'),
        sizeTo: urlParams.getNumber('sizeTo'),
        sortField: urlParams.getString('sortField', ASSET_SORT_FIELD.NAME) as ASSET_SORT_FIELD,
        sortOrder: urlParams.getString('sortOrder', SORT_ORDER.ASCENDING) as SORT_ORDER,
    });
}

export function setAssetSearchUrlParams(params: Partial<IAssetSearchParams>) {
    const {urlParams} = superdeskApi.browser.location;

    urlParams.setString('textSearch', params.textSearch);
    urlParams.setString('setId', params.setId);
    urlParams.setString('name', params.name);
    urlParams.setString('description', params.description);
    urlParams.setStringArray('tags', (params.tags ?? []).map((tag) => (tag.code)));
    urlParams.setString('state', params.state);
    urlParams.setString('filename', params.filename);
    urlParams.setString('mimetypes', params.mimetypes);
    urlParams.setDate('dateFrom', params.dateFrom);
    urlParams.setDate('dateTo', params.dateTo);
    urlParams.setNumber('sizeFrom', params.sizeFrom);
    urlParams.setNumber('sizeTo', params.sizeTo);
    urlParams.setString('sortField', params.sortField);
    urlParams.setString('sortOrder', params.sortOrder);
}

export function getAssetsCount(set_ids: Array<string>): Promise<Dictionary<string, number>> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.queryRawJson<Dictionary<string, number>>(
        COUNT_RESOURCE + JSON.stringify(set_ids),
    )
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to get assets counts for sets'));
            }

            return Promise.reject(error);
        });
}

export function getAssetById(assetId: string): Promise<IAssetItem> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.findOne<IAssetItem>(RESOURCE, assetId)
        .then(fixItemVersionDates)
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext(`Failed to get asset "${assetId}"`));
            }

            return Promise.reject(error);
        });
}

export function getAssetsByIds(ids: Array<string>): Promise<IRestApiResponse<IAssetItem>> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.queryRawJson<IRestApiResponse<IAssetItem>>(
        RESOURCE,
        {source: JSON.stringify({
            query: {
                bool: {
                    must: [
                        superdeskApi.elasticsearch.terms({
                            field: '_id',
                            value: ids,
                        }),
                    ],
                },
            },
        })},
    )
        .then(fixItemResponseVersionDates)
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to get Assets'));
            }

            return Promise.reject(error);
        });
}

export function updateAssetMetadata(
    originalAsset: IAssetItem,
    originalAttachment: IAttachment,
    updates: Partial<IAssetItem>,
): Promise<[IAttachment, IAssetItem]> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return Promise.all([
        superdeskApi.entities.attachment.save(originalAttachment, {
            ...originalAttachment,
            title: updates.name,
            description: updates.description,
            internal: updates.state !== ASSET_STATE.PUBLIC,
        }),
        superdeskApi.dataApi.patch<IAssetItem>(RESOURCE, originalAsset, updates)
            .catch((error: any) => {
                if (isSamsApiError(error)) {
                    notify.error(getApiErrorMessage(error));
                } else {
                    notify.error(gettext('Failed to update asset.'));
                }

                return Promise.reject(error);
            }),
    ]);
}

export function showUploadAssetModal(props?: Partial<IUploadAssetModalProps>): void {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;
    const store = getStoreSync();

    // (re)load all the Sets into the Redux store
    store.dispatch<any>(loadSets())
        .then((sets: Array<ISetItem>) => {
            // Check if there are any usable Sets found
            // Otherwise notify the user to enable one first
            if (sets.filter((set) => set.state === SET_STATE.USABLE).length === 0) {
                notify.error(gettext('No usable Sets found. Enable one first'));
                return;
            }

            // Finally show the upload asset modal
            showModalConnectedToStore<Partial<IUploadAssetModalProps>>(
                UploadAssetModal,
                {
                    modalSize: 'fill',
                    ...props ?? {},
                },
            );
        });
}

export function getAssetDownloadUrl(assetId: IAssetItem['_id']): string {
    const {server} = superdeskApi.instance.config;

    return `${server.url}/${BINARY_RESOURCE}/${assetId}`;
}

export function getAssetBinary(asset: IAssetItem): Promise<void | Response> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.queryRaw<void | Response>(
        BINARY_RESOURCE + asset._id,
    )
        .then((res) => res.arrayBuffer()
            .then((blob: any) => {
                downloadBlob(blob, res.headers.get('Content-type')!, asset.filename);
            }))
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to get binary for asset'));
            }

            return Promise.reject(error);
        });
}

export function getAssetsCompressedBinary(asset_ids: Array<string>): Promise<void | Response> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.queryRaw<void | Response>(
        COMPRESSED_BINARY_RESOURCE + JSON.stringify(asset_ids),
    )
        .then((res) => res.arrayBuffer()
            .then((blob: any) => {
                downloadBlob(blob, 'application/zip', 'download');
            }))
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to get compressed binaries for assets'));
            }

            return Promise.reject(error);
        });
}

export function deleteAsset(item: IAssetItem): Promise<void> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.delete<IAssetItem>(RESOURCE, item)
        .then(() => {
            notify.success(gettext('Asset deleted successfully'));

            return Promise.resolve();
        })
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else if (error._message != null) {
                notify.error(gettext('Error: {{message}}', {message: error._message}));
            } else {
                notify.error(gettext('Failed to delete the Asset'));
            }

            return Promise.reject(error);
        });
}

export function updateAsset(original: IAssetItem, updates: Partial<IAssetItem>): Promise<IAssetItem> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.patch<IAssetItem>(RESOURCE, original, updates)
        .then((asset: IAssetItem) => {
            notify.success(gettext('Asset updated successfully'));

            return asset;
        })
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to update the Asset'));
            }

            return Promise.reject(error);
        });
}

export function lockAsset(original: IAssetItem, updates: Dictionary<string, any>): Promise<Partial<IAssetItem>> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.patch<IAssetItem>(LOCK_ASSET, original, updates)
        .then((asset: IAssetItem) => {
            return asset;
        })
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to lock the Asset'));
            }

            return Promise.reject(error);
        });
}

export function unlockAsset(original: IAssetItem, updates: Dictionary<string, any>): Promise<Partial<IAssetItem>> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.patch<IAssetItem>(UNLOCK_ASSET, original, updates)
        .then((asset: IAssetItem) => {
            return asset;
        })
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to unlock the Asset'));
            }

            return Promise.reject(error);
        });
}

export function getSetsSync(): Dictionary<string, ISetItem> {
    const store = getStoreSync();

    return getSetsById(store.getState());
}

export function searchTags(searchString: string): Promise<IAutoTaggingSearchResult> {
    const {gettext} = superdeskApi.localization;
    const {notify} = superdeskApi.ui;

    return superdeskApi.dataApi.queryRawJson<IAutoTaggingSearchResult>('sams/assets/tags', {'query': searchString})
        .then((res: IAutoTaggingSearchResult) => {
            return res;
        })
        .catch((error: any) => {
            if (isSamsApiError(error)) {
                notify.error(getApiErrorMessage(error));
            } else {
                notify.error(gettext('Failed to get tags'));
            }

            return Promise.reject(error);
        });
}