@fontoxml/fontoxml-development-tools
Version:
Development tools for Fonto.
246 lines (224 loc) • 6.21 kB
JavaScript
/** @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 });
};
}