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
This commit is contained in:
Simon Backx 2023-09-11 13:04:34 +02:00 committed by GitHub
parent acae53d9ed
commit f566729ed6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 88 deletions

View File

@ -1,3 +1,5 @@
import {Knex} from 'knex';
type Entity<T> = { type Entity<T> = {
id: T; id: T;
deleted: boolean; deleted: boolean;
@ -11,9 +13,12 @@ type Order<T> = {
export type ModelClass<T> = { export type ModelClass<T> = {
destroy: (data: {id: T}) => Promise<void>; destroy: (data: {id: T}) => Promise<void>;
findOne: (data: {id: T}, options?: {require?: boolean}) => Promise<ModelInstance<T> | null>; findOne: (data: {id: T}, options?: {require?: boolean}) => Promise<ModelInstance<T> | null>;
findAll: (options: {filter?: string; order?: string, page?: number, limit?: number | 'all'}) => Promise<ModelInstance<T>[]>;
add: (data: object) => Promise<ModelInstance<T>>; add: (data: object) => Promise<ModelInstance<T>>;
getFilteredCollection: (options: {filter?: string}) => {count(): Promise<number>}; getFilteredCollection: (options: {filter?: string}) => {
count(): Promise<number>,
query: (f: (q: Knex.QueryBuilder) => void) => void,
fetchAll: () => Promise<ModelInstance<T>[]>
};
} }
export type ModelInstance<T> = { export type ModelInstance<T> = {
@ -64,22 +69,44 @@ export abstract class BookshelfRepository<IDType, T extends Entity<IDType>> {
return model ? this.modelToEntity(model) : null; return model ? this.modelToEntity(model) : null;
} }
async getAll({filter, order}: { filter?: string; order?: OrderOption<T> } = {}): Promise<T[]> { async #fetchAll({filter, order, page, limit}: { filter?: string; order?: OrderOption<T>; page?: number; limit?: number }): Promise<T[]> {
const models = await this.Model.findAll({ const collection = this.Model.getFilteredCollection({filter});
filter, const orderString = this.#orderToString(order);
order: this.#orderToString(order)
}) as ModelInstance<IDType>[]; 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[]; 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<T>; page: number; limit: number }): Promise<T[]> { async getAll({filter, order}: { filter?: string; order?: OrderOption<T> } = {}): Promise<T[]> {
const models = await this.Model.findAll({ return this.#fetchAll({
filter, filter,
order: this.#orderToString(order), order
limit, });
page }
async getPage({filter, order, page, limit}: { filter?: string; order?: OrderOption<T>; page: number; limit: number }): Promise<T[]> {
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<number> { async getCount({filter}: { filter?: string } = {}): Promise<number> {

View File

@ -1,5 +1,6 @@
import assert from 'assert'; import assert from 'assert';
import {BookshelfRepository, ModelClass, ModelInstance} from '../src/index'; import {BookshelfRepository, ModelClass, ModelInstance} from '../src/index';
import {Knex} from 'knex';
type SimpleEntity = { type SimpleEntity = {
id: string; id: string;
@ -37,6 +38,10 @@ class SimpleBookshelfRepository extends BookshelfRepository<string, SimpleEntity
class Model implements ModelClass<string> { class Model implements ModelClass<string> {
items: ModelInstance<string>[] = []; items: ModelInstance<string>[] = [];
orderRaw?: string;
limit?: number;
offset?: number;
constructor() { constructor() {
this.items = []; this.items = [];
} }
@ -53,9 +58,10 @@ class Model implements ModelClass<string> {
} }
return Promise.resolve(item ?? null); return Promise.resolve(item ?? null);
} }
findAll(options: {filter?: string | undefined; order?: string | undefined; page?: number; limit?: number | 'all'}): Promise<ModelInstance<string>[]> {
fetchAll(): Promise<ModelInstance<string>[]> {
const sorted = this.items.slice().sort((a, b) => { 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 [field, direction] = order.split(' ');
const aValue = a.get(field as string) as number; const aValue = a.get(field as string) as number;
@ -68,7 +74,7 @@ class Model implements ModelClass<string> {
} }
return 0; 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<ModelInstance<string>> { add(data: object): Promise<ModelInstance<string>> {
@ -100,6 +106,24 @@ class Model implements ModelClass<string> {
count() { count() {
return Promise.resolve(this.items.length); 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 () { describe('BookshelfRepository', function () {

View File

@ -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); const result = await itemCollection.fetchAll(options);
if (options.withRelated) { if (options.withRelated) {
_.each(result.models, function each(item) { _.each(result.models, function each(item) {

View File

@ -45,7 +45,7 @@ module.exports = function (Bookshelf) {
case 'findOne': case 'findOne':
return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']); return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']);
case 'findAll': case 'findAll':
return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer', 'page', 'limit']); return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']);
case 'findPage': case 'findPage':
return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']); return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']);
default: default:

View File

@ -221,10 +221,10 @@ Object {
"featured_image": null, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 0", "reason": "Reason 14",
"title": "Recommendation 0", "title": "Recommendation 14",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 1", "reason": "Reason 13",
"title": "Recommendation 1", "title": "Recommendation 13",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 2", "reason": "Reason 12",
"title": "Recommendation 2", "title": "Recommendation 12",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 3", "reason": "Reason 11",
"title": "Recommendation 3", "title": "Recommendation 11",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 4", "reason": "Reason 10",
"title": "Recommendation 4", "title": "Recommendation 10",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 5", "reason": "Reason 9",
"title": "Recommendation 5", "title": "Recommendation 9",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 6", "reason": "Reason 8",
"title": "Recommendation 6", "title": "Recommendation 8",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 8", "reason": "Reason 6",
"title": "Recommendation 8", "title": "Recommendation 6",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -359,10 +359,10 @@ Object {
"featured_image": null, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 9", "reason": "Reason 5",
"title": "Recommendation 9", "title": "Recommendation 5",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 10", "reason": "Reason 4",
"title": "Recommendation 10", "title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 11", "reason": "Reason 3",
"title": "Recommendation 11", "title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 12", "reason": "Reason 2",
"title": "Recommendation 12", "title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 13", "reason": "Reason 1",
"title": "Recommendation 13", "title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 14", "reason": "Reason 0",
"title": "Recommendation 14", "title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -201,7 +201,8 @@ describe('Recommendations Admin API', function () {
favicon: null, favicon: null,
featuredImage: null, featuredImage: null,
excerpt: null, excerpt: null,
oneClickSubscribe: false oneClickSubscribe: false,
createdAt: new Date(i * 5000) // Reliable ordering
}); });
await recommendationsService.repository.save(recommendation); await recommendationsService.repository.save(recommendation);

View File

@ -20,10 +20,10 @@ Object {
"featured_image": null, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 0", "reason": "Reason 6",
"title": "Recommendation 0", "title": "Recommendation 6",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 1", "reason": "Reason 5",
"title": "Recommendation 1", "title": "Recommendation 5",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 2", "reason": "Reason 4",
"title": "Recommendation 2", "title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 4", "reason": "Reason 2",
"title": "Recommendation 4", "title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 5", "reason": "Reason 1",
"title": "Recommendation 5", "title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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, "featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false, "one_click_subscribe": false,
"reason": "Reason 6", "reason": "Reason 0",
"title": "Recommendation 6", "title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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/",
}, },
], ],
} }

View File

@ -26,7 +26,8 @@ describe('Recommendations Content API', function () {
favicon: null, favicon: null,
featuredImage: null, featuredImage: null,
excerpt: null, excerpt: null,
oneClickSubscribe: false oneClickSubscribe: false,
createdAt: new Date(i * 5000) // Reliable ordering
}); });
await recommendationsService.repository.save(recommendation); await recommendationsService.repository.save(recommendation);