Implemented "virtual" Collection for "latest"

refs https://github.com/TryGhost/Arch/issues/95

Rather than storing all of the relations between the latest collection and
posts, we know that it contains all posts. This means we don't have to keep the
collections posts in sync. Instead we can fetch them from the posts table. This
saves a lot of work during recalculation.
This commit is contained in:
Fabien "egg" O'Carroll 2023-09-21 13:09:22 +07:00 committed by Fabien 'egg' O'Carroll
parent 45c1a82909
commit 9b2a94f931
5 changed files with 43 additions and 14 deletions

View File

@ -108,6 +108,7 @@ type QueryOptions = {
interface PostsRepository {
getAll(options: QueryOptions): Promise<CollectionPost[]>;
getAllIds(): Promise<string[]>;
}
export class CollectionsService {
@ -128,8 +129,8 @@ export class CollectionsService {
this.slugService = deps.slugService;
}
private toDTO(collection: Collection): CollectionDTO {
return {
private async toDTO(collection: Collection): Promise<CollectionDTO> {
const dto = {
id: collection.id,
title: collection.title,
slug: collection.slug,
@ -144,6 +145,14 @@ export class CollectionsService {
sort_order: index
}))
};
if (collection.slug === 'latest') {
const allPostIds = await this.postsRepository.getAllIds();
dto.posts = allPostIds.map((id, index) => ({
id,
sort_order: index
}));
}
return dto;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -580,11 +589,9 @@ export class CollectionsService {
async getAll(options?: QueryOptions): Promise<{data: CollectionDTO[], meta: any}> {
const collections = await this.collectionsRepository.getAll(options);
const collectionsDTOs: CollectionDTO[] = [];
for (const collection of collections) {
collectionsDTOs.push(this.toDTO(collection));
}
const collectionsDTOs: CollectionDTO[] = await Promise.all(
collections.map(collection => this.toDTO(collection))
);
return {
data: collectionsDTOs,
@ -602,14 +609,13 @@ export class CollectionsService {
}
async getCollectionsForPost(postId: string): Promise<CollectionDTO[]> {
const collections = await this.collectionsRepository.getAll({
filter: `posts:${postId}`
filter: `posts:${postId},slug:latest`
});
return collections.map(collection => this.toDTO(collection))
.sort((a, b) => {
// NOTE: sorting is here to keep DB engine ordering consistent
return a.slug.localeCompare(b.slug);
});
return Promise.all(collections.sort((a, b) => {
// NOTE: sorting is here to keep DB engine ordering consistent
return a.slug.localeCompare(b.slug);
}).map(collection => this.toDTO(collection)));
}
async destroy(id: string): Promise<Collection | null> {

View File

@ -166,6 +166,19 @@ describe('CollectionsService', function () {
});
});
describe('latest collection', function () {
it('Includes all posts when fetched directly', async function () {
await collectionsService.createCollection({
title: 'Latest',
slug: 'latest',
type: 'automatic',
filter: ''
});
const collection = await collectionsService.getBySlug('latest');
assert(collection?.posts.length === 4);
});
});
describe('edit', function () {
it('Can edit existing collection', async function () {
const savedCollection = await collectionsService.createCollection({

View File

@ -10,4 +10,9 @@ export class PostsRepositoryInMemory extends InMemoryRepository<string, Collecti
tags: entity.tags.map(tag => tag.slug)
};
}
async getAllIds() {
const posts = await this.getAll();
return posts.map(post => post.id);
}
}

View File

@ -4,6 +4,11 @@ class PostsRepository {
this.moment = moment;
}
async getAllIds() {
const rows = await this.models.Post.query().select('id').where('type', 'post');
return rows.map(row => row.id);
}
async getAll({filter, transaction}) {
const {data: models} = await this.models.Post.findPage({
filter: `(${filter})+type:post`,

View File

@ -572,7 +572,7 @@ describe('Collections API', function () {
}, this.skip.bind(this));
const collectionRelatedQueries = queries.filter(query => query.sql.includes('collection'));
assert.equal(collectionRelatedQueries.length, 12);
assert.equal(collectionRelatedQueries.length, 7);
}
await agent