From f566729ed6261b27e8c46516083eb062e5fb39b0 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Mon, 11 Sep 2023 13:04:34 +0200 Subject: [PATCH] Fixed recommendation order (#18060) fixes https://github.com/TryGhost/Product/issues/3851 - Order was not applied via the CRUD plugin - Removed usage of CRUD findAll, and swapped to Bookshelf fetchAll instead, to decrease dependencies of invisible Bookshelf plugins logic - Reverted page and limit options possibility via findAll method --- .../src/BookshelfRepository.ts | 53 ++++++++--- .../test/BookshelfRepository.test.ts | 30 ++++++- .../core/server/models/base/plugins/crud.js | 6 -- .../server/models/base/plugins/sanitize.js | 2 +- .../recommendations.test.js.snap | 90 +++++++++---------- .../e2e-api/admin/recommendations.test.js | 3 +- .../recommendations.test.js.snap | 36 ++++---- .../e2e-api/content/recommendations.test.js | 3 +- 8 files changed, 135 insertions(+), 88 deletions(-) diff --git a/ghost/bookshelf-repository/src/BookshelfRepository.ts b/ghost/bookshelf-repository/src/BookshelfRepository.ts index 8696c34d3e..6887b5d8c4 100644 --- a/ghost/bookshelf-repository/src/BookshelfRepository.ts +++ b/ghost/bookshelf-repository/src/BookshelfRepository.ts @@ -1,3 +1,5 @@ +import {Knex} from 'knex'; + type Entity = { id: T; deleted: boolean; @@ -11,9 +13,12 @@ type Order = { export type ModelClass = { destroy: (data: {id: T}) => Promise; findOne: (data: {id: T}, options?: {require?: boolean}) => Promise | null>; - findAll: (options: {filter?: string; order?: string, page?: number, limit?: number | 'all'}) => Promise[]>; add: (data: object) => Promise>; - getFilteredCollection: (options: {filter?: string}) => {count(): Promise}; + getFilteredCollection: (options: {filter?: string}) => { + count(): Promise, + query: (f: (q: Knex.QueryBuilder) => void) => void, + fetchAll: () => Promise[]> + }; } export type ModelInstance = { @@ -64,22 +69,44 @@ export abstract class BookshelfRepository> { return model ? this.modelToEntity(model) : null; } - async getAll({filter, order}: { filter?: string; order?: OrderOption } = {}): Promise { - const models = await this.Model.findAll({ - filter, - order: this.#orderToString(order) - }) as ModelInstance[]; + async #fetchAll({filter, order, page, limit}: { filter?: string; order?: OrderOption; page?: number; limit?: number }): Promise { + const collection = this.Model.getFilteredCollection({filter}); + const orderString = this.#orderToString(order); + + if ((limit && page) || orderString) { + collection + .query((q) => { + if (limit && page) { + q.limit(limit); + q.offset(limit * (page - 1)); + } + + if (orderString) { + q.orderByRaw( + orderString + ); + } + }); + } + + const models = await collection.fetchAll(); return (await Promise.all(models.map(model => this.modelToEntity(model)))).filter(entity => !!entity) as T[]; } - async getPage({filter, order, page, limit}: { filter?: string; order?: OrderOption; page: number; limit: number }): Promise { - const models = await this.Model.findAll({ + async getAll({filter, order}: { filter?: string; order?: OrderOption } = {}): Promise { + return this.#fetchAll({ filter, - order: this.#orderToString(order), - limit, - page + order + }); + } + + async getPage({filter, order, page, limit}: { filter?: string; order?: OrderOption; page: number; limit: number }): Promise { + return this.#fetchAll({ + filter, + order, + page, + limit }); - return (await Promise.all(models.map(model => this.modelToEntity(model)))).filter(entity => !!entity) as T[]; } async getCount({filter}: { filter?: string } = {}): Promise { diff --git a/ghost/bookshelf-repository/test/BookshelfRepository.test.ts b/ghost/bookshelf-repository/test/BookshelfRepository.test.ts index cb5f1f51ce..231e0b6980 100644 --- a/ghost/bookshelf-repository/test/BookshelfRepository.test.ts +++ b/ghost/bookshelf-repository/test/BookshelfRepository.test.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import {BookshelfRepository, ModelClass, ModelInstance} from '../src/index'; +import {Knex} from 'knex'; type SimpleEntity = { id: string; @@ -37,6 +38,10 @@ class SimpleBookshelfRepository extends BookshelfRepository { items: ModelInstance[] = []; + orderRaw?: string; + limit?: number; + offset?: number; + constructor() { this.items = []; } @@ -53,9 +58,10 @@ class Model implements ModelClass { } return Promise.resolve(item ?? null); } - findAll(options: {filter?: string | undefined; order?: string | undefined; page?: number; limit?: number | 'all'}): Promise[]> { + + fetchAll(): Promise[]> { const sorted = this.items.slice().sort((a, b) => { - for (const order of options.order?.split(',') ?? []) { + for (const order of this.orderRaw?.split(',') ?? []) { const [field, direction] = order.split(' '); const aValue = a.get(field as string) as number; @@ -68,7 +74,7 @@ class Model implements ModelClass { } return 0; }); - return Promise.resolve(sorted); + return Promise.resolve(sorted.slice(this.offset ?? 0, (this.offset ?? 0) + (this.limit ?? sorted.length))); } add(data: object): Promise> { @@ -100,6 +106,24 @@ class Model implements ModelClass { count() { return Promise.resolve(this.items.length); } + + // eslint-disable-next-line no-unused-vars + query(f: (q: Knex.QueryBuilder) => void) { + return f({ + limit: (limit: number) => { + this.limit = limit; + return this; + }, + offset: (offset: number) => { + this.offset = offset; + return this; + }, + orderByRaw: (order: string) => { + this.orderRaw = order; + return this; + } + } as any as Knex.QueryBuilder); + } } describe('BookshelfRepository', function () { diff --git a/ghost/core/core/server/models/base/plugins/crud.js b/ghost/core/core/server/models/base/plugins/crud.js index a56a9d4423..614d5b8dfc 100644 --- a/ghost/core/core/server/models/base/plugins/crud.js +++ b/ghost/core/core/server/models/base/plugins/crud.js @@ -44,12 +44,6 @@ module.exports = function (Bookshelf) { }); } - if (options.page && options.limit) { - itemCollection - .query('limit', options.limit) - .query('offset', options.limit * (options.page - 1)); - } - const result = await itemCollection.fetchAll(options); if (options.withRelated) { _.each(result.models, function each(item) { diff --git a/ghost/core/core/server/models/base/plugins/sanitize.js b/ghost/core/core/server/models/base/plugins/sanitize.js index ded695acef..9344623820 100644 --- a/ghost/core/core/server/models/base/plugins/sanitize.js +++ b/ghost/core/core/server/models/base/plugins/sanitize.js @@ -45,7 +45,7 @@ module.exports = function (Bookshelf) { case 'findOne': return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']); case 'findAll': - return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer', 'page', 'limit']); + return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']); case 'findPage': return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']); default: diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap index 035463a5ba..68abcff152 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap @@ -221,10 +221,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 0", - "title": "Recommendation 0", + "reason": "Reason 14", + "title": "Recommendation 14", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation0.com/", + "url": "https://recommendation14.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -233,10 +233,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 1", - "title": "Recommendation 1", + "reason": "Reason 13", + "title": "Recommendation 13", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation1.com/", + "url": "https://recommendation13.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -245,10 +245,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 2", - "title": "Recommendation 2", + "reason": "Reason 12", + "title": "Recommendation 12", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation2.com/", + "url": "https://recommendation12.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -257,10 +257,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 3", - "title": "Recommendation 3", + "reason": "Reason 11", + "title": "Recommendation 11", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation3.com/", + "url": "https://recommendation11.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -269,10 +269,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 4", - "title": "Recommendation 4", + "reason": "Reason 10", + "title": "Recommendation 10", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation4.com/", + "url": "https://recommendation10.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -281,10 +281,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 5", - "title": "Recommendation 5", + "reason": "Reason 9", + "title": "Recommendation 9", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation5.com/", + "url": "https://recommendation9.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -293,10 +293,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 6", - "title": "Recommendation 6", + "reason": "Reason 8", + "title": "Recommendation 8", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation6.com/", + "url": "https://recommendation8.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -317,10 +317,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 8", - "title": "Recommendation 8", + "reason": "Reason 6", + "title": "Recommendation 6", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation8.com/", + "url": "https://recommendation6.com/", }, ], } @@ -330,7 +330,7 @@ exports[`Recommendations Admin API Can request pages 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2964", + "content-length": "2979", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -359,10 +359,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 9", - "title": "Recommendation 9", + "reason": "Reason 5", + "title": "Recommendation 5", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation9.com/", + "url": "https://recommendation5.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -371,10 +371,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 10", - "title": "Recommendation 10", + "reason": "Reason 4", + "title": "Recommendation 4", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation10.com/", + "url": "https://recommendation4.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -383,10 +383,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 11", - "title": "Recommendation 11", + "reason": "Reason 3", + "title": "Recommendation 3", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation11.com/", + "url": "https://recommendation3.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -395,10 +395,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 12", - "title": "Recommendation 12", + "reason": "Reason 2", + "title": "Recommendation 2", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation12.com/", + "url": "https://recommendation2.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -407,10 +407,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 13", - "title": "Recommendation 13", + "reason": "Reason 1", + "title": "Recommendation 1", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation13.com/", + "url": "https://recommendation1.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -419,10 +419,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 14", - "title": "Recommendation 14", + "reason": "Reason 0", + "title": "Recommendation 0", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation14.com/", + "url": "https://recommendation0.com/", }, ], } @@ -432,7 +432,7 @@ exports[`Recommendations Admin API Can request pages 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1790", + "content-length": "1775", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -507,7 +507,7 @@ exports[`Recommendations Admin API Uses default limit of 5 1: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1573", + "content-length": "1585", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/recommendations.test.js b/ghost/core/test/e2e-api/admin/recommendations.test.js index bfba21107f..bb6937ac0c 100644 --- a/ghost/core/test/e2e-api/admin/recommendations.test.js +++ b/ghost/core/test/e2e-api/admin/recommendations.test.js @@ -201,7 +201,8 @@ describe('Recommendations Admin API', function () { favicon: null, featuredImage: null, excerpt: null, - oneClickSubscribe: false + oneClickSubscribe: false, + createdAt: new Date(i * 5000) // Reliable ordering }); await recommendationsService.repository.save(recommendation); diff --git a/ghost/core/test/e2e-api/content/__snapshots__/recommendations.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/recommendations.test.js.snap index fbfc25087e..5be953cee2 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/recommendations.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/recommendations.test.js.snap @@ -20,10 +20,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 0", - "title": "Recommendation 0", + "reason": "Reason 6", + "title": "Recommendation 6", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation0.com/", + "url": "https://recommendation6.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -32,10 +32,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 1", - "title": "Recommendation 1", + "reason": "Reason 5", + "title": "Recommendation 5", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation1.com/", + "url": "https://recommendation5.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -44,10 +44,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 2", - "title": "Recommendation 2", + "reason": "Reason 4", + "title": "Recommendation 4", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation2.com/", + "url": "https://recommendation4.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -68,10 +68,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 4", - "title": "Recommendation 4", + "reason": "Reason 2", + "title": "Recommendation 2", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation4.com/", + "url": "https://recommendation2.com/", }, ], } @@ -110,10 +110,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 5", - "title": "Recommendation 5", + "reason": "Reason 1", + "title": "Recommendation 1", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation5.com/", + "url": "https://recommendation1.com/", }, Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -122,10 +122,10 @@ Object { "featured_image": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "one_click_subscribe": false, - "reason": "Reason 6", - "title": "Recommendation 6", + "reason": "Reason 0", + "title": "Recommendation 0", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "url": "https://recommendation6.com/", + "url": "https://recommendation0.com/", }, ], } diff --git a/ghost/core/test/e2e-api/content/recommendations.test.js b/ghost/core/test/e2e-api/content/recommendations.test.js index db53df2f7e..e109072920 100644 --- a/ghost/core/test/e2e-api/content/recommendations.test.js +++ b/ghost/core/test/e2e-api/content/recommendations.test.js @@ -26,7 +26,8 @@ describe('Recommendations Content API', function () { favicon: null, featuredImage: null, excerpt: null, - oneClickSubscribe: false + oneClickSubscribe: false, + createdAt: new Date(i * 5000) // Reliable ordering }); await recommendationsService.repository.save(recommendation);