Updated serialization for handling tiers visibility

refs https://github.com/TryGhost/Team/issues/1071

Going forward, if the visibility of a page/post is set for specific tiers, we send a `tiers` array in API response that contains list of tiers with access. This change -

- updates post/page mapper to transform existing data where `visibility` is a custom nql string to tiers array
- updates default include for post/pages to include `products`, which allows attaching relevant tiers from the pivot table
- cleans up usage of `visibility_filter` in serialization
This commit is contained in:
Rishabh 2022-01-26 16:50:52 +05:30 committed by Rishabh Garg
parent 354bb5c9a1
commit 7ab4c44475
14 changed files with 73 additions and 50 deletions

View File

@ -1,7 +1,7 @@
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const models = require('../../models');
const ALLOWED_INCLUDES = ['authors', 'tags'];
const ALLOWED_INCLUDES = ['authors', 'tags', 'tiers'];
const messages = {
postNotFound: 'Post not found.'

View File

@ -1,7 +1,7 @@
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const models = require('../../models');
const ALLOWED_INCLUDES = ['tags', 'authors'];
const ALLOWED_INCLUDES = ['tags', 'authors', 'tiers'];
const messages = {
pageNotFound: 'Page not found.'

View File

@ -2,7 +2,7 @@ const models = require('../../models');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const getPostServiceInstance = require('../../services/posts/posts-service');
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles', 'tiers'];
const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
const messages = {

View File

@ -1,7 +1,7 @@
const models = require('../../models');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const allowedIncludes = ['tags', 'authors'];
const allowedIncludes = ['tags', 'authors', 'tiers'];
const messages = {
postNotFound: 'Post not found.'

View File

@ -2,7 +2,7 @@ const models = require('../../models');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const getPostServiceInstance = require('../../services/posts/posts-service');
const allowedIncludes = ['tags', 'authors', 'authors.roles', 'email'];
const allowedIncludes = ['tags', 'authors', 'authors.roles', 'email', 'tiers'];
const unsafeAttrs = ['status', 'authors', 'visibility'];
const messages = {

View File

@ -38,7 +38,7 @@ function defaultRelations(frame) {
return false;
}
frame.options.withRelated = ['tags', 'authors', 'authors.roles'];
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'tiers'];
}
function setDefaultOrder(frame) {
@ -102,13 +102,6 @@ const forceStatusFilter = (frame) => {
}
};
const transformPageVisibilityFilters = (frame) => {
if (frame.data.pages[0].visibility === 'filter' && frame.data.pages[0].visibility_filter) {
frame.data.pages[0].visibility = frame.data.pages[0].visibility_filter;
}
delete frame.data.pages[0].visibility_filter;
};
module.exports = {
browse(apiConfig, frame) {
debug('browse');
@ -187,7 +180,6 @@ module.exports = {
});
}
transformPageVisibilityFilters(frame);
handlePostsMeta(frame);
defaultFormat(frame);
defaultRelations(frame);

View File

@ -38,7 +38,7 @@ function defaultRelations(frame) {
return false;
}
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email'];
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers'];
}
function setDefaultOrder(frame) {
@ -111,13 +111,6 @@ const transformLegacyEmailRecipientFilters = (frame) => {
}
};
const transformPostVisibilityFilters = (frame) => {
if (frame.data.posts[0].visibility === 'filter' && frame.data.posts[0].visibility_filter) {
frame.data.posts[0].visibility = frame.data.posts[0].visibility_filter;
}
delete frame.data.posts[0].visibility_filter;
};
module.exports = {
browse(apiConfig, frame) {
debug('browse');
@ -212,7 +205,6 @@ module.exports = {
});
}
transformPostVisibilityFilters(frame);
transformLegacyEmailRecipientFilters(frame);
handlePostsMeta(frame);
defaultFormat(frame);

View File

@ -2,8 +2,8 @@ const mapper = require('./utils/mapper');
const gating = require('./utils/post-gating');
module.exports = {
read(model, apiConfig, frame) {
const emailPost = mapper.mapPost(model, frame);
async read(model, apiConfig, frame) {
const emailPost = await mapper.mapPost(model, frame);
gating.forPost(emailPost, frame);
frame.response = {

View File

@ -2,25 +2,29 @@ const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:pa
const mapper = require('./utils/mapper');
module.exports = {
all(models, apiConfig, frame) {
async all(models, apiConfig, frame) {
debug('all');
// CASE: e.g. destroy returns null
if (!models) {
return;
}
let pages = [];
if (models.meta) {
for (let model of models.data) {
let page = await mapper.mapPage(model, frame);
pages.push(page);
}
frame.response = {
pages: models.data.map(model => mapper.mapPage(model, frame)),
pages,
meta: models.meta
};
return;
}
let page = await mapper.mapPage(models, frame);
frame.response = {
pages: [mapper.mapPage(models, frame)]
pages: [page]
};
}
};

View File

@ -2,25 +2,29 @@ const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:po
const mapper = require('./utils/mapper');
module.exports = {
all(models, apiConfig, frame) {
async all(models, apiConfig, frame) {
debug('all');
// CASE: e.g. destroy returns null
if (!models) {
return;
}
let posts = [];
if (models.meta) {
for (let model of models.data) {
let post = await mapper.mapPost(model, frame);
posts.push(post);
}
frame.response = {
posts: models.data.map(model => mapper.mapPost(model, frame)),
posts,
meta: models.meta
};
return;
}
let post = await mapper.mapPost(models, frame);
frame.response = {
posts: [mapper.mapPost(models, frame)]
posts: [post]
};
}
};

View File

@ -1,9 +1,10 @@
const mapper = require('./utils/mapper');
module.exports = {
all(model, apiConfig, frame) {
async all(model, apiConfig, frame) {
const data = await mapper.mapPost(model, frame);
frame.response = {
preview: [mapper.mapPost(model, frame)]
preview: [data]
};
frame.response.preview[0].page = model.get('type') === 'page';
}

View File

@ -1,6 +1,5 @@
const _ = require('lodash');
const localUtils = require('../../../index');
const labsService = require('../../../../../../../shared/labs');
const tag = (attrs, frame) => {
if (localUtils.isContentAPI(frame)) {
@ -122,14 +121,6 @@ const post = (attrs, frame) => {
delete attrs.primary_author;
}
// Handles visibility filter for multiple products
if (attrs.visibility && labsService.isSet('multipleProducts')) {
if (!['members', 'public', 'paid'].includes(attrs.visibility)) {
attrs.visibility_filter = attrs.visibility;
attrs.visibility = 'filter';
}
}
delete attrs.locale;
delete attrs.author;
delete attrs.type;

View File

@ -7,6 +7,10 @@ const clean = require('./clean');
const extraAttrs = require('./extra-attrs');
const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;
const mega = require('../../../../../../services/mega');
const labsService = require('../../../../../../../shared/labs');
const getPostServiceInstance = require('../../../../../../services/posts/posts-service');
const postsService = getPostServiceInstance('canary');
const mapUser = (model, frame) => {
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
@ -27,7 +31,7 @@ const mapTag = (model, frame) => {
return jsonModel;
};
const mapPost = (model, frame) => {
const mapPost = async (model, frame) => {
const extendedOptions = Object.assign(_.cloneDeep(frame.options), {
extraProperties: ['canonical_url']
});
@ -38,6 +42,17 @@ const mapPost = (model, frame) => {
extraAttrs.forPost(frame, model, jsonModel);
// Attach tiers to custom nql visibility filter
if (labsService.isSet('multipleProducts')
&& jsonModel.visibility
&& !['members', 'public', 'paid', 'tiers'].includes(jsonModel.visibility)
) {
const tiers = await postsService.getProductsFromVisibilityFilter(jsonModel.visibility);
jsonModel.visibility = 'tiers';
jsonModel.tiers = tiers;
}
if (utils.isContentAPI(frame)) {
// Content api v2 still expects page prop
if (jsonModel.type === 'page') {
@ -88,8 +103,8 @@ const mapPost = (model, frame) => {
return jsonModel;
};
const mapPage = (model, frame) => {
const jsonModel = mapPost(model, frame);
const mapPage = async (model, frame) => {
const jsonModel = await mapPost(model, frame);
delete jsonModel.email_subject;
delete jsonModel.email_recipient_filter;

View File

@ -1,8 +1,10 @@
const nql = require('@nexes/nql');
const {BadRequestError} = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const messages = {
invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.'
invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.',
invalidVisibilityFilter: 'Invalid visibility filter.'
};
class PostsService {
@ -78,6 +80,28 @@ class PostsService {
return model;
}
async getProductsFromVisibilityFilter(visibilityFilter) {
try {
const allProducts = await this.models.Product.findAll();
const visibilityFilterJson = nql(visibilityFilter).toJSON();
const productsData = (visibilityFilterJson.product ? [visibilityFilterJson] : visibilityFilterJson.$or) || [];
const tiers = productsData
.map((data) => {
return allProducts.find((p) => {
return p.get('slug') === data.product;
});
}).filter(p => !!p).map((d) => {
return d.toJSON();
});
return tiers;
} catch (err) {
return Promise.reject(new BadRequestError({
message: tpl(messages.invalidVisibilityFilter),
context: err.message
}));
}
}
/**
* Calculates if the email should be tried to be sent out
* @private