diff --git a/package.json b/package.json index d53943f..9e572bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cosmicjs/sdk", - "version": "1.0.12", + "version": "1.0.13", "description": "The official client module for Cosmic. This module helps you easily add dynamic content to your website or application using the Cosmic headless CMS.", "keywords": [ "headlesscms", diff --git a/src/clients/bucket/lib/methodChaining.ts b/src/clients/bucket/lib/methodChaining.ts index 99d80ed..96e2858 100644 --- a/src/clients/bucket/lib/methodChaining.ts +++ b/src/clients/bucket/lib/methodChaining.ts @@ -1,6 +1,8 @@ export default class MethodChaining { endpoint: string = ''; + opts: any; + constructor(endpoint: string) { this.endpoint = endpoint; } diff --git a/src/clients/bucket/objects/index.ts b/src/clients/bucket/objects/index.ts index c5ec6bd..1ac3780 100644 --- a/src/clients/bucket/objects/index.ts +++ b/src/clients/bucket/objects/index.ts @@ -17,7 +17,7 @@ export const objectsChainMethods = ( const endpoint = `${apiConfig.apiUrl}/buckets/${ bucketConfig.bucketSlug }/objects?read_key=${bucketConfig.readKey}${encodedQueryParam(query)}`; - return new FindChaining(endpoint); + return new FindChaining(endpoint, bucketConfig); }, findOne>(query: NonEmptyObject) { @@ -26,7 +26,7 @@ export const objectsChainMethods = ( }/objects?read_key=${bucketConfig.readKey}&limit=1${encodedQueryParam( query )}`; - return new FindOneChaining(endpoint); + return new FindOneChaining(endpoint, bucketConfig); }, async insertOne(data: GenericObject) { diff --git a/src/clients/bucket/objects/lib/chaining.ts b/src/clients/bucket/objects/lib/chaining.ts index 511bd7f..d484198 100644 --- a/src/clients/bucket/objects/lib/chaining.ts +++ b/src/clients/bucket/objects/lib/chaining.ts @@ -1,5 +1,28 @@ import MethodChaining from '../../lib/methodChaining'; +/** + * Options for fetching object data. + * @property {Object} media - Options for media objects. + * @property {string} media.props - Comma-separated list of additional properties to fetch for media objects. + * @typedef {Object} MediaType + * @property {string} all - All media properties. + * @property {string} id - The unique identifier of the media object. + * @property {string} name - The name of the media file. + * @property {string} original_name - The original name of the media file. + * @property {number} size - The size of the media file in bytes. + * @property {string} type - The MIME type of the media file. + * @property {string} bucket - The bucket identifier. + * @property {string} created_at - The creation date of the media object. + * @property {string} folder - The folder where the media is stored. + * @property {string} url - The URL of the media file. + * @property {string} imgix_url - The Imgix URL of the media file. + * @property {string} alt_text - The alternative text for the media. + */ +type OptionsType = { + media: { + props: string; + }; +}; export default class Chaining extends MethodChaining { depth(depth: number) { this.endpoint += `&depth=${depth}`; @@ -15,4 +38,11 @@ export default class Chaining extends MethodChaining { this.endpoint += `&after=${after}`; return this; } + + options(options: OptionsType) { + if (options) { + this.opts = options; + } + return this; + } } diff --git a/src/clients/bucket/objects/lib/find.chaining.ts b/src/clients/bucket/objects/lib/find.chaining.ts index d3afccf..f379bb1 100644 --- a/src/clients/bucket/objects/lib/find.chaining.ts +++ b/src/clients/bucket/objects/lib/find.chaining.ts @@ -1,8 +1,18 @@ import { PromiseFnType } from '../../../../types/promise.types'; import { promiserTryCatchWrapper } from '../../../../utils/request.promiser'; import Chaining from './chaining'; +import { addFullMediaData } from '../../../../utils/addFullMedia'; +import { BucketConfig } from '../../../../types/config.types'; +import { createBucketClient } from '../..'; export default class FindChaining extends Chaining { + private bucketConfig: BucketConfig; + + constructor(endpoint: string, bucketConfig: BucketConfig) { + super(endpoint); + this.bucketConfig = bucketConfig; + } + limit(limit: number) { this.endpoint += `&limit=${limit}`; return this; @@ -12,8 +22,16 @@ export default class FindChaining extends Chaining { onFulfilled?: PromiseFnType, onRejected?: PromiseFnType ) { - await promiserTryCatchWrapper(this.endpoint, onRejected, (res) => - onFulfilled?.(res) - ); + await promiserTryCatchWrapper(this.endpoint, onRejected, async (res) => { + // eslint-disable-next-line no-underscore-dangle + if (this.opts && this.opts.media && res.objects) { + res.objects = await addFullMediaData( + res.objects, + createBucketClient(this.bucketConfig), + this.opts.media.props + ); + } + onFulfilled?.(res); + }); } } diff --git a/src/clients/bucket/objects/lib/findOne.chaining.ts b/src/clients/bucket/objects/lib/findOne.chaining.ts index e30effe..4ce3217 100644 --- a/src/clients/bucket/objects/lib/findOne.chaining.ts +++ b/src/clients/bucket/objects/lib/findOne.chaining.ts @@ -1,16 +1,33 @@ import { PromiseFnType } from '../../../../types/promise.types'; import { promiserTryCatchWrapper } from '../../../../utils/request.promiser'; import Chaining from './chaining'; +import { addFullMediaData } from '../../../../utils/addFullMedia'; +import { BucketConfig } from '../../../../types/config.types'; +import { createBucketClient } from '../..'; export default class FindOneChaining extends Chaining { + private bucketConfig: BucketConfig; + + constructor(endpoint: string, bucketConfig: BucketConfig) { + super(endpoint); + this.bucketConfig = bucketConfig; + } + async then( onFulfilled?: PromiseFnType, onRejected?: PromiseFnType ) { - await promiserTryCatchWrapper(this.endpoint, onRejected, (res) => { - onFulfilled?.({ - object: res.objects && res.objects.length ? res.objects[0] : null, - }); + await promiserTryCatchWrapper(this.endpoint, onRejected, async (res) => { + let object = res.objects && res.objects.length ? res.objects[0] : null; + if (this.opts && this.opts.media && object) { + object = await addFullMediaData( + object, + createBucketClient(this.bucketConfig), + this.opts.media.props + ); + } + + onFulfilled?.({ object }); }); } } diff --git a/src/utils/addFullMedia.ts b/src/utils/addFullMedia.ts new file mode 100644 index 0000000..6ea8d4e --- /dev/null +++ b/src/utils/addFullMedia.ts @@ -0,0 +1,77 @@ +const fetchMediaData = async ( + cosmic: any, + filenames: string[], + props: string +) => { + const query = { + name: { $in: filenames }, + }; + const { media } = await cosmic.media + .find(query) + .props(!props || props === 'all' ? '' : `name,url,imgix_url,${props}`); + return media; +}; + +const extractMediaFiles = (obj: any): string[] => { + const mediaFiles: string[] = []; + JSON.stringify(obj, (_, value) => { + if (value && typeof value === 'object') { + const url = value.imgix_url || value.url; + if (url) { + mediaFiles.push(url.split('/').pop().split('?')[0]); + } + } + return value; + }); + return [...new Set(mediaFiles)]; +}; + +const mapMediaDataToResponse = ( + response: any, + mediaData: any[], + props: string +) => { + const mediaMap = new Map(mediaData.map((item) => [item.name, item])); + + const addFullMedia = (obj: any) => { + if (obj && typeof obj === 'object') { + Object.keys(obj).forEach((key) => { + if (obj[key] && typeof obj[key] === 'object') { + const url = obj[key].imgix_url || obj[key].url; + if (url) { + const filename = url.split('/').pop().split('?')[0]; + if (mediaMap.has(filename)) { + // eslint-disable-next-line no-param-reassign + if (!props.includes('name')) { + delete mediaMap.get(filename).name; + } + const newObj = { ...mediaMap.get(filename) }; + Object.assign(obj[key], newObj); + } + } + addFullMedia(obj[key]); + } + }); + } + }; + + addFullMedia(response); +}; + +const addFullMediaData = async (response: any, cosmic: any, props: string) => { + const processItem = async (item: any) => { + const mediaFiles = extractMediaFiles(item); + if (mediaFiles.length > 0) { + const mediaData = await fetchMediaData(cosmic, mediaFiles, props); + mapMediaDataToResponse(item, mediaData, props); + } + return item; + }; + + if (Array.isArray(response)) { + return Promise.all(response.map((item) => processItem(item))); + } + return processItem(response); +}; + +export { addFullMediaData };