import { Config } from '@stackbit/sdk'; import * as StackbitTypes from '@stackbit/types'; import type { ParametersOfCSIMethod, ReturnTypeOfCSIMethod } from '@stackbit/types'; import * as ContentStoreTypes from '../types'; import type { ContentSourceData } from '../types'; import type { BackCompatContentSourceInterface } from './backward-compatibility'; import { getUserContextForSrcType, findContentSourcesDataForTypeOrId, getUserContextForSrcTypeThunk } from '../content-store-utils'; import { createConfigDelegate } from './config-delegate'; import { createDocumentRecursively, getCreateDocumentThunk } from './create-update-csi-docs'; export interface DocumentHookBaseOptions { stackbitConfig: Config | null; contentSourceData: ContentSourceData; getContentSourceDataById: () => Record; user?: ContentStoreTypes.User; logger: StackbitTypes.Logger; } export interface DocumentHookOptions< Action extends 'createDocument' | 'updateDocument' | 'deleteDocument' | 'publishDocuments' | 'unpublishDocuments' | 'archiveDocument' | 'unarchiveDocument' > extends DocumentHookBaseOptions { actionOptions: Parameters>[0]; } export async function createDocumentHooked(options: DocumentHookOptions<'createDocument'>): ReturnTypeOfCSIMethod<'createDocument'> { // If no onDocumentCreate defined in the config, call the original content // source createDocument method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentCreate) { return options.contentSourceData.instance.createDocument(options.actionOptions); } return options.stackbitConfig.onDocumentCreate({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. createDocumentOptions: { ...options.actionOptions }, createDocument: async (actionOptions?: Parameters[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.createDocument({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext, modelMap: options.actionOptions.modelMap }); }, ...getDocumentHookOptions<'createDocument'>(options) }); } export async function updateDocumentHooked(options: DocumentHookOptions<'updateDocument'>): ReturnTypeOfCSIMethod<'updateDocument'> { // If no onDocumentUpdate defined in the config, call the original content // source updateDocument method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentUpdate) { return options.contentSourceData.instance.updateDocument(options.actionOptions); } return options.stackbitConfig.onDocumentUpdate({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. updateDocumentOptions: { ...options.actionOptions }, updateDocument: async (actionOptions?: Parameters[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.updateDocument({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext, modelMap: options.actionOptions.modelMap }); }, ...getDocumentHookOptions<'updateDocument'>(options) }); } export async function deleteDocumentHooked(options: DocumentHookOptions<'deleteDocument'>): ReturnTypeOfCSIMethod<'deleteDocument'> { // If no onDocumentDelete defined in the config, call the original content // source deleteDocument method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentDelete) { return options.contentSourceData.instance.deleteDocument(options.actionOptions); } return options.stackbitConfig.onDocumentDelete({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. deleteDocumentOptions: { ...options.actionOptions }, deleteDocument: async (actionOptions?: Parameters[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.deleteDocument({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext }); }, ...getDocumentHookOptions<'deleteDocument'>(options) }); } export async function publishDocumentHooked(options: DocumentHookOptions<'publishDocuments'>): ReturnTypeOfCSIMethod<'publishDocuments'> { // If no onDocumentsPublish defined in the config, call the original content // source publishDocuments method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentsPublish) { return options.contentSourceData.instance.publishDocuments(options.actionOptions); } return options.stackbitConfig.onDocumentsPublish({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. publishDocumentsOptions: { ...options.actionOptions }, publishDocuments: async (actionOptions?: Parameters[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.publishDocuments({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext }); }, ...getDocumentHookOptions<'publishDocuments'>(options) }); } export async function unpublishDocumentHooked(options: DocumentHookOptions<'unpublishDocuments'>): ReturnTypeOfCSIMethod<'unpublishDocuments'> { // If no onDocumentsUnpublish defined in the config, call the original content // source publishDocuments method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentsUnpublish) { return options.contentSourceData.instance.unpublishDocuments?.(options.actionOptions); } return options.stackbitConfig.onDocumentsUnpublish({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. unpublishDocumentsOptions: { ...options.actionOptions }, unpublishDocuments: async (actionOptions?: ParametersOfCSIMethod<'unpublishDocuments'>[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.unpublishDocuments?.({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext }); }, ...getDocumentHookOptions<'unpublishDocuments'>(options) }); } export async function archiveDocumentHooked(options: DocumentHookOptions<'archiveDocument'>): ReturnTypeOfCSIMethod<'archiveDocument'> { // If no onDocumentArchive defined in the config, call the original content // source archiveDocument method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentArchive) { return options.contentSourceData.instance.archiveDocument?.(options.actionOptions); } return options.stackbitConfig.onDocumentArchive({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. archiveDocumentOptions: { ...options.actionOptions }, archiveDocument: async (actionOptions?: ParametersOfCSIMethod<'archiveDocument'>[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.archiveDocument?.({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext }); }, ...getDocumentHookOptions<'archiveDocument'>(options) }); } export async function unarchiveDocumentHooked(options: DocumentHookOptions<'unarchiveDocument'>): ReturnTypeOfCSIMethod<'unarchiveDocument'> { // If no onDocumentUnarchive defined in the config, call the original content // source unarchiveDocument method with the provided actionOptions. if (!options.stackbitConfig?.onDocumentUnarchive) { return options.contentSourceData.instance.unarchiveDocument?.(options.actionOptions); } return options.stackbitConfig.onDocumentUnarchive({ // Spread actionOptions to clone them shallowly to prevent accidental // property overrides within user methods. unarchiveDocumentOptions: { ...options.actionOptions }, unarchiveDocument: async (actionOptions?: ParametersOfCSIMethod<'unarchiveDocument'>[0]) => { if (!actionOptions) { actionOptions = options.actionOptions; } return options.contentSourceData.instance.unarchiveDocument?.({ ...actionOptions, userContext: actionOptions.userContext ?? options.actionOptions.userContext }); }, ...getDocumentHookOptions<'unarchiveDocument'>(options) }); } function getDocumentHookOptions< Action extends 'createDocument' | 'updateDocument' | 'deleteDocument' | 'publishDocuments' | 'unpublishDocuments' | 'archiveDocument' | 'unarchiveDocument' >(options: DocumentHookOptions): StackbitTypes.DocumentHookBaseOptions { const srcType = options.contentSourceData.srcType; const srcProjectId = options.contentSourceData.srcProjectId; const contentSourceDataById = options.getContentSourceDataById(); const configDelegate = createConfigDelegate({ contentSourceDataById, logger: options.logger }); return { srcType: srcType, srcProjectId: srcProjectId, contentSourceActions: getContentSourceActionsForContentSourceData({ contentSourceData: options.contentSourceData, user: options.user, stackbitConfig: options.stackbitConfig, getContentSourceDataById: options.getContentSourceDataById, logger: options.logger }), getContentSourceActionsForSource: getContentSourceActionsForSourceThunk({ getContentSourceDataById: options.getContentSourceDataById, logger: options.logger, user: options.user, stackbitConfig: options.stackbitConfig }), getUserContextForContentSourceType: getUserContextForSrcTypeThunk(options.user), ...configDelegate }; } export function getContentSourceActionsForSourceThunk({ getContentSourceDataById, logger, user, stackbitConfig }: { getContentSourceDataById: () => Record; logger: StackbitTypes.Logger; user?: ContentStoreTypes.User; stackbitConfig: Config | null; }): (options: { srcType: string; srcProjectId?: string }) => StackbitTypes.ContentSourceActions | undefined { return (options: { srcType: string; srcProjectId?: string }) => { return getContentSourceActionsForSource({ logger: logger, srcType: options.srcType, srcProjectId: options.srcProjectId, user: user, stackbitConfig, getContentSourceDataById }); }; } function getContentSourceActionsForSource({ srcType, srcProjectId, logger, user, stackbitConfig, getContentSourceDataById }: { logger: StackbitTypes.Logger; srcType: string; srcProjectId?: string; user?: ContentStoreTypes.User; stackbitConfig: Config | null; getContentSourceDataById: () => Record; }): StackbitTypes.ContentSourceActions | undefined { const contentSourcesData = findContentSourcesDataForTypeOrId({ contentSourceDataById: getContentSourceDataById(), srcType, srcProjectId }); if (contentSourcesData.length === 0) { logger.warn(`The getContentSourceActionsForContentSource() did not find content sources for srcType: '${srcType}', srcProjectId:'${srcProjectId}'.`); return undefined; } else if (contentSourcesData.length > 1) { logger.warn( `The getContentSourceActionsForContentSource() found more than one content sources for '${srcType}'. ` + `Please specify 'srcType' and 'srcProjectId' to narrow down the search.` ); return undefined; } const contentSourceData = contentSourcesData[0]!; return getContentSourceActionsForContentSourceData({ contentSourceData, user, stackbitConfig, getContentSourceDataById, logger }); } function getContentSourceActionsForContentSourceData({ contentSourceData, user, stackbitConfig, getContentSourceDataById, logger }: { contentSourceData: ContentSourceData; user?: ContentStoreTypes.User; stackbitConfig: Config | null; getContentSourceDataById: () => Record; logger: StackbitTypes.Logger; }): StackbitTypes.ContentSourceActions { return { createDocument: async (options: Parameters[0]) => { return createDocumentHooked({ actionOptions: { ...options, userContext: options.userContext ?? getUserContextForSrcType(contentSourceData.srcType, user), modelMap: contentSourceData.csiModelMap }, contentSourceData, user, stackbitConfig, getContentSourceDataById, logger }); }, createDocumentFromObject: async (options: Parameters[0]) => { return createDocumentRecursively({ object: options.object, locale: options.locale, modelName: options.modelName, contentSourceId: contentSourceData.id, contentSourceDataById: getContentSourceDataById(), assetSources: stackbitConfig?.assetSources ?? [], userLogger: logger, createDocument: getCreateDocumentThunk({ stackbitConfig, getContentSourceDataById, logger, user }) }); }, updateDocument: (options: Parameters[0]) => { return updateDocumentHooked({ actionOptions: { ...options, userContext: options.userContext ?? getUserContextForSrcType(contentSourceData.srcType, user), modelMap: contentSourceData.csiModelMap }, contentSourceData, user, stackbitConfig, getContentSourceDataById, logger }); }, deleteDocument: (options: Parameters[0]) => { return deleteDocumentHooked({ actionOptions: { ...options, userContext: options.userContext ?? getUserContextForSrcType(contentSourceData.srcType, user) }, contentSourceData, user, stackbitConfig, getContentSourceDataById, logger }); }, publishDocuments: (options: Parameters[0]) => { return publishDocumentHooked({ actionOptions: { ...options, userContext: options.userContext ?? getUserContextForSrcType(contentSourceData.srcType, user) }, contentSourceData, user, stackbitConfig, getContentSourceDataById, logger }); } }; }