UNPKG

@stackbit/cms-contentful

Version:

Stackbit Contentful CMS Interface

470 lines (419 loc) 17.4 kB
import _ from 'lodash'; import { createClient, PlainClientAPI, EntryProps, AssetProps, AssetFileProp, LocaleProps, QueryParams, QueryOptions, CollectionProp, UserProps, ContentTypeProps, EditorInterfaceProps, AppInstallationProps, CreateEntryProps, CreateAssetProps, GetSpaceEnvironmentParams, BulkActionPayload, BulkActionValidatePayload, BulkActionPublishPayload, GetBulkActionParams, BulkActionProps, WebhookProps, CreateWebhooksProps, ReleaseProps, ScheduledActionProps, CursorPaginatedCollectionProp, ReleasePayload, ReleaseQueryOptions, BulkActionUnpublishPayload } from 'contentful-management'; import { Logger } from '@stackbit/types'; import { convertScheduledAction, findScheduleForRelease } from './contentful-scheduled-actions-converter'; export { EntryProps, AssetProps, LocaleProps, UserProps } from 'contentful-management'; interface PlainApiClientOptions { accessToken: string; spaceId: string; environment: string; managementHost?: string; uploadHost?: string; } export function createPlainApiClient({ accessToken, spaceId, environment, managementHost, uploadHost }: PlainApiClientOptions) { return createClient( { accessToken: accessToken, ...(managementHost ? { host: managementHost } : null), ...(uploadHost ? { hostUpload: uploadHost } : null) }, { type: 'plain', defaults: { spaceId: spaceId, environmentId: environment } } ); } export async function fetchEntryById(client: PlainClientAPI, entryId: string): Promise<EntryProps> { return client.entry.get({ entryId }); } export async function fetchAssetById(client: PlainClientAPI, assetId: string): Promise<AssetProps> { return client.asset.get({ assetId }); } export async function createEntry(client: PlainClientAPI, contentTypeId: string, data: CreateEntryProps): Promise<EntryProps> { return client.entry.create({ contentTypeId }, data); } export async function updateEntry(client: PlainClientAPI, entryId: string, entry: EntryProps): Promise<EntryProps> { return client.entry.update({ entryId }, entry); } export async function publishEntry(client: PlainClientAPI, entry: EntryProps): Promise<EntryProps> { return client.entry.publish({ entryId: entry.sys.id }, entry); } export async function unpublishEntry(client: PlainClientAPI, entryId: string) { return client.entry.unpublish({ entryId }); } export async function archiveEntry(client: PlainClientAPI, entryId: string) { return client.entry.archive({ entryId }); } export async function unarchiveEntry(client: PlainClientAPI, entryId: string) { return client.entry.unarchive({ entryId }); } export async function deleteEntry(client: PlainClientAPI, entryId: string) { return client.entry.delete({ entryId }); } export async function createAsset(client: PlainClientAPI, data: CreateAssetProps): Promise<AssetProps> { return client.asset.create({}, data); } export async function updateAsset(client: PlainClientAPI, assetId: string, asset: AssetProps): Promise<AssetProps> { return client.asset.update({ assetId }, asset); } export async function createAssetFromFile(client: PlainClientAPI, data: Omit<AssetFileProp, 'sys'>): Promise<AssetProps> { return client.asset.createFromFiles({}, data); } export async function processAssetForAllLocales(client: PlainClientAPI, data: AssetProps): Promise<AssetProps> { return client.asset.processForAllLocales({}, data); } export async function publishAsset(client: PlainClientAPI, data: AssetProps): Promise<AssetProps> { return client.asset.publish({ assetId: data.sys.id }, data); } export async function createValidateBulkAction( client: PlainClientAPI, options: GetSpaceEnvironmentParams, data: BulkActionValidatePayload ): Promise<BulkActionProps<BulkActionValidatePayload>> { return client.bulkAction.validate(options, data); } export async function createPublishBulkAction( client: PlainClientAPI, options: GetSpaceEnvironmentParams, data: BulkActionPublishPayload ): Promise<BulkActionProps<BulkActionPublishPayload>> { return client.bulkAction.publish(options, data); } export async function createUnpublishBulkAction( client: PlainClientAPI, options: GetSpaceEnvironmentParams, data: BulkActionUnpublishPayload ): Promise<BulkActionProps<BulkActionUnpublishPayload>> { return client.bulkAction.unpublish(options, data); } export async function createWebhook(client: PlainClientAPI, data: CreateWebhooksProps) { return client.webhook.create({}, data); } export async function getBulkAction<T extends BulkActionPayload>(client: PlainClientAPI, data: GetBulkActionParams): Promise<BulkActionProps<T>> { return client.bulkAction.get<T>(data); } export async function fetchEntriesByIds(client: PlainClientAPI, entryIds: string[], query?: QueryOptions): Promise<EntryProps[]> { const entryIdChunks = _.chunk(entryIds, 100); let entries: EntryProps[] = []; for (const entryIdsChunk of entryIdChunks) { const result = await client.entry.getMany({ query: { 'sys.id[in]': entryIdsChunk.join(','), limit: 1000, ...query } }); entries = entries.concat(result.items); } return entries; } export async function fetchAssetsByIds(client: PlainClientAPI, assetIds: string[], query?: QueryOptions): Promise<AssetProps[]> { const assetIdChunks = _.chunk(assetIds, 100); let assets: AssetProps[] = []; for (const entryIdsChunk of assetIdChunks) { const result = await client.asset.getMany({ query: { 'sys.id[in]': entryIdsChunk.join(','), limit: 1000, ...query } }); assets = assets.concat(result.items); } return assets; } export async function fetchAllEntries(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<EntryProps>({ getMany: client.entry.getMany, logger }); } export async function fetchEntriesUpdatedAfter(client: PlainClientAPI, date: string, logger?: Logger) { return fetchAllItems<EntryProps>({ getMany: client.entry.getMany, logger, query: { 'sys.updatedAt[gt]': date } }); } export async function fetchAllAssets(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<AssetProps>({ getMany: client.asset.getMany, logger }); } export async function fetchAssetsUpdatedAfter(client: PlainClientAPI, date: string, logger?: Logger) { return fetchAllItems<AssetProps>({ getMany: client.asset.getMany, logger, query: { 'sys.updatedAt[gt]': date } }); } export async function fetchAllLocales(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<LocaleProps>({ getMany: client.locale.getMany, logger }); } export async function fetchAllUsers(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<UserProps>({ getMany: client.user.getManyForSpace, logger }); } export async function fetchAllContentTypes(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<ContentTypeProps>({ getMany: client.contentType.getMany, logger }); } export async function hasContentTypesUpdatedAfter(client: PlainClientAPI, date: string, logger?: Logger): Promise<boolean> { const updatedContentTypes = await fetchAllItems<ContentTypeProps>({ getMany: client.contentType.getMany, logger, query: { 'sys.updatedAt[gt]': date, limit: 1 } }); return updatedContentTypes.length > 0; } export async function fetchContentTypesUpdatedAfter(client: PlainClientAPI, date: string, logger?: Logger) { return fetchAllItems<ContentTypeProps>({ getMany: client.contentType.getMany, logger, query: { 'sys.updatedAt[gt]': date } }); } export async function fetchAllEditorInterfaces(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<EditorInterfaceProps>({ getMany: client.editorInterface.getMany, logger }); } export async function fetchAllAppInstallations(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<AppInstallationProps>({ getMany: client.appInstallation.getMany, logger }); } export async function fetchAllWebhooks(client: PlainClientAPI, logger?: Logger) { return fetchAllItems<WebhookProps>({ getMany: client.webhook.getMany, logger }); } export async function fetchAllReleases(client: PlainClientAPI, logger?: Logger) { return fetchAllItemsCursor<ReleaseProps>(client.release.query, {}, logger); } export async function fetchAllSchedules(client: PlainClientAPI, params: { environment: string; entityId?: string; status?: string }, logger?: Logger) { return fetchAllItemsCursor<ScheduledActionProps>( client.scheduledActions.getMany, { 'environment.sys.id': params.environment, 'entity.sys.id': params.entityId, 'sys.status': params.status }, logger ); } export async function fetchScheduleById(client: PlainClientAPI, params: { environment: string; scheduleId: string }) { return client.scheduledActions.get({ scheduledActionId: params.scheduleId, environmentId: params.environment }); } export async function fetchEntityVersions(client: PlainClientAPI, params: { environment: string; entityId: string }) { return client.snapshot.getManyForEntry({ entryId: params.entityId, environmentId: params.environment }); } export async function fetchEntityVersion(client: PlainClientAPI, params: { environment: string; entityId: string; versionId: string }) { return client.snapshot.getForEntry({ entryId: params.entityId, snapshotId: params.versionId, environmentId: params.environment }); } export async function createScheduledAction( client: PlainClientAPI, options: { documentIds: string[]; name: string; executeAt: string; action: ScheduledActionProps['action']; environment: string } ) { const contentfulRelease = await client.release.create( {}, { entities: { sys: { type: 'Array' }, items: options.documentIds.map((docId) => ({ sys: { linkType: 'Entry', type: 'Link', id: docId } })) }, title: options.name } ); const contentfulSchedule = await client.scheduledActions.create( {}, { entity: { sys: { type: 'Link', linkType: 'Release', id: contentfulRelease.sys.id } }, environment: { sys: { type: 'Link', linkType: 'Environment', id: options.environment } }, scheduledFor: { datetime: options.executeAt }, action: options.action } ); return { schedule: convertScheduledAction(contentfulRelease, contentfulSchedule), contentfulRelease, contentfulSchedule }; } export async function updateScheduledAction( client: PlainClientAPI, scheduledActionId: string, options: { documentIds?: string[]; name?: string; executeAt?: string; environment: string } ) { let contentfulRelease = await client.release.get({ releaseId: scheduledActionId }); if (!contentfulRelease) { throw new Error('Contentful release not found for scheduled action'); } const releaseUpdateObj: ReleasePayload = { ..._.pick(contentfulRelease, ['title', 'entities']), ...(options.documentIds ? { entities: { sys: { type: 'Array' }, items: options.documentIds.map((docId) => ({ sys: { linkType: 'Entry', type: 'Link', id: docId } })) } } : {}), ...(options.name ? { title: options.name } : {}) }; contentfulRelease = await client.release.update({ releaseId: scheduledActionId, version: contentfulRelease.sys.version }, releaseUpdateObj); const contentfulSchedules = await fetchAllSchedules(client, { environment: options.environment, entityId: contentfulRelease.sys.id, status: 'scheduled' }); const releaseSchedule = findScheduleForRelease(_.sortBy(contentfulSchedules, ['scheduledFor.datetime'])); if (!releaseSchedule) { throw new Error('Contentful schedule not found for scheduled action, or schedule is already executed or cancelled'); } const scheduleUpdateObj: Pick<ScheduledActionProps, 'action' | 'entity' | 'environment' | 'scheduledFor'> = { ..._.pick(releaseSchedule, ['action', 'entity', 'environment']), scheduledFor: { ...releaseSchedule.scheduledFor, ...(options.executeAt ? { datetime: options.executeAt } : {}) } }; const contentfulSchedule = await client.scheduledActions.update( { scheduledActionId: releaseSchedule.sys.id, version: releaseSchedule.sys.version }, scheduleUpdateObj ); return convertScheduledAction(contentfulRelease, contentfulSchedule); } export async function cancelScheduledAction(client: PlainClientAPI, scheduledActionId: string, options: { environment: string }) { const contentfulRelease = await client.release.get({ releaseId: scheduledActionId }); if (!contentfulRelease) { throw new Error('Contentful release not found for scheduled action'); } const contentfulSchedules = await fetchAllSchedules(client, { environment: options.environment, entityId: contentfulRelease.sys.id, status: 'scheduled' }); let releaseSchedule = findScheduleForRelease(_.sortBy(contentfulSchedules, ['scheduledFor.datetime'])); if (!releaseSchedule) { throw new Error('Contentful schedule not found for scheduled action'); } releaseSchedule = await client.scheduledActions.delete({ scheduledActionId: releaseSchedule.sys.id, environmentId: options.environment }); return convertScheduledAction(contentfulRelease, releaseSchedule); } export async function getScheduledAction(client: PlainClientAPI, scheduledActionId: string, options: { environment: string }) { const contentfulRelease = await client.release.get({ releaseId: scheduledActionId }); if (!contentfulRelease) { return null; } const contentfulSchedules = await fetchAllSchedules(client, { environment: options.environment, entityId: contentfulRelease.sys.id }); const releaseSchedule = findScheduleForRelease(_.sortBy(contentfulSchedules, ['scheduledFor.datetime'])); if (!releaseSchedule) { return null; } return convertScheduledAction(contentfulRelease, releaseSchedule); } export async function fetchAllItems<T>({ getMany, logger, query }: { getMany: (params: QueryParams) => Promise<CollectionProp<T>>; logger?: Logger; query?: QueryOptions; }): Promise<T[]> { let limit = 1000; let items: T[] = []; let hasMoreItems = true; let retries = 0; while (hasMoreItems) { try { const result = await getMany({ query: { skip: items.length, limit: limit, ...query } }); items = items.concat(result.items); hasMoreItems = result.total > items.length && result.items.length === limit; retries = 0; limit = 1000; if (result.total > 1000) { logger?.info(`Fetched ${items.length} of ${result.total} items`); } } catch (err: any) { if (retries > 4) { throw err; } if (_.get(err, 'message')?.includes('Response size too big')) { limit = Math.trunc(limit / 2); } else { throw err; } retries++; } } return items; } export async function fetchAllItemsCursor<T>( fn: (params: { query?: ReleaseQueryOptions }) => Promise<CursorPaginatedCollectionProp<T>>, query: any, logger?: Logger ): Promise<T[]> { // Contentful didn't do a good job of typing scheduledActions.getMany, so the type names here are specific to Release, // but apply to ScheduledActions as well, as both are implemented as cursorPaginatedCollections let limit = 1000; let items: T[] = []; let hasMoreItems = true; let pageNext = undefined; let retries = 0; while (hasMoreItems) { try { const result: CursorPaginatedCollectionProp<T> = await fn({ query: { ...query, pageNext, limit } }); items = items.concat(result.items); hasMoreItems = !!result.pages?.next; const nextUrlParams = result.pages?.next?.split('?')[1]; pageNext = new URLSearchParams(nextUrlParams).get('pageNext') ?? undefined; retries = 0; limit = 1000; logger?.info(`Fetched ${items.length} items`); } catch (err: any) { if (retries > 4) { throw err; } if (_.get(err, 'message')?.includes('Response size too big')) { limit = Math.trunc(limit / 2); } else { throw err; } retries++; } } return items; }