UNPKG

@fontoxml/fontoxml-development-tools

Version:

Development tools for Fonto.

246 lines (224 loc) 6.21 kB
/** @typedef {import('../../src/getAppConfig.js').DevCmsConfig} DevCmsConfig */ /** @typedef {import('./stores/DevelopmentCms')} DevelopmentCms */ /** * @typedef DocumentToSave * * @property {boolean} [autosave] * @property {DocumentContext} [documentContext] * @property {string} documentId * @property {string} [revisionId] * @property {string} content * @property {{ [key: string]: unknown }} [metadata] */ /** * @typedef Lock * * @property {boolean} isLockAcquired * @property {boolean} isLockAvailable */ /** * @typedef DocumentContext * * @property {{ [key: string]: unknown }} [documentMetadata] * @property {boolean} [isLockAcquired] */ /** * @typedef ResultBody * * @property {DocumentContext} [documentContext] * @property {string} [revisionId] * @property {Lock} [lock] */ /** * @typedef ErrorResultBody * * @property {string} [revisionId] * @property {Lock} [lock] */ /** * @typedef DocumentErrorResult * * @property {string} documentId * @property {number} status * @property {ErrorResultBody} [body] */ /** * @typedef DocumentResult * * @property {string} documentId * @property {200} status * @property {ResultBody} body */ /** * @param {DevCmsConfig} config */ export default function configureDocumentPutPostRouteHandler(config) { /** * @param {DocumentToSave} documentToSave * @param {{ editSessionToken: string, user: {}}} currentSession * @param {DevelopmentCms} cms * * @return {Promise<DocumentResult | DocumentErrorResult>} */ async function saveDocument(documentToSave, currentSession, cms) { const documentId = documentToSave.documentId; try { // Make sure the existing content can be loaded. await new Promise((resolve, reject) => { cms.load( documentId, currentSession.editSessionToken, (error, content) => { if (error) { reject(error); return; } resolve(content); }, ); }); const existingRevisionId = await new Promise((resolve, reject) => { cms.getLatestRevisionId( documentId, currentSession.editSessionToken, (error, revisionId) => { if (error) { reject(error); return; } resolve(revisionId); }, ); }); const documentLoadLock = { ...config.documentLoadLock, ...config.documentLoadLockOverrides[documentId], }; /** @type {DocumentContext} */ const documentContext = documentToSave.documentContext || {}; const currentState = { lock: { isLockAcquired: documentContext.isLockAcquired !== undefined ? documentLoadLock.isLockAvailable && documentContext.isLockAcquired : documentLoadLock.isLockAcquired, isLockAvailable: documentLoadLock.isLockAvailable, reason: !documentLoadLock.isLockAvailable ? documentLoadLock.lockReason : undefined, }, revisionId: existingRevisionId, }; if (!currentState.lock.isLockAvailable) { // Not available has to mean not acquired, so 412 is the correct response. // Sending 403 here would cause the document to be considered inaccessible // instead of merely having an unavailable lock. return { documentId, status: 412, body: currentState, }; } if ( documentToSave.revisionId && documentToSave.revisionId !== currentState.revisionId ) { return { documentId, status: 412, body: currentState, }; } if (!currentState.lock.isLockAcquired) { // Saving without having a lock is allowed by the batch save endpoint. } const newContent = documentToSave.content; await new Promise((resolve, reject) => { cms.save(documentId, newContent, currentSession, (error) => { if (error) { reject(error); return; } resolve(); }); }); documentContext.documentMetadata = documentToSave.metadata; // Retrieve the latest revision id to return to the client. const newRevisionId = await new Promise((resolve, reject) => { cms.getLatestRevisionId( documentId, currentSession.editSessionToken, (error, revisionId) => { if (error) { reject(error); return; } resolve(revisionId); }, ); }); return { documentId, status: 200, body: { revisionId: newRevisionId, documentContext, }, }; } catch (error) { return { documentId, status: error.status === 404 ? 404 : 500, }; } } return async (req, res) => { const currentSession = req.getFontoSession( req.body?.context?.editSessionToken, ); // Sanity check. A batch may never contain the same document twice. It is both a problem for // parallel processing, and it needs logic to only return one of them in the results array. // Even though this can be solved on the backend, it is not the kind of complexity we want // each implementation of this endpoints to be required to solve. /** @type {DocumentToSave[]} */ const documentsToSave = []; const documentIds = new Set(); for (const documentToSave of req.body.documents) { if ( typeof config.documentSaveBatchResultsLimit === 'number' && documentsToSave.length >= config.documentSaveBatchResultsLimit ) { break; } if (documentIds.has(documentToSave.documentId)) { res .status(500) .send( new Error( 'Error while processing request, due to the request containing duplicate documents.', ), ); return; } documentsToSave.push(documentToSave); documentIds.add(documentToSave.documentId); } /** @type {(DocumentResult | DocumentErrorResult)[]} */ let results; try { results = await Promise.all( documentsToSave.map((documentToSave) => saveDocument(documentToSave, currentSession, req.cms), ), ); } catch (error) { res.status(500).send(error); return; } res .status(200) .set('content-type', 'application/json; charset=utf-8') .json({ results }); }; }