UNPKG

@fontoxml/fontoxml-development-tools

Version:

Development tools for Fonto.

229 lines (206 loc) 6.18 kB
import asyncRouteWithLockCleanupHandler from '../asyncRouteWithLockCleanupHandler.js'; /** @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 * @param {(filePath: string) => Promise<DevCmsFileLock>} acquireLock * * @return {Promise<DocumentResult | DocumentErrorResult>} */ async function saveDocument( documentToSave, currentSession, cms, acquireLock, ) { const documentId = documentToSave.documentId; let fileLock; try { if (documentId && documentId.indexOf('..') !== -1) { return { documentId, status: 403, }; } // Make sure the existing content can be loaded. fileLock = await acquireLock(documentId); const contentAndLatestRevisionId = await cms.getFileAndLatestRevisionId( documentId, currentSession.editSessionToken, fileLock, ); if (!contentAndLatestRevisionId) { return { documentId, status: 404, }; } 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: contentAndLatestRevisionId.revisionId, }; 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; const newRevisionId = await cms.updateFile( documentId, newContent, currentSession.user, currentSession.editSessionToken, fileLock, ); documentContext.isLockAcquired = currentState.lock.isLockAcquired; documentContext.documentMetadata = documentToSave.metadata; return { documentId, status: 200, body: { revisionId: newRevisionId, documentContext, // lock: currentState.lock, }, }; } catch (error) { if (error.statusCode === 404) { // Should not happen, as we already checked the existence of the document. return { documentId, status: 404, }; } } finally { fileLock?.release(); } } return asyncRouteWithLockCleanupHandler(async (acquireLock, 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)) { // Technically this is a 400 due tue the request being incorrect, however we only // have status code 500 in the API spec, and not status code 400. So throw, which // is handled as a 500. throw new Error( 'Error while processing request, due to the request containing duplicate documents.', ); } documentsToSave.push(documentToSave); documentIds.add(documentToSave.documentId); } const results = await Promise.all( documentsToSave.map((documentToSave) => saveDocument(documentToSave, currentSession, req.cms, acquireLock), ), ); res .status(200) .set('content-type', 'application/json; charset=utf-8') .json({ results }); }); }