@stackbit/cms-contentful
Version:
Stackbit Contentful CMS Interface
470 lines (419 loc) • 17.4 kB
text/typescript
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;
}