From 40d0a745df900c2a1edd82425b7cc80f3f29b620 Mon Sep 17 00:00:00 2001 From: Katharina Irrgang Date: Tue, 27 Mar 2018 16:16:15 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Multiple=20authors=20(#9426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no issue This PR adds the server side logic for multiple authors. This adds the ability to add multiple authors per post. We keep and support single authors (maybe till the next major - this is still in discussion) ### key notes - `authors` are not fetched by default, only if we need them - the migration script iterates over all posts and figures out if an author_id is valid and exists (in master we can add invalid author_id's) and then adds the relation (falls back to owner if invalid) - ~~i had to push a fork of bookshelf to npm because we currently can't bump bookshelf + the two bugs i discovered are anyway not yet merged (https://github.com/kirrg001/bookshelf/commits/master)~~ replaced by new bookshelf release - the implementation of single & multiple authors lives in a single place (introduction of a new concept: model relation) - if you destroy an author, we keep the behaviour for now -> remove all posts where the primary author id matches. furthermore, remove all relations in posts_authors (e.g. secondary author) - we make re-use of the `excludeAttrs` concept which was invented in the contributors PR (to protect editing authors as author/contributor role) -> i've added a clear todo that we need a logic to make a diff of the target relation -> both for tags and authors - `authors` helper available (same as `tags` helper) - `primary_author` computed field available - `primary_author` functionality available (same as `primary_tag` e.g. permalinks, prev/next helper etc) --- core/server/api/posts.js | 7 +- core/server/api/utils.js | 33 +- core/server/config/overrides.json | 3 +- .../server/controllers/frontend/fetch-data.js | 13 +- .../controllers/frontend/post-lookup.js | 8 +- .../data/importer/importers/data/posts.js | 98 +- core/server/data/meta/author_fb_url.js | 4 +- core/server/data/meta/author_image.js | 4 +- core/server/data/meta/author_url.js | 5 +- core/server/data/meta/creator_url.js | 4 +- core/server/data/meta/index.js | 4 +- core/server/data/meta/schema.js | 18 +- .../versions/1.22/1-multiple-authors-DDL.js | 38 + .../versions/1.22/1-multiple-authors-DML.js | 55 ++ core/server/data/schema/default-settings.json | 2 +- .../server/data/schema/fixtures/fixtures.json | 207 ++-- core/server/data/schema/schema.js | 66 +- .../server/data/xml/sitemap/post-generator.js | 4 + core/server/helpers/author.js | 3 + core/server/helpers/authors.js | 53 ++ core/server/helpers/has.js | 82 +- core/server/helpers/index.js | 2 + core/server/helpers/prev_next.js | 7 +- core/server/models/base/index.js | 13 +- core/server/models/plugins/filter.js | 36 + core/server/models/post.js | 138 +-- core/server/models/relations/authors.js | 365 +++++++ core/server/models/relations/index.js | 7 + core/server/services/rss/generate-feed.js | 2 +- core/server/services/url/utils.js | 7 + core/server/translations/en.json | 1 + core/test/functional/routes/api/posts_spec.js | 53 +- .../integration/api/advanced_browse_spec.js | 120 ++- .../integration/api/api_configuration_spec.js | 1 + core/test/integration/api/api_posts_spec.js | 77 +- core/test/integration/api/api_users_spec.js | 51 +- .../data/importer/importers/data_spec.js | 82 +- .../integration/model/model_posts_spec.js | 95 +- .../test/integration/model/model_tags_spec.js | 2 +- core/test/unit/api/utils_spec.js | 69 +- .../controllers/frontend/fetch-data_spec.js | 4 +- .../controllers/frontend/post-lookup_spec.js | 8 +- core/test/unit/data/export/index_spec.js | 25 +- .../test/unit/data/meta/author_fb_url_spec.js | 6 +- core/test/unit/data/meta/author_image_spec.js | 10 +- core/test/unit/data/meta/author_url_spec.js | 8 +- core/test/unit/data/meta/creator_url_spec.js | 8 +- core/test/unit/data/meta/schema_spec.js | 8 +- .../unit/data/schema/fixtures/utils_spec.js | 5 +- core/test/unit/data/schema/integrity_spec.js | 4 +- core/test/unit/helpers/authors_spec.js | 173 ++++ core/test/unit/helpers/ghost_head_spec.js | 35 +- core/test/unit/helpers/has_spec.js | 138 ++- core/test/unit/helpers/index_spec.js | 2 +- core/test/unit/helpers/next_post_spec.js | 42 +- core/test/unit/helpers/prev_post_spec.js | 42 +- core/test/unit/models/post_spec.js | 897 +++++++++++++++++- .../services/channels/parent-router_spec.js | 22 +- .../services/permissions/can-this_spec.js | 11 +- .../unit/services/rss/generate-feed_spec.js | 4 +- core/test/utils/api.js | 4 +- core/test/utils/fixtures/data-generator.js | 59 ++ .../utils/fixtures/export/export-authors.json | 304 ++++++ .../test/utils/fixtures/filter-param/index.js | 17 +- core/test/utils/index.js | 48 +- core/test/utils/mocks/knex.js | 2 +- package.json | 4 +- yarn.lock | 153 ++- 68 files changed, 3269 insertions(+), 613 deletions(-) create mode 100644 core/server/data/migrations/versions/1.22/1-multiple-authors-DDL.js create mode 100644 core/server/data/migrations/versions/1.22/1-multiple-authors-DML.js create mode 100644 core/server/helpers/authors.js create mode 100644 core/server/models/relations/authors.js create mode 100644 core/server/models/relations/index.js create mode 100644 core/test/unit/helpers/authors_spec.js create mode 100644 core/test/utils/fixtures/export/export-authors.json diff --git a/core/server/api/posts.js b/core/server/api/posts.js index 6f5187635a..99f7362cc5 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -7,10 +7,13 @@ var Promise = require('bluebird'), models = require('../models'), common = require('../lib/common'), docName = 'posts', + /** + * @deprecated: `author`, will be removed in Ghost 2.0 + */ allowedIncludes = [ - 'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields' + 'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields', 'authors', 'authors.roles' ], - unsafeAttrs = ['author_id', 'status'], + unsafeAttrs = ['author_id', 'status', 'authors'], posts; /** diff --git a/core/server/api/utils.js b/core/server/api/utils.js index e1c7bb769e..9c6a683328 100644 --- a/core/server/api/utils.js +++ b/core/server/api/utils.js @@ -316,12 +316,43 @@ utils = { })); } - // convert author property to author_id to match the name in the database if (docName === 'posts') { + /** + * Convert author property to author_id to match the name in the database. + * + * @deprecated: `author`, will be removed in Ghost 2.0 + */ if (object.posts[0].hasOwnProperty('author')) { object.posts[0].author_id = object.posts[0].author; delete object.posts[0].author; } + + /** + * Ensure correct incoming `post.authors` structure. + * + * NOTE: + * The `post.authors[*].id` attribute is required till we release Ghost 2.0. + * Ghost 1.x keeps the deprecated support for `post.author_id`, which is the primary author id and needs to be + * updated if the order of the `post.authors` array changes. + * If we allow adding authors via the post endpoint e.g. `authors=[{name: 'newuser']` (no id property), it's hard + * to update the primary author id (`post.author_id`), because the new author `id` is generated when attaching + * the author to the post. And the attach operation happens in bookshelf-relations, which happens after + * the event handling in the post model. + * + * It's solvable, but not worth right now solving, because the admin UI does not support this feature. + * + * TLDR; You can only attach existing authors to a post. + * + * @TODO: remove `id` restriction in Ghost 2.0 + */ + if (object.posts[0].hasOwnProperty('authors')) { + if (!_.isArray(object.posts[0].authors) || + (object.posts[0].authors.length && _.filter(object.posts[0].authors, 'id').length !== object.posts[0].authors.length)) { + return Promise.reject(new common.errors.BadRequestError({ + message: common.i18n.t('errors.api.utils.invalidStructure', {key: 'posts[*].authors'}) + })); + } + } } // will remove unwanted null values diff --git a/core/server/config/overrides.json b/core/server/config/overrides.json index 6e2809acd6..46c52ed498 100644 --- a/core/server/config/overrides.json +++ b/core/server/config/overrides.json @@ -27,7 +27,8 @@ "private": "private", "subscribe": "subscribe", "amp": "amp", - "primaryTagFallback": "all" + "primaryTagFallback": "all", + "primaryAuthorFallback": "all" }, "slugs": { "reserved": ["admin", "app", "apps", "categories", diff --git a/core/server/controllers/frontend/fetch-data.js b/core/server/controllers/frontend/fetch-data.js index 3a1d83d4d0..9dffc254f0 100644 --- a/core/server/controllers/frontend/fetch-data.js +++ b/core/server/controllers/frontend/fetch-data.js @@ -3,7 +3,7 @@ * Dynamically build and execute queries on the API for channels */ var api = require('../../api'), - _ = require('lodash'), + _ = require('lodash'), Promise = require('bluebird'), themes = require('../../services/themes'), queryDefaults, @@ -16,10 +16,14 @@ queryDefaults = { options: {} }; -// Default post query needs to always include author & tags +/** + * Default post query needs to always include author, authors & tags + * + * @deprecated: `author`, will be removed in Ghost 2.0 + */ _.extend(defaultPostQuery, queryDefaults, { options: { - include: 'author,tags' + include: 'author,authors,tags' } }); @@ -85,8 +89,7 @@ function processQuery(query, slugParam) { */ function fetchData(channelOptions) { // @TODO improve this further - var pageOptions = channelOptions.isRSS ? - {options: channelOptions.postOptions} : fetchPostsPerPage(channelOptions.postOptions), + var pageOptions = channelOptions.isRSS ? {options: channelOptions.postOptions} : fetchPostsPerPage(channelOptions.postOptions), postQuery, props = {}; diff --git a/core/server/controllers/frontend/post-lookup.js b/core/server/controllers/frontend/post-lookup.js index 4f10ddbb34..7292feb71d 100644 --- a/core/server/controllers/frontend/post-lookup.js +++ b/core/server/controllers/frontend/post-lookup.js @@ -40,8 +40,12 @@ function postLookup(postUrl) { isEditURL = true; } - // Query database to find post - return api.posts.read(_.extend(_.pick(params, 'slug', 'id'), {include: 'author,tags'})).then(function then(result) { + /** + * Query database to find post. + * + * @deprecated: `author`, will be removed in Ghost 2.0 + */ + return api.posts.read(_.extend(_.pick(params, 'slug', 'id'), {include: 'author,authors,tags'})).then(function then(result) { var post = result.posts[0]; if (!post) { diff --git a/core/server/data/importer/importers/data/posts.js b/core/server/data/importer/importers/data/posts.js index ccc2dd65f6..45f8f14537 100644 --- a/core/server/data/importer/importers/data/posts.js +++ b/core/server/data/importer/importers/data/posts.js @@ -11,7 +11,7 @@ class PostsImporter extends BaseImporter { super(allDataFromFile, { modelName: 'Post', dataKeyToImport: 'posts', - requiredFromFile: ['posts', 'tags', 'posts_tags'], + requiredFromFile: ['posts', 'tags', 'posts_tags', 'posts_authors'], requiredImportedData: ['tags'], requiredExistingData: ['tags'] }); @@ -30,90 +30,98 @@ class PostsImporter extends BaseImporter { } /** - * Naive function to attach related tags. - * Target tags should not be created. We add the relation by foreign key. + * Naive function to attach related tags and authors. */ addNestedRelations() { this.requiredFromFile.posts_tags = _.orderBy(this.requiredFromFile.posts_tags, ['post_id', 'sort_order'], ['asc', 'asc']); + this.requiredFromFile.posts_authors = _.orderBy(this.requiredFromFile.posts_authors, ['post_id', 'sort_order'], ['asc', 'asc']); /** - * {post_id: 1, tag_id: 2} + * from {post_id: 1, tag_id: 2} to post.tags=[{id:id}] + * from {post_id: 1, author_id: 2} post.authors=[{id:id}] */ - _.each(this.requiredFromFile.posts_tags, (postTagRelation) => { - if (!postTagRelation.post_id) { - return; - } + const run = (relations, target, fk) => { + _.each(relations, (relation) => { + if (!relation.post_id) { + return; + } - let postToImport = _.find(this.dataToImport, {id: postTagRelation.post_id}); + let postToImport = _.find(this.dataToImport, {id: relation.post_id}); - // CASE: we won't import a relation when the target post does not exist - if (!postToImport) { - return; - } + // CASE: we won't import a relation when the target post does not exist + if (!postToImport) { + return; + } - if (!postToImport.tags || !_.isArray(postToImport.tags)) { - postToImport.tags = []; - } + if (!postToImport[target] || !_.isArray(postToImport[target])) { + postToImport[target] = []; + } - // CASE: duplicate relation? - if (!_.find(postToImport.tags, {tag_id: postTagRelation.tag_id})) { - postToImport.tags.push({ - tag_id: postTagRelation.tag_id - }); - } - }); + // CASE: detect duplicate relations + if (!_.find(postToImport[target], {id: relation[fk]})) { + postToImport[target].push({ + id: relation[fk] + }); + } + }); + }; + + run(this.requiredFromFile.posts_tags, 'tags', 'tag_id'); + run(this.requiredFromFile.posts_authors, 'authors', 'author_id'); } /** - * Replace all `tag_id` references. + * Replace all identifier references. */ replaceIdentifiers() { - /** - * {post_id: 1, tag_id: 2} - */ - _.each(this.dataToImport, (postToImport, postIndex) => { - if (!postToImport.tags || !postToImport.tags.length) { + const run = (postToImport, postIndex, targetProperty, tableName) => { + if (!postToImport[targetProperty] || !postToImport[targetProperty].length) { return; } let indexesToRemove = []; - _.each(postToImport.tags, (tag, tagIndex) => { - let tagInFile = _.find(this.requiredFromFile.tags, {id: tag.tag_id}); + _.each(postToImport[targetProperty], (object, index) => { + let objectInFile = _.find(this.requiredFromFile[tableName], {id: object.id}); - if (!tagInFile) { - let existingTag = _.find(this.requiredExistingData.tags, {id: tag.tag_id}); + if (!objectInFile) { + let existingObject = _.find(this.requiredExistingData[tableName], {id: object.id}); - // CASE: tag is not in file, tag is not in db - if (!existingTag) { - indexesToRemove.push(tagIndex); + // CASE: is not in file, is not in db + if (!existingObject) { + indexesToRemove.push(index); return; } else { - this.dataToImport[postIndex].tags[tagIndex].tag_id = existingTag.id; + this.dataToImport[postIndex][targetProperty][index].id = existingObject.id; return; } } // CASE: search through imported data - let importedTag = _.find(this.requiredImportedData.tags, {slug: tagInFile.slug}); + let importedObject = _.find(this.requiredImportedData[tableName], {slug: objectInFile.slug}); - if (importedTag) { - this.dataToImport[postIndex].tags[tagIndex].tag_id = importedTag.id; + if (importedObject) { + this.dataToImport[postIndex][targetProperty][index].id = importedObject.id; return; } // CASE: search through existing data - let existingTag = _.find(this.requiredExistingData.tags, {slug: tagInFile.slug}); + let existingObject = _.find(this.requiredExistingData[tableName], {slug: objectInFile.slug}); - if (existingTag) { - this.dataToImport[postIndex].tags[tagIndex].tag_id = existingTag.id; + if (existingObject) { + this.dataToImport[postIndex][targetProperty][index].id = existingObject.id; } else { - indexesToRemove.push(tagIndex); + indexesToRemove.push(index); } }); - this.dataToImport[postIndex].tags = _.filter(this.dataToImport[postIndex].tags, ((tag, index) => { + this.dataToImport[postIndex][targetProperty] = _.filter(this.dataToImport[postIndex][targetProperty], ((object, index) => { return indexesToRemove.indexOf(index) === -1; })); + }; + + _.each(this.dataToImport, (postToImport, postIndex) => { + run(postToImport, postIndex, 'tags', 'tags'); + run(postToImport, postIndex, 'authors', 'users'); }); return super.replaceIdentifiers(); diff --git a/core/server/data/meta/author_fb_url.js b/core/server/data/meta/author_fb_url.js index a76f7ac977..7f164de868 100644 --- a/core/server/data/meta/author_fb_url.js +++ b/core/server/data/meta/author_fb_url.js @@ -5,8 +5,8 @@ function getAuthorFacebookUrl(data) { var context = data.context ? data.context : null, contextObject = getContextObject(data, context); - if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.author && contextObject.author.facebook) { - return contextObject.author.facebook; + if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.primary_author && contextObject.primary_author.facebook) { + return contextObject.primary_author.facebook; } else if (_.includes(context, 'author') && contextObject.facebook) { return contextObject.facebook; } diff --git a/core/server/data/meta/author_image.js b/core/server/data/meta/author_image.js index 13ea3b27d3..287493f2e7 100644 --- a/core/server/data/meta/author_image.js +++ b/core/server/data/meta/author_image.js @@ -6,8 +6,8 @@ function getAuthorImage(data, absolute) { var context = data.context ? data.context : null, contextObject = getContextObject(data, context); - if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.author && contextObject.author.profile_image) { - return urlService.utils.urlFor('image', {image: contextObject.author.profile_image}, absolute); + if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.primary_author && contextObject.primary_author.profile_image) { + return urlService.utils.urlFor('image', {image: contextObject.primary_author.profile_image}, absolute); } return null; } diff --git a/core/server/data/meta/author_url.js b/core/server/data/meta/author_url.js index 4aa950dc08..a7aa3a91c3 100644 --- a/core/server/data/meta/author_url.js +++ b/core/server/data/meta/author_url.js @@ -8,8 +8,9 @@ function getAuthorUrl(data, absolute) { if (data.author) { return urlService.utils.urlFor('author', {author: data.author}, absolute); } - if (data[context] && data[context].author) { - return urlService.utils.urlFor('author', {author: data[context].author}, absolute); + + if (data[context] && data[context].primary_author) { + return urlService.utils.urlFor('author', {author: data[context].primary_author}, absolute); } return null; } diff --git a/core/server/data/meta/creator_url.js b/core/server/data/meta/creator_url.js index d0fb61ca3d..a819a2f572 100644 --- a/core/server/data/meta/creator_url.js +++ b/core/server/data/meta/creator_url.js @@ -5,8 +5,8 @@ function getCreatorTwitterUrl(data) { var context = data.context ? data.context : null, contextObject = getContextObject(data, context); - if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.author && contextObject.author.twitter) { - return contextObject.author.twitter; + if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.primary_author && contextObject.primary_author.twitter) { + return contextObject.primary_author.twitter; } else if (_.includes(context, 'author') && contextObject.twitter) { return contextObject.twitter; } diff --git a/core/server/data/meta/index.js b/core/server/data/meta/index.js index ae808cfc96..60b74a932b 100644 --- a/core/server/data/meta/index.js +++ b/core/server/data/meta/index.js @@ -90,8 +90,8 @@ function getMetaData(data, root) { metaData.excerpt = customExcerpt ? customExcerpt : metaDescription ? metaDescription : fallbackExcerpt; } - if (data.post && data.post.author && data.post.author.name) { - metaData.authorName = data.post.author.name; + if (data.post && data.post.primary_author && data.post.primary_author.name) { + metaData.authorName = data.post.primary_author.name; } return Promise.props(getImageDimensions(metaData)).then(function () { diff --git a/core/server/data/meta/schema.js b/core/server/data/meta/schema.js index 847106c4b8..9974f64539 100644 --- a/core/server/data/meta/schema.js +++ b/core/server/data/meta/schema.js @@ -39,14 +39,14 @@ function trimSameAs(data, context) { var sameAs = []; if (context === 'post') { - if (data.post.author.website) { - sameAs.push(escapeExpression(data.post.author.website)); + if (data.post.primary_author.website) { + sameAs.push(escapeExpression(data.post.primary_author.website)); } - if (data.post.author.facebook) { - sameAs.push(social.urls.facebook(data.post.author.facebook)); + if (data.post.primary_author.facebook) { + sameAs.push(social.urls.facebook(data.post.primary_author.facebook)); } - if (data.post.author.twitter) { - sameAs.push(social.urls.twitter(data.post.author.twitter)); + if (data.post.primary_author.twitter) { + sameAs.push(social.urls.twitter(data.post.primary_author.twitter)); } } else if (context === 'author') { if (data.author.website) { @@ -79,12 +79,12 @@ function getPostSchema(metaData, data) { }, author: { '@type': 'Person', - name: escapeExpression(data.post.author.name), + name: escapeExpression(data.post.primary_author.name), image: schemaImageObject(metaData.authorImage), url: metaData.authorUrl, sameAs: trimSameAs(data, 'post'), - description: data.post.author.metaDescription ? - escapeExpression(data.post.author.metaDescription) : + description: data.post.primary_author.metaDescription ? + escapeExpression(data.post.primary_author.metaDescription) : null }, headline: escapeExpression(metaData.metaTitle), diff --git a/core/server/data/migrations/versions/1.22/1-multiple-authors-DDL.js b/core/server/data/migrations/versions/1.22/1-multiple-authors-DDL.js new file mode 100644 index 0000000000..e480ed95d7 --- /dev/null +++ b/core/server/data/migrations/versions/1.22/1-multiple-authors-DDL.js @@ -0,0 +1,38 @@ +'use strict'; + +const Promise = require('bluebird'), + common = require('../../../../lib/common'), + commands = require('../../../schema').commands, + table = 'posts_authors', + message1 = 'Adding table: ' + table, + message2 = 'Dropping table: ' + table; + +module.exports.up = function addMultipleAuthorsTable(options) { + let connection = options.connection; + + return connection.schema.hasTable(table) + .then(function (exists) { + if (exists) { + common.logging.warn(message1); + return Promise.resolve(); + } + + common.logging.info(message1); + return commands.createTable(table, connection); + }); +}; + +module.exports.down = function removeMultipleAuthorsTable(options) { + let connection = options.connection; + + return connection.schema.hasTable(table) + .then(function (exists) { + if (!exists) { + common.logging.warn(message2); + return Promise.resolve(); + } + + common.logging.info(message2); + return commands.deleteTable(table, connection); + }); +}; diff --git a/core/server/data/migrations/versions/1.22/1-multiple-authors-DML.js b/core/server/data/migrations/versions/1.22/1-multiple-authors-DML.js new file mode 100644 index 0000000000..dcce6a7991 --- /dev/null +++ b/core/server/data/migrations/versions/1.22/1-multiple-authors-DML.js @@ -0,0 +1,55 @@ +'use strict'; + +const _ = require('lodash'), + Promise = require('bluebird'), + common = require('../../../../lib/common'), + models = require('../../../../models'); + +module.exports.config = { + transaction: true +}; + +module.exports.up = function handleMultipleAuthors(options) { + const postAllColumns = ['id', 'author_id'], + userColumns = ['id']; + + let localOptions = _.merge({ + context: {internal: true} + }, options); + + return models.User.getOwnerUser(_.merge({columns: userColumns}, localOptions)) + .then(function (ownerUser) { + return models.Post.findAll(_.merge({columns: postAllColumns}, localOptions)) + .then(function (posts) { + common.logging.info('Adding `posts_authors` relations'); + + return Promise.map(posts.models, function (post) { + let authorIdToSet; + + // CASE: ensure `post.author_id` is a valid user id + return models.User.findOne({id: post.get('author_id')}, _.merge({columns: userColumns}, localOptions)) + .then(function (user) { + if (!user) { + authorIdToSet = ownerUser.id; + } else { + authorIdToSet = post.get('author_id'); + } + }) + .then(function () { + // CASE: insert primary author + return models.Post.edit({ + author_id: authorIdToSet, + authors: [{ + id: post.get('author_id') + }] + }, _.merge({id: post.id}, localOptions)); + }); + }, {concurrency: 100}); + }); + }); +}; + +module.exports.down = function handleMultipleAuthors(options) { + common.logging.info('Removing `posts_authors` relations'); + return options.connection('posts_authors').truncate(); +}; diff --git a/core/server/data/schema/default-settings.json b/core/server/data/schema/default-settings.json index a9ccab3c02..44c6d9d391 100644 --- a/core/server/data/schema/default-settings.json +++ b/core/server/data/schema/default-settings.json @@ -60,7 +60,7 @@ "defaultValue": "/:slug/", "validations": { "matches": "^(\/:?[a-z0-9_-]+){1,5}\/$", - "matches": "(:id|:slug|:year|:month|:day|:author|:primary_tag)", + "matches": "(:id|:slug|:year|:month|:day|:author|:primary_tag|:primary_author)", "notContains": "/ghost/" } }, diff --git a/core/server/data/schema/fixtures/fixtures.json b/core/server/data/schema/fixtures/fixtures.json index d95a2bb38f..8ceb5792d6 100644 --- a/core/server/data/schema/fixtures/fixtures.json +++ b/core/server/data/schema/fixtures/fixtures.json @@ -1,108 +1,5 @@ { "models": [ - { - "name": "Post", - "entries": [ - { - "title": "Setting up your own Ghost theme", - "slug": "themes", - "mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"card-markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"Creating a totally custom design for your publication\\n\\nGhost comes with a beautiful default theme called Casper, which is designed to be a clean, readable publication layout and can be easily adapted for most purposes. However, Ghost can also be completely themed to suit your needs. Rather than just giving you a few basic settings which act as a poor proxy for code, we just let you write code.\\n\\nThere are a huge range of both free and premium pre-built themes which you can get from the [Ghost Theme Marketplace](http:\/\/marketplace.ghost.org), or you can simply create your own from scratch.\\n\\n[![marketplace](https:\/\/casper.ghost.org\/v1.0.0\/images\/marketplace.jpg)](http:\/\/marketplace.ghost.org)\\n\\n> Anyone can write a completely custom Ghost theme, with just some solid knowledge of HTML and CSS\\n\\nGhost themes are written with a templating language called handlebars, which has a bunch of dynamic helpers to insert your data into template files. Like `{{author.name}}`, for example, outputs the name of the current author.\\n\\nThe best way to learn how to write your own Ghost theme is to have a look at [the source code for Casper](https:\/\/github.com\/TryGhost\/Casper), which is heavily commented and should give you a sense of how everything fits together.\\n\\n- `default.hbs` is the main template file, all contexts will load inside this file unless specifically told to use a different template.\\n- `post.hbs` is the file used in the context of viewing a post.\\n- `index.hbs` is the file used in the context of viewing the home page.\\n- and so on\\n\\nWe've got [full and extensive theme documentation](http:\/\/themes.ghost.org\/docs\/about) which outlines every template file, context and helper that you can use.\\n\\nIf you want to chat with other people making Ghost themes to get any advice or help, there's also a **#themes** channel in our [public Slack community](https:\/\/slack.ghost.org) which we always recommend joining!\"}]],\"sections\":[[10,0]]}", - "feature_image": "https://casper.ghost.org/v1.0.0/images/design.jpg", - "featured": false, - "page": false, - "status": "published", - "meta_title": null, - "meta_description": null, - "created_by": "5951f5fca366002ebd5dbef7", - "published_by": "5951f5fca366002ebd5dbef7", - "author_id": "5951f5fca366002ebd5dbef7" - }, - { - "title": "Advanced Markdown tips", - "slug": "advanced-markdown", - "mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"card-markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"There are lots of powerful things you can do with the Ghost editor\\n\\nIf you've gotten pretty comfortable with [all the basics](\/the-editor\/) of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!\\n\\nAs with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.\\n\\n\\n## Special formatting\\n\\nAs well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:\\n\\n+ ~~strike through~~\\n+ ==highlight==\\n+ \\\\*escaped characters\\\\*\\n\\n\\n## Writing code blocks\\n\\nThere are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, `like this`. Larger snippets of code can be displayed across multiple lines using triple back ticks:\\n\\n```\\n.my-link {\\n text-decoration: underline;\\n}\\n```\\n\\nIf you want to get really fancy, you can even add syntax highlighting using [Prism.js](http:\/\/prismjs.com\/).\\n\\n\\n## Full bleed images\\n\\nOne neat trick which you can use in Markdown to distinguish between different types of images is to add a `#hash` value to the end of the source URL, and then target images containing the hash with special styling. For example:\\n\\n![walking](https:\/\/casper.ghost.org\/v1.0.0\/images\/walking.jpg#full)\\n\\nwhich is styled with...\\n\\n```\\nimg[src$=\\\"#full\\\"] {\\n max-width: 100vw;\\n}\\n```\\n\\nThis creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.\\n\\n\\n## Reference lists\\n\\n**The quick brown [fox][1], jumped over the lazy [dog][2].**\\n\\n[1]: https:\/\/en.wikipedia.org\/wiki\/Fox \\\"Wikipedia: Fox\\\"\\n[2]: https:\/\/en.wikipedia.org\/wiki\/Dog \\\"Wikipedia: Dog\\\"\\n\\nAnother way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.\\n\\n\\n## Creating footnotes\\n\\nThe quick brown fox[^1] jumped over the lazy dog[^2].\\n\\n[^1]: Foxes are red\\n[^2]: Dogs are usually not red\\n\\nFootnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.\\n\\n\\n## Full HTML\\n\\nPerhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:\\n\\n\\n\"}]],\"sections\":[[10,0]]}", + "html": "

There are lots of powerful things you can do with the Ghost editor

\n

If you've gotten pretty comfortable with all the basics of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!

\n

As with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.

\n

Special formatting

\n

As well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:

\n\n

Writing code blocks

\n

There are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, like this. Larger snippets of code can be displayed across multiple lines using triple back ticks:

\n
.my-link {\n    text-decoration: underline;\n}\n
\n

If you want to get really fancy, you can even add syntax highlighting using Prism.js.

\n

Full bleed images

\n

One neat trick which you can use in Markdown to distinguish between different types of images is to add a #hash value to the end of the source URL, and then target images containing the hash with special styling. For example:

\n

\"walking\"

\n

which is styled with...

\n
img[src$="#full"] {\n    max-width: 100vw;\n}\n
\n

This creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.

\n

Reference lists

\n

The quick brown fox, jumped over the lazy dog.

\n

Another way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.

\n

Creating footnotes

\n

The quick brown fox[1] jumped over the lazy dog[2].

\n

Footnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.

\n

Full HTML

\n

Perhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:

\n\n
\n
\n
    \n
  1. Foxes are red ↩︎

    \n
  2. \n
  3. Dogs are usually not red ↩︎

    \n
  4. \n
\n
\n
", + "amp": null, + "plaintext": "There are lots of powerful things you can do with the Ghost editor\n\nIf you've gotten pretty comfortable with all the basics [/the-editor/] of\nwriting in Ghost, then you may enjoy some more advanced tips about the types of\nthings you can do with Markdown!\n\nAs with the last post about the editor, you'll want to be actually editing this\npost as you read it so that you can see all the Markdown code we're using.\n\nSpecial formatting\nAs well as bold and italics, you can also use some other special formatting in\nMarkdown when the need arises, for example:\n\n * strike through\n * highlight\n * *escaped characters*\n\nWriting code blocks\nThere are two types of code elements which can be inserted in Markdown, the\nfirst is inline, and the other is block. Inline code is formatted by wrapping\nany word or words in back-ticks, like this. Larger snippets of code can be\ndisplayed across multiple lines using triple back ticks:\n\n.my-link {\n text-decoration: underline;\n}\n\n\nIf you want to get really fancy, you can even add syntax highlighting using \nPrism.js [http://prismjs.com/].\n\nFull bleed images\nOne neat trick which you can use in Markdown to distinguish between different\ntypes of images is to add a #hash value to the end of the source URL, and then\ntarget images containing the hash with special styling. For example:\n\n\n\nwhich is styled with...\n\nimg[src$=\"#full\"] {\n max-width: 100vw;\n}\n\n\nThis creates full-bleed images in the Casper theme, which stretch beyond their\nusual boundaries right up to the edge of the window. Every theme handles these\ntypes of things slightly differently, but it's a great trick to play with if you\nwant to have a variety of image sizes and styles.\n\nReference lists\nThe quick brown fox [https://en.wikipedia.org/wiki/Fox], jumped over the lazy \ndog [https://en.wikipedia.org/wiki/Dog].\n\nAnother way to insert links in markdown is using reference lists. You might want\nto use this style of linking to cite reference material in a Wikipedia-style.\nAll of the links are listed at the end of the document, so you can maintain full\nseparation between content and its source or reference.\n\nCreating footnotes\nThe quick brown fox[1] jumped over the lazy dog[2].\n\nFootnotes are a great way to add additional contextual details when appropriate.\nGhost will automatically add footnote content to the very end of your post.\n\nFull HTML\nPerhaps the best part of Markdown is that you're never limited to just Markdown.\nYou can write HTML directly in the Ghost editor and it will just work as HTML\nusually does. No limits! Here's a standard YouTube embed code as an example:\n\n\n--------------------------------------------------------------------------------\n\n 1. Foxes are red ↩︎\n \n \n 2. Dogs are usually not red ↩︎", + "feature_image": "https://casper.ghost.org/v1.0.0/images/advanced.jpg", + "featured": 0, + "page": 0, + "status": "published", + "locale": null, + "visibility": "public", + "meta_title": null, + "meta_description": null, + "author_id": "5951f5fca366002ebd5dbef7", + "created_at": "2017-09-01T12:29:50.000Z", + "created_by": "5951f5fca366002ebd5dbef7", + "updated_at": "2017-09-01T12:29:50.000Z", + "updated_by": "5951f5fca366002ebd5dbef7", + "published_at": "2017-09-01T12:29:51.000Z", + "published_by": "5951f5fca366002ebd5dbef7", + "custom_excerpt": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null + }, + { + "id": "59a952be7d79ed06b0d21129", + "uuid": "8a6fdc10-fcde-48ba-b662-4d366cef5653", + "title": "Advanced Markdown tips", + "slug": "advanced-markdown-2", + "mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"card-markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"There are lots of powerful things you can do with the Ghost editor\\n\\nIf you've gotten pretty comfortable with [all the basics](/the-editor/) of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!\\n\\nAs with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.\\n\\n\\n## Special formatting\\n\\nAs well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:\\n\\n+ ~~strike through~~\\n+ ==highlight==\\n+ \\\\*escaped characters\\\\*\\n\\n\\n## Writing code blocks\\n\\nThere are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, `like this`. Larger snippets of code can be displayed across multiple lines using triple back ticks:\\n\\n```\\n.my-link {\\n text-decoration: underline;\\n}\\n```\\n\\nIf you want to get really fancy, you can even add syntax highlighting using [Prism.js](http://prismjs.com/).\\n\\n\\n## Full bleed images\\n\\nOne neat trick which you can use in Markdown to distinguish between different types of images is to add a `#hash` value to the end of the source URL, and then target images containing the hash with special styling. For example:\\n\\n![walking](https://casper.ghost.org/v1.0.0/images/walking.jpg#full)\\n\\nwhich is styled with...\\n\\n```\\nimg[src$=\\\"#full\\\"] {\\n max-width: 100vw;\\n}\\n```\\n\\nThis creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.\\n\\n\\n## Reference lists\\n\\n**The quick brown [fox][1], jumped over the lazy [dog][2].**\\n\\n[1]: https://en.wikipedia.org/wiki/Fox \\\"Wikipedia: Fox\\\"\\n[2]: https://en.wikipedia.org/wiki/Dog \\\"Wikipedia: Dog\\\"\\n\\nAnother way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.\\n\\n\\n## Creating footnotes\\n\\nThe quick brown fox[^1] jumped over the lazy dog[^2].\\n\\n[^1]: Foxes are red\\n[^2]: Dogs are usually not red\\n\\nFootnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.\\n\\n\\n## Full HTML\\n\\nPerhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:\\n\\n\\n\"}]],\"sections\":[[10,0]]}", + "html": "

There are lots of powerful things you can do with the Ghost editor

\n

If you've gotten pretty comfortable with all the basics of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!

\n

As with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.

\n

Special formatting

\n

As well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:

\n\n

Writing code blocks

\n

There are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, like this. Larger snippets of code can be displayed across multiple lines using triple back ticks:

\n
.my-link {\n    text-decoration: underline;\n}\n
\n

If you want to get really fancy, you can even add syntax highlighting using Prism.js.

\n

Full bleed images

\n

One neat trick which you can use in Markdown to distinguish between different types of images is to add a #hash value to the end of the source URL, and then target images containing the hash with special styling. For example:

\n

\"walking\"

\n

which is styled with...

\n
img[src$="#full"] {\n    max-width: 100vw;\n}\n
\n

This creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.

\n

Reference lists

\n

The quick brown fox, jumped over the lazy dog.

\n

Another way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.

\n

Creating footnotes

\n

The quick brown fox[1] jumped over the lazy dog[2].

\n

Footnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.

\n

Full HTML

\n

Perhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:

\n\n
\n
\n
    \n
  1. Foxes are red ↩︎

    \n
  2. \n
  3. Dogs are usually not red ↩︎

    \n
  4. \n
\n
\n
", + "amp": null, + "plaintext": "There are lots of powerful things you can do with the Ghost editor\n\nIf you've gotten pretty comfortable with all the basics [/the-editor/] of\nwriting in Ghost, then you may enjoy some more advanced tips about the types of\nthings you can do with Markdown!\n\nAs with the last post about the editor, you'll want to be actually editing this\npost as you read it so that you can see all the Markdown code we're using.\n\nSpecial formatting\nAs well as bold and italics, you can also use some other special formatting in\nMarkdown when the need arises, for example:\n\n * strike through\n * highlight\n * *escaped characters*\n\nWriting code blocks\nThere are two types of code elements which can be inserted in Markdown, the\nfirst is inline, and the other is block. Inline code is formatted by wrapping\nany word or words in back-ticks, like this. Larger snippets of code can be\ndisplayed across multiple lines using triple back ticks:\n\n.my-link {\n text-decoration: underline;\n}\n\n\nIf you want to get really fancy, you can even add syntax highlighting using \nPrism.js [http://prismjs.com/].\n\nFull bleed images\nOne neat trick which you can use in Markdown to distinguish between different\ntypes of images is to add a #hash value to the end of the source URL, and then\ntarget images containing the hash with special styling. For example:\n\n\n\nwhich is styled with...\n\nimg[src$=\"#full\"] {\n max-width: 100vw;\n}\n\n\nThis creates full-bleed images in the Casper theme, which stretch beyond their\nusual boundaries right up to the edge of the window. Every theme handles these\ntypes of things slightly differently, but it's a great trick to play with if you\nwant to have a variety of image sizes and styles.\n\nReference lists\nThe quick brown fox [https://en.wikipedia.org/wiki/Fox], jumped over the lazy \ndog [https://en.wikipedia.org/wiki/Dog].\n\nAnother way to insert links in markdown is using reference lists. You might want\nto use this style of linking to cite reference material in a Wikipedia-style.\nAll of the links are listed at the end of the document, so you can maintain full\nseparation between content and its source or reference.\n\nCreating footnotes\nThe quick brown fox[1] jumped over the lazy dog[2].\n\nFootnotes are a great way to add additional contextual details when appropriate.\nGhost will automatically add footnote content to the very end of your post.\n\nFull HTML\nPerhaps the best part of Markdown is that you're never limited to just Markdown.\nYou can write HTML directly in the Ghost editor and it will just work as HTML\nusually does. No limits! Here's a standard YouTube embed code as an example:\n\n\n--------------------------------------------------------------------------------\n\n 1. Foxes are red ↩︎\n \n \n 2. Dogs are usually not red ↩︎", + "feature_image": "https://casper.ghost.org/v1.0.0/images/advanced.jpg", + "featured": 0, + "page": 0, + "status": "published", + "locale": null, + "visibility": "public", + "meta_title": null, + "meta_description": null, + "author_id": "5951f5fca366002ebd5dbef7", + "created_at": "2017-09-01T12:29:50.000Z", + "created_by": "5951f5fca366002ebd5dbef7", + "updated_at": "2017-09-01T12:29:50.000Z", + "updated_by": "5951f5fca366002ebd5dbef7", + "published_at": "2017-09-01T12:29:51.000Z", + "published_by": "5951f5fca366002ebd5dbef7", + "custom_excerpt": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null + } + ], + "posts_tags": [ + { + "id": "59a952bf7d79ed06b0d211cb", + "post_id": "59a952be7d79ed06b0d21127", + "tag_id": "59a952be7d79ed06b0d2112e", + "sort_order": 0 + } + ], + "tags": [ + { + "id": "59a952be7d79ed06b0d2112e", + "name": "tag1", + "slug": "tag1", + "description": null, + "feature_image": "/content/images/2017/05/tagImage-1.jpeg", + "parent_id": null, + "visibility": "public", + "meta_title": null, + "meta_description": null, + "created_at": "2017-05-30T08:44:09.000Z", + "created_by": "5951f5fca366002ebd5dbef7", + "updated_at": "2017-05-30T12:03:10.000Z", + "updated_by": "5951f5fca366002ebd5dbef7" + } + ], + "users": [ + { + "id": "5951f5fca366002ebd5dbef7", + "name": "Joe Blogg's Brother", + "slug": "joe-bloggs-brother", + "ghost_auth_access_token": null, + "ghost_auth_id": null, + "password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC", + "email": "jbloggsbrother@example.com", + "profile_image": "/content/images/2017/05/authorlogo.jpeg", + "cover_image": "/content/images/2017/05/authorcover.jpeg", + "bio": "I'm Joe's brother, the good looking one!", + "website": "http://joebloggsbrother.com", + "location": null, + "facebook": null, + "twitter": null, + "accessibility": null, + "status": "active", + "locale": "en_US", + "visibility": "public", + "meta_title": null, + "meta_description": null, + "tour": "[\"getting-started\",\"using-the-editor\",\"static-post\",\"featured-post\",\"upload-a-theme\"]", + "last_seen": "2017-09-01T12:30:37.000Z", + "created_at": "2017-09-01T12:29:51.000Z", + "created_by": "1", + "updated_at": "2017-09-01T12:30:59.000Z", + "updated_by": "5951f5fca366002ebd5dbef8" + }, + { + "id": "5951f5fca366002ebd5dbef8", + "name": "Joe Blogg's Mother", + "slug": "joe-bloggs-mother", + "ghost_auth_access_token": null, + "ghost_auth_id": null, + "password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC", + "email": "jbloggsmother@example.com", + "profile_image": "/content/images/2017/05/authorlogo.jpeg", + "cover_image": "/content/images/2017/05/authorcover.jpeg", + "bio": "I'm Joe's brother, the good looking one!", + "website": "http://joebloggsmother.com", + "location": null, + "facebook": null, + "twitter": null, + "accessibility": null, + "status": "active", + "locale": "en_US", + "visibility": "public", + "meta_title": null, + "meta_description": null, + "tour": "[\"getting-started\",\"using-the-editor\",\"static-post\",\"featured-post\",\"upload-a-theme\"]", + "last_seen": "2017-08-01T12:30:37.000Z", + "created_at": "2017-09-01T12:29:51.000Z", + "created_by": "1", + "updated_at": "2017-09-01T12:30:59.000Z", + "updated_by": "1" + }, + { + "id": "5951f5fca366002ebd5dbef9", + "name": "Joe Blogg's Father", + "slug": "joe-bloggs-father", + "ghost_auth_access_token": null, + "ghost_auth_id": null, + "password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC", + "email": "jbloggsfather@example.com", + "profile_image": "/content/images/2017/05/authorlogo.jpeg", + "cover_image": "/content/images/2017/05/authorcover.jpeg", + "bio": "I'm Joe's father, the good looking one!", + "website": "http://joebloggsfather.com", + "location": null, + "facebook": null, + "twitter": null, + "accessibility": null, + "status": "active", + "locale": "en_US", + "visibility": "public", + "meta_title": null, + "meta_description": null, + "tour": "[\"getting-started\",\"using-the-editor\",\"static-post\",\"featured-post\",\"upload-a-theme\"]", + "last_seen": "2017-07-01T12:30:37.000Z", + "created_at": "2017-09-01T12:29:51.000Z", + "created_by": "1", + "updated_at": "2017-09-01T12:30:59.000Z", + "updated_by": "1" + }, + { + "id": "5951f5fca366002ebd5dbef10", + "name": "Joe Blogg's Father", + "slug": "joe-bloggs-father", + "ghost_auth_access_token": null, + "ghost_auth_id": null, + "password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC", + "email": "jbloggsfather@example.com", + "profile_image": "/content/images/2017/05/authorlogo.jpeg", + "cover_image": "/content/images/2017/05/authorcover.jpeg", + "bio": "I'm Joe's father, the good looking one!", + "website": "http://joebloggsfather.com", + "location": null, + "facebook": null, + "twitter": null, + "accessibility": null, + "status": "active", + "locale": "en_US", + "visibility": "public", + "meta_title": null, + "meta_description": null, + "tour": "[\"getting-started\",\"using-the-editor\",\"static-post\",\"featured-post\",\"upload-a-theme\"]", + "last_seen": "2017-07-01T12:30:37.000Z", + "created_at": "2017-09-01T12:29:51.000Z", + "created_by": "1", + "updated_at": "2017-09-01T12:30:59.000Z", + "updated_by": "1" + } + ], + "posts_authors": [ + { + "id": "5a8c0aa22a49c40927b18474", + "post_id": "59a952be7d79ed06b0d21127", + "author_id": "5951f5fca366002ebd5dbef8", + "sort_order": 0 + }, + { + "id": "5a8c0aa22a49c40927b18474", + "post_id": "59a952be7d79ed06b0d21127", + "author_id": "5951f5fca366002ebd5dbef8", + "sort_order": 0 + }, + { + "id": "5a8c0aa22a49c40927b18475", + "post_id": "unknown", + "author_id": "5951f5fca366002ebd5dbef7", + "sort_order": 1 + }, + { + "id": "5a8c0aa22a49c40927b18476", + "post_id": "59a952be7d79ed06b0d21127", + "author_id": "5951f5fca366002ebd5dbef7", + "sort_order": 2 + }, + { + "id": "5a8c0aa22a49c40927b18477", + "post_id": "59a952be7d79ed06b0d21127", + "author_id": "5951f5fca366002ebd5dbef9", + "sort_order": 4 + }, + { + "id": "5a8c0aa22a49c40927b18478", + "post_id": "59a952be7d79ed06b0d21129", + "author_id": "5951f5fca366002ebd5dbef9", + "sort_order": 0 + }, + { + "id": "5a8c0aa22a49c40927b18479", + "post_id": "59a952be7d79ed06b0d21129", + "author_id": "5951f5fca366002ebd5dbefff", + "sort_order": 1 + }, { + "id": "5a8c0aa22a49c40927b18479", + "post_id": "59a952be7d79ed06b0d21129", + "author_id": "5951f5fca366002ebd5dbef10", + "sort_order": 2 + } + ] + } + } + ] +} diff --git a/core/test/utils/fixtures/filter-param/index.js b/core/test/utils/fixtures/filter-param/index.js index a95920cd65..3970752fa7 100644 --- a/core/test/utils/fixtures/filter-param/index.js +++ b/core/test/utils/fixtures/filter-param/index.js @@ -1,9 +1,9 @@ /** * These fixtures are just for testing the filter spec */ -var _ = require('lodash'), +var _ = require('lodash'), ObjectId = require('bson-objectid'), - db = require('../../../../server/data/db'), + db = require('../../../../server/data/db'), markdownToMobiledoc = require('../../../utils/fixtures/data-generator').markdownToMobiledoc, data = {}; @@ -317,7 +317,8 @@ function createTags(knex, DataGenerator) { } function createPosts(knex, DataGenerator) { - var postsTags = []; + var postsTags = [], postsAuthors = []; + data.posts = _.map(data.posts, function (post) { post = DataGenerator.forKnex.createPost(post); @@ -333,10 +334,20 @@ function createPosts(knex, DataGenerator) { return post; }); + _.each(data.posts, function (post) { + postsAuthors.push({ + id: ObjectId.generate(), + post_id: post.id, + author_id: post.author_id + }); + }); + // Next, insert it into the database & return the correctly indexed data return writeFetchFix(knex, 'posts').then(function (createdPosts) { return knex('posts_tags').insert(postsTags).then(function () { return createdPosts; + }).then(function () { + return knex('posts_authors').insert(postsAuthors); }); }); } diff --git a/core/test/utils/index.js b/core/test/utils/index.js index 8e7788f755..30120da562 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -68,6 +68,28 @@ fixtures = { return db.knex('tags').insert(DataGenerator.forKnex.tags); }).then(function () { return db.knex('posts_tags').insert(DataGenerator.forKnex.posts_tags); + }).then(function () { + return db.knex('posts_authors').insert(DataGenerator.forKnex.posts_authors) + .catch(function (err) { + var clonedPostsAuthors; + + // CASE: routing tests insert extra posts, but some tests don't add the users from the data generator + // The only users which exist via the default Ghost fixtures are the Owner and the Ghost author + // This results a MySQL error: `ER_NO_REFERENCED_ROW_2` + // @TODO: rework if we overhaul the test env + if (err.errno === 1452) { + clonedPostsAuthors = _.cloneDeep(DataGenerator.forKnex.posts_authors); + + // Fallback to owner user - this user does exist for sure + _.each(clonedPostsAuthors, function (postsAuthorRelation) { + postsAuthorRelation.author_id = DataGenerator.forKnex.users[0].id; + }); + + return db.knex('posts_authors').insert(clonedPostsAuthors); + } + + throw err; + }); }); }, @@ -100,7 +122,14 @@ fixtures = { return sequence(_.times(posts.length, function (index) { return function () { - return db.knex('posts').insert(posts[index]); + return db.knex('posts').insert(posts[index]) + .then(function () { + return db.knex('posts_authors').insert({ + id: ObjectId.generate(), + post_id: posts[index].id, + author_id: posts[index].author_id + }); + }); }; })); }).then(function () { @@ -155,7 +184,14 @@ fixtures = { return sequence(_.times(posts.length, function (index) { return function () { - return db.knex('posts').insert(posts[index]); + return db.knex('posts').insert(posts[index]) + .then(function () { + return db.knex('posts_authors').insert({ + id: ObjectId.generate(), + post_id: posts[index].id, + author_id: posts[index].author_id + }); + }); }; })); }, @@ -635,7 +671,7 @@ initFixtures = function initFixtures() { * @returns {Function} */ setup = function setup() { - var self = this, + const self = this, args = arguments; return function setup() { @@ -703,7 +739,11 @@ createPost = function createPost(options) { return db.knex('posts') .insert(post) .then(function () { - return post; + return db.knex('posts_authors').insert({ + id: ObjectId.generate(), + author_id: post.author_id, + post_id: post.id + }).return(post); }); }; diff --git a/core/test/utils/mocks/knex.js b/core/test/utils/mocks/knex.js index 9f451f2391..9754098530 100644 --- a/core/test/utils/mocks/knex.js +++ b/core/test/utils/mocks/knex.js @@ -18,7 +18,7 @@ class KnexMock { initialiseDb() { this.db = {}; - _.each(_.pick(_.cloneDeep(DataGenerator.forKnex), ['posts', 'users', 'tags', 'permissions', 'roles']), (objects, tableName) => { + _.each(_.pick(_.cloneDeep(DataGenerator.forKnex), ['posts', 'users', 'tags', 'permissions', 'roles', 'posts_authors']), (objects, tableName) => { this.db[tableName] = []; _.each(objects, (object) => { diff --git a/package.json b/package.json index bba3eee08f..a5ca293c6b 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,12 @@ "express-hbs": "1.0.4", "extract-zip": "1.6.6", "fs-extra": "3.0.1", - "ghost-gql": "0.0.8", + "ghost-gql": "0.0.9", "ghost-ignition": "2.9.0", "ghost-storage-base": "0.0.1", "glob": "5.0.15", "got": "7.1.0", - "gscan": "1.3.4", + "gscan": "1.4.0", "html-to-text": "3.3.0", "image-size": "0.6.2", "intl": "1.2.5", diff --git a/yarn.lock b/yarn.lock index fa3b2fb60c..6010b5e274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -115,8 +115,8 @@ analytics-node@2.4.1: superagent-retry "^0.6.0" ansi-escapes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" ansi-regex@^2.0.0: version "2.1.1" @@ -476,10 +476,11 @@ bignumber.js@4.0.4: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.0.4.tgz#7c40f5abcd2d6623ab7b99682ee7db81b11889a4" bl@^1.0.0, bl@^1.0.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" dependencies: - readable-stream "^2.0.5" + readable-stream "^2.3.5" + safe-buffer "^5.1.1" bl@~0.4.1: version "0.4.2" @@ -621,6 +622,10 @@ buffer-crc32@^0.2.1: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -713,8 +718,8 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000815" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000815.tgz#0e218fa133d0d071c886aa041b435258cc746891" + version "1.0.30000820" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000820.tgz#7c20e25cea1768b261b724f82e3a6a253aaa1468" caseless@~0.12.0: version "0.12.0" @@ -847,12 +852,12 @@ cliui@^3.0.3: wrap-ansi "^2.0.0" clone@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" clone@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" co@^4.6.0: version "4.6.0" @@ -952,8 +957,8 @@ commander@2.9.0: graceful-readlink ">= 1.0.0" commander@^2.13.0, commander@^2.9.0, commander@~2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322" + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" @@ -1003,9 +1008,10 @@ concat-stream@1.6.0: typedarray "^0.0.6" concat-stream@^1.4.1, concat-stream@^1.5.0, concat-stream@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26" + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" dependencies: + buffer-from "^1.0.0" inherits "^2.0.3" readable-stream "^2.2.2" typedarray "^0.0.6" @@ -1468,8 +1474,8 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" electron-to-chromium@^1.2.7: - version "1.3.39" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.39.tgz#d7a4696409ca0995e2750156da612c221afad84d" + version "1.3.40" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.40.tgz#1fbd6d97befd72b8a6f921dc38d22413d2f6fddf" emits@^3.0.0: version "3.0.0" @@ -1551,7 +1557,7 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@4.19.0, eslint@^4.0.0: +eslint@4.19.0: version "4.19.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.0.tgz#9e900efb5506812ac374557034ef6f5c3642fc4c" dependencies: @@ -1594,6 +1600,49 @@ eslint@4.19.0, eslint@^4.0.0: table "4.0.2" text-table "~0.2.0" +eslint@^4.0.0: + version "4.19.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.4" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + regexpp "^1.0.1" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "4.0.2" + text-table "~0.2.0" + espree@^3.5.4: version "3.5.4" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" @@ -1952,8 +2001,8 @@ form-data@^2.3.1, form-data@~2.3.1: mime-types "^2.1.12" formidable@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.0.tgz#ce291bfec67c176e282f891ece2c37de0c83ae84" + version "1.2.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" forwarded@~0.1.2: version "0.1.2" @@ -1987,7 +2036,7 @@ fs-extra@^0.26.2: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-minipass@^1.2.3: +fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" dependencies: @@ -2075,9 +2124,9 @@ getsetdeep@~2.0.0: dependencies: typechecker "~2.0.1" -ghost-gql@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ghost-gql/-/ghost-gql-0.0.8.tgz#630410cf1f71ccffbdab3d9d01419981c794b0ce" +ghost-gql@0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/ghost-gql/-/ghost-gql-0.0.9.tgz#46b80b07651e71fac77b94d8fabf688baabf2c32" dependencies: lodash "^4.17.4" @@ -2195,8 +2244,8 @@ global-prefix@^1.0.1: which "^1.2.14" globals@^11.0.1: - version "11.3.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" + version "11.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.4.0.tgz#b85c793349561c16076a3c13549238a27945f1bc" globals@^9.18.0: version "9.18.0" @@ -2482,9 +2531,9 @@ grunt@~0.4.0: underscore.string "~2.2.1" which "~1.0.5" -gscan@1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/gscan/-/gscan-1.3.4.tgz#24bd6f2a2e88d7cb9e6691f45e0d6f2c0de58471" +gscan@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/gscan/-/gscan-1.4.0.tgz#c2df7f422d3094e012f96c8d3be4fa1463d3915f" dependencies: bluebird "^3.4.6" chalk "^1.1.1" @@ -2968,8 +3017,8 @@ is-path-cwd@^1.0.0: resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" dependencies: is-path-inside "^1.0.0" @@ -3537,8 +3586,8 @@ loud-rejection@^1.0.0: signal-exit "^3.0.0" lowercase-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" lru-cache@2: version "2.7.3" @@ -3667,8 +3716,8 @@ methods@^1.1.1, methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" micromatch@^3.0.4: - version "3.1.9" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89" + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -3682,7 +3731,7 @@ micromatch@^3.0.4: object.pick "^1.3.0" regex-not "^1.0.0" snapdragon "^0.8.1" - to-regex "^3.0.1" + to-regex "^3.0.2" "mime-db@>= 1.33.0 < 2", mime-db@~1.33.0: version "1.33.0" @@ -3763,10 +3812,11 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" -minipass@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.1.tgz#5ada97538b1027b4cf7213432428578cb564011f" +minipass@^2.2.1, minipass@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40" dependencies: + safe-buffer "^5.1.1" yallist "^3.0.0" minizlib@^1.1.0: @@ -4653,8 +4703,8 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 supports-color "^3.2.3" postcss@^6.0.14: - version "6.0.20" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.20.tgz#686107e743a12d5530cb68438c590d5b2bf72c3c" + version "6.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d" dependencies: chalk "^2.3.2" source-map "^0.6.1" @@ -4668,8 +4718,8 @@ posthtml-parser@^0.2.0: isobject "^2.1.0" posthtml-render@^1.0.5: - version "1.1.1" - resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.1.1.tgz#a5ff704a6787c835a476eebff747e39f14069788" + version "1.1.2" + resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.1.2.tgz#1755717c3ad12f4e6d8846767fa2f0bafdd7f33f" posthtml@^0.9.0: version "0.9.2" @@ -4887,7 +4937,7 @@ readable-stream@2.3.3: string_decoder "~1.0.3" util-deprecate "~1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.5: version "2.3.5" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" dependencies: @@ -5039,8 +5089,8 @@ resolve@1.1.7, resolve@1.1.x, resolve@~1.1.0: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolve@^1.1.6, resolve@^1.1.7, resolve@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + version "1.6.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c" dependencies: path-parse "^1.0.5" @@ -5697,14 +5747,15 @@ tar@^2.0.0: inherits "2" tar@^4: - version "4.4.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.0.tgz#3aaf8c29b6b800a8215f33efb4df1c95ce2ac2f5" + version "4.4.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749" dependencies: chownr "^1.0.1" - fs-minipass "^1.2.3" - minipass "^2.2.1" + fs-minipass "^1.2.5" + minipass "^2.2.4" minizlib "^1.1.0" mkdirp "^0.5.0" + safe-buffer "^5.1.1" yallist "^3.0.2" tarn@^1.1.2: @@ -5788,7 +5839,7 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" -to-regex@^3.0.1: +to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" dependencies: