UNPKG

@webiny/api-page-builder-so-ddb

Version:

The DynamoDB storage operations Webiny Page Builder API.

751 lines (740 loc) 20 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.createPageStorageOperations = void 0; var _error = _interopRequireDefault(require("@webiny/error")); var _get = require("@webiny/db-dynamodb/utils/get"); var _cleanup = require("@webiny/db-dynamodb/utils/cleanup"); var _dbDynamodb = require("@webiny/db-dynamodb"); var _PageDynamoDbFieldPlugin = require("../../plugins/definitions/PageDynamoDbFieldPlugin"); var _keys = require("./keys"); const GSI1_INDEX = "GSI1"; /** * To be able to efficiently query pages we need the following records in the database: * - latest * PK - fixed string + #L * SK - pageId * - revision * PK - fixed string + pageId * SK - version * - published * PK - fixed string + #P * SK - pageId * - path * PK - fixed string + #PATH * SK - path */ const createRevisionType = () => { return "pb.page"; }; /** * Type that marks latest page record. */ const createLatestType = () => { return "pb.page.l"; }; /** * Type that marks published page record. */ const createPublishedType = () => { return "pb.page.p"; }; const createPageStorageOperations = params => { const { entity, plugins } = params; const create = async params => { const { page } = params; const revisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(page), SK: (0, _keys.createRevisionSortKey)(page) }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; const publishedKeys = { PK: (0, _keys.createPublishedPartitionKey)(page), SK: (0, _keys.createPublishedSortKey)(page) }; const titleLC = page.title.toLowerCase(); /** * We need to create * - latest * - revision */ const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, put: [{ ...page, titleLC, ...latestKeys, TYPE: createLatestType() }, { ...page, titleLC, ...revisionKeys, TYPE: createRevisionType() }] }); if (page.status === "published") { entityBatch.put({ ...page, ...publishedKeys, GSI1_PK: (0, _keys.createPathPartitionKey)(page), GSI1_SK: page.path, TYPE: createPublishedType() }); } try { await entityBatch.execute(); return page; } catch (ex) { throw new _error.default(ex.message || "Could not create new page.", ex.code || "CREATE_PAGE_ERROR", { revisionKeys, latestKeys, page }); } }; const createFrom = async params => { const { page, latestPage, original } = params; const revisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(page), SK: (0, _keys.createRevisionSortKey)(page) }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; /** * We need to create * - latest * - revision */ const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, put: [{ ...page, ...latestKeys, TYPE: createLatestType() }, { ...page, ...revisionKeys, TYPE: createRevisionType() }] }); try { await entityBatch.execute(); return page; } catch (ex) { throw new _error.default(ex.message || "Could not create new page from existing page.", ex.code || "CREATE_PAGE_FROM_ERROR", { revisionKeys, latestKeys, latestPage, original, page }); } }; const update = async params => { const { original, page } = params; const revisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(page), SK: (0, _keys.createRevisionSortKey)(page) }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; const latestPage = await (0, _get.getClean)({ entity, keys: latestKeys }); const titleLC = page.title.toLowerCase(); /** * We need to update * - revision * - latest if this is the latest */ const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, put: [{ ...page, titleLC, ...revisionKeys, TYPE: createRevisionType() }] }); /** * Latest if it is the one. */ if (latestPage && latestPage.id === page.id) { entityBatch.put({ ...page, titleLC, ...latestKeys, TYPE: createLatestType() }); } try { await entityBatch.execute(); return page; } catch (ex) { throw new _error.default(ex.message || "Could not update existing page.", ex.code || "UPDATE_PAGE_ERROR", { original, page, latestPage, latestKeys, revisionKeys }); } }; const deleteOne = async params => { const { page, latestPage, publishedPage } = params; const revisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(page), SK: (0, _keys.createRevisionSortKey)(page) }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; const publishedKeys = { PK: (0, _keys.createPublishedPartitionKey)(page), SK: (0, _keys.createPublishedSortKey)(page) }; /** * We need to delete * - revision * - published if is published * We need to update * - latest, if it exists, with previous record */ const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, delete: [revisionKeys] }); if (publishedPage && publishedPage.id === page.id) { entityBatch.delete(publishedKeys); } let previousLatestPage = null; if (latestPage && latestPage.id === page.id) { const partitionKey = (0, _keys.createRevisionPartitionKey)(page); const previousLatestRecord = await (0, _dbDynamodb.queryOne)({ entity, partitionKey, options: { lt: (0, _keys.createRevisionSortKey)(latestPage), reverse: true } }); if (previousLatestRecord) { entityBatch.put({ ...previousLatestRecord, ...latestKeys, TYPE: createLatestType() }); previousLatestPage = (0, _cleanup.cleanupItem)(entity, previousLatestRecord); } } try { await entityBatch.execute(); } catch (ex) { throw new _error.default(ex.message || "Could not batch write all the page records.", ex.code || "BATCH_WRITE_RECORDS_ERROR"); } return [page, previousLatestPage]; }; /** * We need to delete * - latest * - published * - path * - all revisions */ const deleteAll = async params => { const { page } = params; const partitionKey = (0, _keys.createRevisionPartitionKey)(page); const queryAllParams = { entity, partitionKey, options: { gte: " " } }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; const publishedKeys = { PK: (0, _keys.createPublishedPartitionKey)(page), SK: (0, _keys.createPublishedSortKey)(page) }; const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, delete: [latestKeys] }); let revisions = []; try { revisions = await (0, _dbDynamodb.queryAll)(queryAllParams); } catch (ex) { throw new _error.default(ex.message || "Could not query for all revisions of the page.", ex.code || "LIST_REVISIONS_ERROR", { params: queryAllParams }); } let deletedPublishedRecord = false; /** * We need to go through all possible entries and delete them. * Also, delete the published entry path record. */ for (const revision of revisions) { if (!deletedPublishedRecord && revision.status === "published") { entityBatch.delete(publishedKeys); deletedPublishedRecord = true; } entityBatch.delete({ PK: revision.PK, SK: revision.SK }); } try { await entityBatch.execute(); } catch (ex) { throw new _error.default(ex.message || "Could not delete all the page records.", ex.code || "DELETE_RECORDS_ERROR"); } return [page]; }; /** * - update the revision record * - if the revision being published is also the "latest" revision, update the "latest" record * - set the status of the previously published revision to "unpublished" * - create/update the "published" record */ const publish = async params => { const { page, latestPage, publishedPage } = params; page.status = "published"; const revisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(page), SK: (0, _keys.createRevisionSortKey)(page) }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; const publishedKeys = { PK: (0, _keys.createPublishedPartitionKey)(page), SK: (0, _keys.createPublishedSortKey)(page) }; /** * Update the given revision of the page. */ const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, put: [{ ...page, ...revisionKeys, TYPE: createRevisionType() }] }); if (latestPage.id === page.id) { entityBatch.put({ ...page, ...latestKeys, TYPE: createLatestType() }); } /** * If we already have a published revision, and it's not the revision being published: * - set the existing published revision to "unpublished" */ if (publishedPage && publishedPage.id !== page.id) { const publishedRevisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(publishedPage), SK: (0, _keys.createRevisionSortKey)(publishedPage) }; entityBatch.put({ ...publishedPage, status: "unpublished", ...publishedRevisionKeys, TYPE: createRevisionType() }); } entityBatch.put({ ...page, ...publishedKeys, GSI1_PK: (0, _keys.createPathPartitionKey)(page), GSI1_SK: page.path, TYPE: createPublishedType() }); try { await entityBatch.execute(); } catch (ex) { throw new _error.default(ex.message || "Could not update all the page records when publishing.", ex.code || "UPDATE_RECORDS_ERROR"); } return page; }; /** * We need to * - update the revision record * - remove the "published" record * - if the revision being unpublished is also the "latest" revision, update the "latest" record with the new status */ const unpublish = async params => { const { page, latestPage } = params; page.status = "unpublished"; const revisionKeys = { PK: (0, _keys.createRevisionPartitionKey)(page), SK: (0, _keys.createRevisionSortKey)(page) }; const latestKeys = { PK: (0, _keys.createLatestPartitionKey)(page), SK: (0, _keys.createLatestSortKey)(page) }; const publishedKeys = { PK: (0, _keys.createPublishedPartitionKey)(page), SK: (0, _keys.createPublishedSortKey)(page) }; const entityBatch = (0, _dbDynamodb.createEntityWriteBatch)({ entity, put: [{ ...page, ...revisionKeys, TYPE: createRevisionType() }], delete: [publishedKeys] }); if (latestPage.id === page.id) { entityBatch.put({ ...page, ...latestKeys, TYPE: createLatestType() }); } try { await entityBatch.execute(); } catch (ex) { throw new _error.default(ex.message || "Could not update all the page records when unpublishing.", ex.code || "UPDATE_RECORDS_ERROR"); } return page; }; /** * There are only few options to use when getting the page. * For that reason we try to have it as simple as possible when querying. */ const get = async params => { const { where } = params; const { pid, id, path, published } = where; let { version } = where; /** * In case of having full ID and not having version we can take the version from the id. */ if (id && id.includes("#") && !version) { version = Number(id.split("#").pop()); } let keys; if (path) { return getByPath({ where: { ...where, path } }); } else if (!id && !pid) { throw new _error.default("There are no ID or pageId.", "MALFORMED_GET_REQUEST", { where }); } else if (published) { keys = { PK: (0, _keys.createPublishedPartitionKey)(where), SK: (0, _keys.createPublishedSortKey)({ id: id || pid }) }; } else if (version) { keys = { PK: (0, _keys.createRevisionPartitionKey)({ ...where, id: id || pid }), SK: (0, _keys.createRevisionSortKey)({ version }) }; } else { keys = { PK: (0, _keys.createLatestPartitionKey)(where), SK: (0, _keys.createLatestSortKey)({ id: id || pid }) }; } try { return await (0, _get.getClean)({ entity, keys }); } catch (ex) { throw new _error.default(ex.message || "Could not load page by given params.", ex.code || "GET_PAGE_ERROR", { where, keys }); } }; const getByPath = async params => { const { where } = params; const pathKeys = { PK: (0, _keys.createPathPartitionKey)(where), SK: (0, _keys.createPathSortKey)(where) }; const queryOptions = { entity, partitionKey: pathKeys.PK, options: { index: GSI1_INDEX, eq: pathKeys.SK } }; try { return await (0, _dbDynamodb.queryOneClean)(queryOptions); } catch (ex) { throw new _error.default(ex.message || "Could not get page by given path.", ex.code || "GET_PAGE_BY_PATH_ERROR", { params }); } }; const list = async params => { const { where: initialWhere } = params; const { latest, published } = initialWhere; /** * We do not allow loading both published and latest at the same time. * @see PageStorageOperationsListWhere */ if (published && latest) { throw new _error.default("Both published and latest cannot be defined at the same time.", "MALFORMED_WHERE_ERROR", { where: params.where }); } const { limit: initialLimit, after: previousCursor } = params; const limit = initialLimit || 50; const options = { gte: " " }; const { tags_in: tags, tags_rule: tagsRule, search } = initialWhere; const where = { ...initialWhere }; delete where.search; delete where.tags_in; delete where.tags_rule; delete where.tenant; delete where.locale; delete where.latest; delete where.published; if (tags && tags.length > 0) { if (tagsRule === "any") { where.tags_in = tags; } else { where.tags_and_in = tags; } } if (search) { /** * We need to pass fuzzy into where, so we need to cast it as where because it does not exist on the original type */ where.fuzzy = { fields: ["title", "snippet", "path"], value: search }; } let partitionKey; if (published) { partitionKey = (0, _keys.createPublishedPartitionKey)(initialWhere); // where.listPublished_not = false; } else { partitionKey = (0, _keys.createLatestPartitionKey)(initialWhere); where.listLatest_not = false; } const queryAllParams = { entity, partitionKey, options }; let dbRecords = []; try { dbRecords = await (0, _dbDynamodb.queryAll)(queryAllParams); } catch (ex) { throw new _error.default(ex.message || "Could not load pages by given query params.", ex.code || "LIST_PAGES_ERROR", { partitionKey, options }); } const fields = plugins.byType(_PageDynamoDbFieldPlugin.PageDynamoDbFieldPlugin.type); const filteredPages = (0, _dbDynamodb.filterItems)({ items: dbRecords, plugins, where, fields }).map(item => { return (0, _cleanup.cleanupItem)(entity, item); }); const sortedPages = (0, _dbDynamodb.sortItems)({ items: filteredPages, sort: params.sort, fields }); const totalCount = sortedPages.length; const start = parseInt((0, _dbDynamodb.decodeCursor)(previousCursor) || "0") || 0; const hasMoreItems = totalCount > start + limit; const end = limit > totalCount + start + limit ? undefined : start + limit; const pages = sortedPages.slice(start, end); /** * Although we do not need a cursor here, we will use it as such to keep it standardized. * Number is simply encoded. */ const cursor = pages.length > 0 ? (0, _dbDynamodb.encodeCursor)(start + limit) : null; const meta = { hasMoreItems, totalCount, cursor }; return { items: pages, meta }; }; /** * Listing of the revisions will be done through the DynamoDB since there are no revisions saved in the Elasticsearch. */ const listRevisions = async params => { const { where, sort } = params; const queryAllParams = { entity, partitionKey: (0, _keys.createRevisionPartitionKey)({ ...where, id: where.pid }), options: { gte: " ", reverse: false } }; let items = []; try { items = await (0, _dbDynamodb.queryAll)(queryAllParams); } catch (ex) { throw new _error.default(ex.message || "Could not load all the revisions from requested page.", ex.code || "LOAD_PAGE_REVISIONS_ERROR", { params }); } const fields = plugins.byType(_PageDynamoDbFieldPlugin.PageDynamoDbFieldPlugin.type); return (0, _dbDynamodb.sortItems)({ items, fields, sort }); }; const listTags = async params => { const { where } = params; const options = { gte: " " }; const partitionKey = (0, _keys.createLatestPartitionKey)(where); const queryAllParams = { entity, partitionKey, options }; let pages = []; try { pages = await (0, _dbDynamodb.queryAll)(queryAllParams); } catch (ex) { throw new _error.default(ex.message || "Could not load pages by given query params.", ex.code || "LIST_PAGES_TAGS_ERROR", { partitionKey, options }); } const tags = new Set(); for (const page of pages) { let tagList = page.settings?.general?.tags; if (!tagList?.length) { continue; } else if (where.search) { const re = new RegExp(where.search, "i"); tagList = tagList.filter(tag => !!tag && tag.match(re) !== null); } for (const tag of tagList) { tags.add(tag); } } return Array.from(tags); }; return { get, create, update, delete: deleteOne, deleteAll, createFrom, list, listRevisions, publish, unpublish, listTags }; }; exports.createPageStorageOperations = createPageStorageOperations; //# sourceMappingURL=index.js.map