@webiny/api-page-builder-so-ddb
Version:
The DynamoDB storage operations Webiny Page Builder API.
751 lines (740 loc) • 20 kB
JavaScript
"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