UNPKG

@contentstack/cli-variants

Version:

Variants plugin

419 lines (360 loc) 14.5 kB
import omit from 'lodash/omit'; import { existsSync } from 'fs'; import { HttpClient, HttpResponse, HttpClientOptions, ContentstackClient, ContentstackConfig, managementSDKClient, authenticationHandler, log, } from '@contentstack/cli-utilities'; import { APIConfig, AdapterType, AnyProperty, ExportConfig, ImportConfig, VariantOptions, VariantsOption, VariantInterface, VariantEntryStruct, CreateVariantEntryDto, CreateVariantEntryOptions, APIResponse, PublishVariantEntryDto, PublishVariantEntryOptions, } from '../types'; import messages from '../messages'; import { AdapterHelper } from './adapter-helper'; import { formatErrors } from './error-helper'; export class VariantHttpClient<C> extends AdapterHelper<C, HttpClient> implements VariantInterface<C, HttpClient> { public baseURL: string; public exportConfig?: ExportConfig; constructor(config: APIConfig, options?: HttpClientOptions) { super(config, options); this.baseURL = config.baseURL?.includes('http') ? `${config.baseURL}/v3` : `https://${config.baseURL}/v3`; this.apiClient.baseUrl(this.baseURL); log.debug(`VariantHttpClient initialized with base URL: ${this.baseURL}`, this.exportConfig?.context); } async init(): Promise<void> { log.debug('Initializing VariantHttpClient...', this.exportConfig?.context); await authenticationHandler.getAuthDetails(); const token = authenticationHandler.accessToken; log.debug( `Authentication type: ${authenticationHandler.isOauthEnabled ? 'OAuth' : 'Token'}`, this.exportConfig?.context, ); if (authenticationHandler.isOauthEnabled) { log.debug('Setting OAuth authorization header...', this.exportConfig?.context); this.apiClient.headers({ authorization: token }); } else { log.debug('Setting authtoken header...', this.exportConfig?.context); this.apiClient.headers({ authtoken: token }); } } async variantEntry(options: VariantOptions) { log.debug('VariantEntry method called (placeholder implementation)', { module: 'variant-api-adapter' }); // TODO single entry variant return { entry: {} }; } /** * The function `variantEntries` retrieves variant entries based on specified options and stores them * in an array. * @param {VariantsOption} options - The `options` parameter in the `variantEntries` function is an * object that contains the following properties: * @param {Record<string, any>[]} entries - The `entries` parameter in the `variantEntries` function * is an array of objects where each object represents a record with key-value pairs. This parameter * is used to store the entries retrieved from the API response or passed down recursively when * fetching all data. * @returns The function `variantEntries` returns a Promise that resolves to an object with an * `entries` property containing an array of record objects or an unknown array. The function can * also return void if certain conditions are not met. */ async variantEntries( options: VariantsOption, entries: Record<string, any>[] = [], ): Promise<{ entries?: Record<string, any>[] | unknown[] } | void> { const variantConfig = (this.config as ExportConfig).modules.variantEntry; const { callback, entry_uid, getAllData, returnResult, content_type_uid, locale, skip = variantConfig.query.skip || 0, limit = variantConfig.query.limit || 100, include_variant = variantConfig.query.include_variant || true, include_count = variantConfig.query.include_count || true, include_publish_details = variantConfig.query.include_publish_details || true, } = options; log.debug( `Fetching variant entries for content type: ${content_type_uid}, entry: ${entry_uid}, locale: ${locale}`, this.exportConfig?.context, ); log.debug( `Query parameters - skip: ${skip}, limit: ${limit}, include_variant: ${include_variant}, include_count: ${include_count}, include_publish_details: ${include_publish_details}`, this.exportConfig?.context, ); if (variantConfig.serveMockData && callback) { log.debug('Using mock data for variant entries...', this.exportConfig?.context); let data = [] as Record<string, any>[]; if (existsSync(variantConfig.mockDataPath)) { log.debug(`Loading mock data from: ${variantConfig.mockDataPath}`, this.exportConfig?.context); data = require(variantConfig.mockDataPath) as Record<string, any>[]; } callback(data); return; } if (!locale) { log.debug('No locale provided, skipping variant entries fetch', this.exportConfig?.context); return; } const start = Date.now(); let endpoint = `/content_types/${content_type_uid}/entries/${entry_uid}/variants?locale=${locale}`; if (skip) { endpoint = endpoint.concat(`&skip=${skip}`); } if (limit) { endpoint = endpoint.concat(`&limit=${limit}`); } if (include_variant) { endpoint = endpoint.concat(`&include_variant=${include_variant}`); } if (include_count) { endpoint = endpoint.concat(`&include_count=${include_count}`); } if (include_publish_details) { endpoint = endpoint.concat(`&include_publish_details=${include_publish_details}`); } const query = this.constructQuery( omit(variantConfig.query, [ 'skip', 'limit', 'locale', 'include_variant', 'include_count', 'include_publish_details', ]), ); if (query) { endpoint = endpoint.concat(query); } log.debug(`Making API call to: ${endpoint}`, this.exportConfig?.context); const data = await this.apiClient.get(endpoint); const response = (await this.handleVariantAPIRes(data)) as { entries: VariantEntryStruct[]; count: number }; if (response?.entries?.length) { log.debug( `Received ${response.entries?.length} variant entries out of total ${response.count}`, this.exportConfig?.context, ); } if (callback) { log.debug('Executing callback with variant entries...', this.exportConfig?.context); callback(response.entries); } else { log.debug('Adding variant entries to collection...', this.exportConfig?.context); entries = entries.concat(response.entries); } if (getAllData && skip + limit < response.count) { const end = Date.now(); const exeTime = end - start; if (exeTime < 1000) { // 1 API call per second log.debug(`Rate limiting: waiting ${1000 - exeTime}ms before next request`, this.exportConfig?.context ); await this.delay(1000 - exeTime); } if (!options.skip) { options['skip'] = 0; } options.skip += limit; log.debug(`Continuing to fetch variant entries with skip: ${options.skip}`, this.exportConfig?.context ); return await this.variantEntries(options, entries); } if (returnResult) { log.debug('Returning variant entries result...', this.exportConfig?.context ); return { entries }; } } /** * Creates a variant entry. * * @param input - The input data for the variant entry. * @param options - The options for creating the variant entry. * @param apiParams - Additional parameters for the API. * @returns A Promise that resolves to a VariantEntryStruct, a string, or void. */ async createVariantEntry( input: CreateVariantEntryDto, options: CreateVariantEntryOptions, apiParams: Record<string, any>, ): Promise<VariantEntryStruct | string | void> { const { reject, resolve, variantUid } = apiParams; const variantConfig = (this.config as ImportConfig).modules.variantEntry; const { locale = variantConfig.query.locale || 'en-us', variant_id, entry_uid, content_type_uid } = options; log.debug( `Creating variant entry for content type: ${content_type_uid}, entry: ${entry_uid}, variant: ${variant_id}`, this.exportConfig?.context, ); let endpoint = `content_types/${content_type_uid}/entries/${entry_uid}/variants/${variant_id}?locale=${locale}`; const query = this.constructQuery(omit(variantConfig.query, ['locale'])); if (query) { endpoint = endpoint.concat(query); } log.debug(`Making API call to: ${endpoint}`, this.exportConfig?.context); const onSuccess = (response: any) => { resolve({ response, apiData: { variantUid, entryUid: entry_uid }, log }); }; const onReject = (error: any) => { reject({ error, apiData: { variantUid, entryUid: entry_uid }, log, }); }; try { this.apiClient.headers({ api_version: undefined }); const res = await this.apiClient.put<VariantEntryStruct>(endpoint, { entry: input }); const data = await this.handleVariantAPIRes(res); if (res.status >= 200 && res.status < 300) { onSuccess(data); } else { onReject(data); } } catch (error: any) { onReject(error); } } /** * Publishes a variant entry. * * @param input - The input data for publishing the variant entry. * @param options - The options for publishing the variant entry. * @param apiParams - Additional API parameters. * @returns A Promise that resolves to the published variant entry response. */ async publishVariantEntry( input: PublishVariantEntryDto, options: PublishVariantEntryOptions, apiParams: Record<string, any>, ) { const { reject, resolve, variantUid } = apiParams; const { entry_uid, content_type_uid } = options; log.debug( `Publishing variant entry for content type: ${content_type_uid}, entry: ${entry_uid}`, this.exportConfig?.context, ); let endpoint = `content_types/${content_type_uid}/entries/${entry_uid}/publish`; log.debug(`Making API call to: ${endpoint}`, this.exportConfig?.context); const onSuccess = (response: any) => { log.debug(`Variant entry published successfully: ${entry_uid}`, this.exportConfig?.context ); resolve({ response, apiData: { entryUid: entry_uid, variantUid, locales: input.entry.locales }, log }); }; const onReject = (error: any) => { log.debug(`Failed to publish variant entry: ${entry_uid}`, this.exportConfig?.context ); reject({ error, apiData: { entryUid: entry_uid, variantUid, locales: input.entry.locales }, log, }); }; try { this.apiClient.headers({ api_version: 3.2 }); const res = await this.apiClient.post<any>(endpoint, input); const data = await this.handleVariantAPIRes(res); if (res.status >= 200 && res.status < 300) { onSuccess(data); } else { onReject(data); } } catch (error: any) { onReject(error); } } /** * Handles the API response for variant requests. * @param res - The API response object. * @returns The variant API response data. * @throws If the API response status is not within the success range, an error message is thrown. */ async handleVariantAPIRes( res: APIResponse, ): Promise<VariantEntryStruct | { entries: VariantEntryStruct[]; count: number } | string | any> { const { status, data } = res; log.debug(`API response status: ${status}`, this.exportConfig?.context); if (status >= 200 && status < 300) { log.debug('API request successful.', this.exportConfig?.context); return data; } log.debug(`API request failed with status: ${status}`, this.exportConfig?.context); // Refresh the access token if the response status is 401 await authenticationHandler.refreshAccessToken(res); const errorMsg = data?.errors ? formatErrors(data.errors) : data?.error_message || data?.message || data; log.debug(`API error: ${errorMsg}`, this.exportConfig?.context); throw errorMsg; } } export class VariantManagementSDK<T> extends AdapterHelper<T, ContentstackClient> implements VariantInterface<T, ContentstackClient> { public override apiClient!: ContentstackClient; public exportConfig?: any; async init(): Promise<void> { this.apiClient = await managementSDKClient(this.config); } async variantEntry(options: VariantOptions) { // TODO SDK implementation return { entry: {} }; } async variantEntries(options: VariantsOption) { // TODO SDK implementation return { entries: [{}] }; } createVariantEntry( input: CreateVariantEntryDto, options: CreateVariantEntryOptions, apiParams: Record<string, any>, ): Promise<VariantEntryStruct | string | void> { // FIXME placeholder return Promise.resolve({} as VariantEntryStruct); } async handleVariantAPIRes( res: APIResponse, ): Promise<VariantEntryStruct | { entries: VariantEntryStruct[]; count: number } | string> { return res.data; } constructQuery(query: Record<string, any>): string | void { log.debug('ConstructQuery method called (SDK placeholder implementation)', this.exportConfig?.context); } async delay(ms: number): Promise<void> { log.debug(`Delay method called for ${ms}ms (SDK placeholder implementation)`, this.exportConfig?.context); } } export class VariantAdapter<T> { protected variantInstance; public readonly messages: typeof messages; public exportConfig?: any; constructor(config: ContentstackConfig & AnyProperty & AdapterType<T, ContentstackConfig>); constructor(config: APIConfig & AdapterType<T, APIConfig & AnyProperty>, options?: HttpClientOptions); constructor( config: APIConfig & AdapterType<T, (APIConfig & AnyProperty) | ContentstackConfig>, options?: HttpClientOptions, ) { log.debug('Initializing VariantAdapter...', this.exportConfig?.context); if (config.httpClient) { log.debug('Using HTTP client variant instance.', this.exportConfig?.context); const { httpClient, Adapter, ...restConfig } = config; this.variantInstance = new Adapter(restConfig, options); } else { log.debug('Using SDK variant instance.', this.exportConfig?.context); const { Adapter, ...restConfig } = config; this.variantInstance = new Adapter(restConfig); } this.messages = messages; log.debug('VariantAdapter initialized successfully.', this.exportConfig?.context); } } export default VariantAdapter;