Skip to content

Commit

Permalink
🏗 Extracted post metadata in new post_meta table (#11102)
Browse files Browse the repository at this point in the history
NOTE: The post metadata table split is purely an internal optimization for v3 and doesn't require or expect any external actions including related API usage in v3

We keep running into issues adding new fields to the post table because there are too many fields making the post table "too wide". We have also hit MySQL limitations in how many bytes can be in a row (64kb) with post table.

In v3, we decided to split the 8 post fields (meta, twitter and og) used for meta data into a posts_meta table as these 8 fields are all "problem" `varchar` fields and make sense logically grouped together. The API layer is unaffected by the split as input/output serializers ensure the data flow works the same way as it was in v2. Only thing to note is json export in v3 will have slightly different structure with posts meta fields as separate.

- Creates new post_meta schema/table with 8 fields (2 meta_* , 3 twitter_* and 3 og_*)
- Update relations between post and post_meta table
- Update input/output serializers to keep existing API behavior
- Avoids new entry in post_meta table for post where all meta fields are null
- Keeps the current fields API param behavior
- Handles migration of existing posts to new table structure
- Updates importer/exporter to work seamlessly with table changes
  • Loading branch information
rishabhgrg committed Sep 16, 2019
1 parent 378ebe6 commit 8ec12d9
Show file tree
Hide file tree
Showing 30 changed files with 515 additions and 46 deletions.
9 changes: 9 additions & 0 deletions core/server/api/canary/utils/serializers/input/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const debug = require('ghost-ignition').debug('api:canary:utils:serializers:inpu
const converters = require('../../../../../lib/mobiledoc/converters');
const url = require('./utils/url');
const localUtils = require('../../index');
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;

function removeMobiledocFormat(frame) {
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
Expand Down Expand Up @@ -45,6 +46,12 @@ function defaultFormat(frame) {
frame.options.formats = 'mobiledoc';
}

function handlePostsMeta(frame) {
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
let meta = _.pick(frame.data.pages[0], metaAttrs);
frame.data.pages[0].posts_meta = meta;
}

/**
* CASE:
*
Expand Down Expand Up @@ -147,6 +154,7 @@ module.exports = {
});
}

handlePostsMeta(frame);
defaultFormat(frame);
defaultRelations(frame);
},
Expand All @@ -156,6 +164,7 @@ module.exports = {

debug('edit');

handlePostsMeta(frame);
forceStatusFilter(frame);
forcePageFilter(frame);
},
Expand Down
9 changes: 9 additions & 0 deletions core/server/api/canary/utils/serializers/input/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const url = require('./utils/url');
const localUtils = require('../../index');
const labs = require('../../../../../services/labs');
const converters = require('../../../../../lib/mobiledoc/converters');
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;

function removeMobiledocFormat(frame) {
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
Expand Down Expand Up @@ -54,6 +55,12 @@ function defaultFormat(frame) {
frame.options.formats = 'mobiledoc';
}

function handlePostsMeta(frame) {
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
let meta = _.pick(frame.data.posts[0], metaAttrs);
frame.data.posts[0].posts_meta = meta;
}

/**
* CASE:
*
Expand Down Expand Up @@ -182,13 +189,15 @@ module.exports = {
});
}

handlePostsMeta(frame);
defaultFormat(frame);
defaultRelations(frame);
},

edit(apiConfig, frame) {
this.add(apiConfig, frame, {add: false});

handlePostsMeta(frame);
forceStatusFilter(frame);
forcePageFilter(frame);
},
Expand Down
10 changes: 10 additions & 0 deletions core/server/api/canary/utils/serializers/output/utils/mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const date = require('./date');
const members = require('./members');
const clean = require('./clean');
const extraAttrs = require('./extra-attrs');
const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;

const mapUser = (model, frame) => {
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
Expand Down Expand Up @@ -57,6 +58,15 @@ const mapPost = (model, frame) => {
});
}

// Transforms post/page metadata to flat structure
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
_(metaAttrs).filter((k) => {
return (!frame.options.columns || (frame.options.columns && frame.options.columns.includes(k)));
}).each((attr) => {
jsonModel[attr] = _.get(jsonModel.posts_meta, attr) || null;
});
delete jsonModel.posts_meta;

return jsonModel;
};

Expand Down
9 changes: 9 additions & 0 deletions core/server/api/v2/utils/serializers/input/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:pa
const converters = require('../../../../../lib/mobiledoc/converters');
const url = require('./utils/url');
const localUtils = require('../../index');
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;

function removeMobiledocFormat(frame) {
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
Expand Down Expand Up @@ -45,6 +46,12 @@ function defaultFormat(frame) {
frame.options.formats = 'mobiledoc';
}

function handlePostsMeta(frame) {
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
let meta = _.pick(frame.data.pages[0], metaAttrs);
frame.data.pages[0].posts_meta = meta;
}

/**
* CASE:
*
Expand Down Expand Up @@ -147,6 +154,7 @@ module.exports = {
});
}

handlePostsMeta(frame);
defaultFormat(frame);
defaultRelations(frame);
},
Expand All @@ -156,6 +164,7 @@ module.exports = {

debug('edit');

handlePostsMeta(frame);
forceStatusFilter(frame);
forcePageFilter(frame);
},
Expand Down
9 changes: 9 additions & 0 deletions core/server/api/v2/utils/serializers/input/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const url = require('./utils/url');
const localUtils = require('../../index');
const labs = require('../../../../../services/labs');
const converters = require('../../../../../lib/mobiledoc/converters');
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;

function removeMobiledocFormat(frame) {
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
Expand Down Expand Up @@ -54,6 +55,12 @@ function defaultFormat(frame) {
frame.options.formats = 'mobiledoc';
}

function handlePostsMeta(frame) {
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
let meta = _.pick(frame.data.posts[0], metaAttrs);
frame.data.posts[0].posts_meta = meta;
}

/**
* CASE:
*
Expand Down Expand Up @@ -182,13 +189,15 @@ module.exports = {
});
}

handlePostsMeta(frame);
defaultFormat(frame);
defaultRelations(frame);
},

edit(apiConfig, frame) {
this.add(apiConfig, frame, {add: false});

handlePostsMeta(frame);
forceStatusFilter(frame);
forcePageFilter(frame);
},
Expand Down
10 changes: 10 additions & 0 deletions core/server/api/v2/utils/serializers/output/utils/mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const date = require('./date');
const members = require('./members');
const clean = require('./clean');
const extraAttrs = require('./extra-attrs');
const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;

const mapUser = (model, frame) => {
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
Expand Down Expand Up @@ -57,6 +58,15 @@ const mapPost = (model, frame) => {
});
}

// Transforms post/page metadata to flat structure
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
_(metaAttrs).filter((k) => {
return (!frame.options.columns || (frame.options.columns && frame.options.columns.includes(k)));
}).each((attr) => {
jsonModel[attr] = _.get(jsonModel.posts_meta, attr) || null;
});
delete jsonModel.posts_meta;

return jsonModel;
};

Expand Down
17 changes: 16 additions & 1 deletion core/server/data/importer/importers/data/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ const uuid = require('uuid');
const BaseImporter = require('./base');
const converters = require('../../../../lib/mobiledoc/converters');
const validation = require('../../../validation');
const postsMetaSchema = require('../../../schema').tables.posts_meta;
const metaAttrs = _.keys(_.omit(postsMetaSchema, ['id']));

class PostsImporter extends BaseImporter {
constructor(allDataFromFile) {
super(allDataFromFile, {
modelName: 'Post',
dataKeyToImport: 'posts',
requiredFromFile: ['posts', 'tags', 'posts_tags', 'posts_authors'],
requiredFromFile: ['posts', 'tags', 'posts_tags', 'posts_authors', 'posts_meta'],
requiredImportedData: ['tags'],
requiredExistingData: ['tags']
});
Expand All @@ -24,6 +26,18 @@ class PostsImporter extends BaseImporter {
});
}

/**
* Sanitizes post metadata, picking data from sepearate table(for >= v3) or post itself(for < v3)
*/
sanitizePostsMeta(model) {
let postsMetaFromFile = _.find(this.requiredFromFile.posts_meta, {post_id: model.id}) || _.pick(model, metaAttrs);
let postsMetaData = Object.assign({}, _.mapValues(postsMetaSchema, () => null), postsMetaFromFile);
model.posts_meta = postsMetaData;
_.each(metaAttrs, (attr) => {
delete model[attr];
});
}

/**
* Naive function to attach related tags and authors.
*/
Expand Down Expand Up @@ -202,6 +216,7 @@ class PostsImporter extends BaseImporter {
model.mobiledoc = JSON.stringify(mobiledoc);
model.html = converters.mobiledocConverter.render(JSON.parse(model.mobiledoc));
}
this.sanitizePostsMeta(model);
});

// NOTE: We only support removing duplicate posts within the file to import.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const common = require('../../../../lib/common');
const commands = require('../../../schema').commands;
const table = 'posts_meta';
const message1 = `Adding table: ${table}`;
const message2 = `Dropping table: ${table}`;

module.exports.up = (options) => {
const connection = options.connection;

return connection.schema.hasTable(table)
.then(function (exists) {
if (exists) {
common.logging.warn(message1);
return;
}

common.logging.info(message1);
return commands.createTable(table, connection);
});
};

module.exports.down = (options) => {
const connection = options.connection;

return connection.schema.hasTable(table)
.then(function (exists) {
if (!exists) {
common.logging.warn(message2);
return;
}

common.logging.info(message2);
return commands.deleteTable(table, connection);
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const postsMetaSchema = require('../../../schema').tables.posts_meta;
const ObjectId = require('bson-objectid');
const _ = require('lodash');
const models = require('../../../../models');
const common = require('../../../../lib/common');

module.exports.config = {
transaction: true
};

module.exports.up = (options) => {
const localOptions = _.merge({
context: {internal: true},
migrating: true
}, options);
const metaAttrs = _.keys(postsMetaSchema);

return models.Posts
.forge()
.query((qb) => {
// We only want to add entries in new table for posts which have any metadata
qb.whereNotNull('meta_title');
qb.orWhereNotNull('meta_description');
qb.orWhereNotNull('twitter_title');
qb.orWhereNotNull('twitter_description');
qb.orWhereNotNull('twitter_image');
qb.orWhereNotNull('og_description');
qb.orWhereNotNull('og_title');
qb.orWhereNotNull('og_image');
})
.fetch(localOptions)
.then(({models: posts}) => {
if (posts.length > 0) {
common.logging.info(`Adding ${posts.length} entries to posts_meta`);
let postsMetaEntries = _.map(posts, (post) => {
let postsMetaEntry = metaAttrs.reduce(function (obj, entry) {
return Object.assign(obj, {
[entry]: post.get(entry)
});
}, {});
postsMetaEntry.post_id = post.get('id');
postsMetaEntry.id = ObjectId.generate();
return postsMetaEntry;
});
return localOptions.transacting('posts_meta').insert(postsMetaEntries);
} else {
common.logging.info('Skipping populating posts_meta table: found 0 posts with metadata');
return Promise.resolve();
}
});
};

module.exports.down = function (options) {
let localOptions = _.merge({
context: {internal: true},
migrating: true
}, options);
const metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));

return models.PostsMeta
.findAll(localOptions)
.then(({models: postsMeta}) => {
if (postsMeta.length > 0) {
common.logging.info(`Adding metadata for ${postsMeta.length} posts from posts_meta table`);
return Promise.map(postsMeta, (postsMeta) => {
let data = metaAttrs.reduce(function (obj, entry) {
return Object.assign(obj, {
[entry]: postsMeta.get(entry)
});
}, {});
return localOptions.transacting('posts').where({id: postsMeta.get('post_id')}).update(data);
});
} else {
common.logging.info('Skipping populating meta fields from posts_meta: found 0 entries');
return Promise.resolve();
}
});
};
Loading

0 comments on commit 8ec12d9

Please sign in to comment.