diff --git a/.changeset/nasty-comics-fetch.md b/.changeset/nasty-comics-fetch.md new file mode 100644 index 0000000..6889f65 --- /dev/null +++ b/.changeset/nasty-comics-fetch.md @@ -0,0 +1,5 @@ +--- +'@cosmicjs/sdk': minor +--- + +Adds: props graph syntax to Objects fetching, media data fetching option diff --git a/.gitignore b/.gitignore index 488b755..f2b2b84 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ node_modules dist -test.* \ No newline at end of file +test.* diff --git a/.npmignore b/.npmignore index cfc8d87..6741fb9 100644 --- a/.npmignore +++ b/.npmignore @@ -11,3 +11,5 @@ src .prettierrc.js .nvmrc tsconfig.json + +test.* diff --git a/package.json b/package.json index d53943f..bc6e2f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cosmicjs/sdk", - "version": "1.0.12", + "version": "1.1.0", "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..3b4cf4f 100644 --- a/src/clients/bucket/lib/methodChaining.ts +++ b/src/clients/bucket/lib/methodChaining.ts @@ -1,23 +1,57 @@ export default class MethodChaining { endpoint: string = ''; + opts: any; + constructor(endpoint: string) { this.endpoint = endpoint; } props(props: string | Array) { - let propStr = props; - if (Array.isArray(propStr)) { - propStr = propStr - .filter((prop) => typeof prop === 'string') + let propStr: string; + + if (typeof props === 'string') { + propStr = + props.startsWith('{') && props.endsWith('}') + ? this.parseGraphQLProps(props.slice(1, -1)) + : props; + } else if (Array.isArray(props)) { + propStr = props + .filter((prop): prop is string => typeof prop === 'string') .map((prop) => prop.trim()) - .filter((prop) => !!prop) - .toString(); + .filter(Boolean) + .join(','); + } else { + throw new Error('Invalid props type'); } - this.endpoint += `&props=${propStr}`; + this.endpoint += `&props=${encodeURIComponent(propStr)}`; return this; } + private parseGraphQLProps(propsString: string): string { + const lines = propsString + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + const result: string[] = []; + const currentPath: string[] = []; + + for (const line of lines) { + if (line.includes('{')) { + const [key] = line.split('{'); + if (key !== undefined) { + currentPath.push(key.trim()); + } + } else if (line === '}') { + currentPath.pop(); + } else { + result.push([...currentPath, line].join('.')); + } + } + + return result.join(','); + } + sort(sort: string) { this.endpoint += `&sort=${sort}`; return this; diff --git a/src/clients/bucket/media/index.ts b/src/clients/bucket/media/index.ts index 5f58d19..0c3f721 100644 --- a/src/clients/bucket/media/index.ts +++ b/src/clients/bucket/media/index.ts @@ -48,9 +48,6 @@ export const mediaChainMethods = ( if (params.metadata) { data.append('metadata', JSON.stringify(params.metadata)); } - if (params.alt_text) { - data.append('alt_text', params.alt_text); - } if (params.trigger_webhook) { data.append('trigger_webhook', params.trigger_webhook.toString()); } 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/types/media.types.ts b/src/types/media.types.ts index d1bf32e..c50f7fc 100644 --- a/src/types/media.types.ts +++ b/src/types/media.types.ts @@ -3,7 +3,6 @@ import { GenericObject } from './generic.types'; export type InsertMediaType = { media: any; folder?: string; - alt_text?: string; metadata?: GenericObject; trigger_webhook?: boolean; }; 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 };