Added use of a slug generator to offload calculation of slugs

When we end up wiring this to the database, this generator will also ensure
uniqueness by appending/incrementing a number on the end of the slug. Long term
it would be good to offload this to a shared slug service, this could also
ensure that slugs are unique globally or between multiple tables, if desired
This commit is contained in:
Fabien "egg" O'Carroll 2023-06-28 22:50:50 +01:00 committed by Fabien 'egg' O'Carroll
parent 0a3e36cd62
commit cf83d169db
6 changed files with 47 additions and 16 deletions

View File

@ -20,9 +20,14 @@ const messages = {
}
};
interface SlugService {
generate(desired: string): Promise<string>;
}
type CollectionsServiceDeps = {
collectionsRepository: CollectionRepository;
postsRepository: PostsRepository;
slugService: SlugService;
DomainEvents: {
subscribe: (event: any, handler: (e: any) => void) => void;
};
@ -96,12 +101,14 @@ export class CollectionsService {
subscribe: (event: any, handler: (e: any) => void) => void;
};
private uniqueChecker: RepositoryUniqueChecker;
private slugService: SlugService;
constructor(deps: CollectionsServiceDeps) {
this.collectionsRepository = deps.collectionsRepository;
this.postsRepository = deps.postsRepository;
this.DomainEvents = deps.DomainEvents;
this.uniqueChecker = new RepositoryUniqueChecker(this.collectionsRepository);
this.slugService = deps.slugService;
}
private toDTO(collection: Collection): CollectionDTO {
@ -165,9 +172,10 @@ export class CollectionsService {
}
async createCollection(data: CollectionInputDTO): Promise<CollectionDTO> {
const slug = await this.slugService.generate(data.slug || data.title);
const collection = await Collection.create({
title: data.title,
slug: data.slug,
slug: slug,
description: data.description,
type: data.type,
filter: data.filter,

View File

@ -41,7 +41,12 @@ describe('CollectionsService', function () {
collectionsService = new CollectionsService({
collectionsRepository,
postsRepository,
DomainEvents
DomainEvents,
slugService: {
async generate(input) {
return input.replace(/\s+/g, '-').toLowerCase();
}
}
});
});
@ -199,10 +204,12 @@ describe('CollectionsService', function () {
id: savedCollection.id,
title: 'Edited title',
description: 'Edited description',
feature_image: '/assets/images/edited.jpg'
feature_image: '/assets/images/edited.jpg',
slug: 'changed'
});
assert.equal(editedCollection?.title, 'Edited title', 'Collection title should be edited');
assert.equal(editedCollection?.slug, 'changed', 'Collection slug should be edited');
assert.equal(editedCollection?.description, 'Edited description', 'Collection description should be edited');
assert.equal(editedCollection?.feature_image, '/assets/images/edited.jpg', 'Collection feature_image should be edited');
assert.equal(editedCollection?.type, 'manual', 'Collection type should not be edited');

View File

@ -16,7 +16,12 @@ class CollectionsServiceWrapper {
const collectionsService = new CollectionsService({
collectionsRepository: collectionsRepositoryInMemory,
postsRepository: postsRepository,
DomainEvents: DomainEvents
DomainEvents: DomainEvents,
slugService: {
async generate(input) {
return input.toLowerCase().trim().replace(/^[^a-z0-9A-Z_]+/, '').replace(/[^a-z0-9A-Z_]+$/, '').replace(/[^a-z0-9A-Z_]+/g, '-');
}
}
});
this.api = collectionsService;

View File

@ -19,7 +19,8 @@ Object {
"sort_order": 1,
},
],
"title": "Test Collection",
"slug": "test-featured-collection",
"title": "Test Featured Collection",
"type": "automatic",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
},
@ -31,7 +32,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
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": "374",
"content-length": "417",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -81,6 +82,7 @@ Object {
"sort_order": 6,
},
],
"slug": "test-collection-with-published_at-filter",
"title": "Test Collection with published_at filter",
"type": "automatic",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -93,7 +95,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
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": "681",
"content-length": "731",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -192,6 +194,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection",
"title": "Test Collection",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -203,6 +206,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection-to-read",
"title": "Test Collection to Read",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -225,7 +229,7 @@ exports[`Collections API Browse Can browse Collections 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": "1759",
"content-length": "1817",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -833,6 +837,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection",
"title": "Test Collection",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -845,7 +850,7 @@ exports[`Collections API Can add a Collection 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": "263",
"content-length": "288",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -866,6 +871,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection-to-delete",
"title": "Test Collection to Delete",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -878,7 +884,7 @@ exports[`Collections API Can delete a Collection 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": "248",
"content-length": "283",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -944,6 +950,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection-to-read",
"title": "Test Collection to Read",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -956,7 +963,7 @@ exports[`Collections API Can read a Collection 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": "246",
"content-length": "279",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -977,6 +984,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection-to-read",
"title": "Test Collection to Read",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -989,7 +997,7 @@ exports[`Collections API Can read a Collection 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": "246",
"content-length": "279",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1039,6 +1047,7 @@ Object {
"filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"slug": "test-collection-to-edit",
"title": "Test Collection Edited",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -1051,7 +1060,7 @@ exports[`Collections API Edit Can edit a Collection 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": "245",
"content-length": "278",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -1326,6 +1326,7 @@ Object {
"sort_order": 0,
},
],
"slug": "collection-to-remove",
"title": "Collection to remove.",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.\\\\d\\{3\\}Z/,
@ -1420,7 +1421,7 @@ exports[`Posts API Update Can add and remove collections 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": "3992",
"content-length": "4022",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1451,6 +1452,7 @@ Object {
"sort_order": 0,
},
],
"slug": "collection-to-add",
"title": "Collection to add.",
"type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.\\\\d\\{3\\}Z/,
@ -1545,7 +1547,7 @@ exports[`Posts API Update Can add and remove collections 6: [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": "3989",
"content-length": "4016",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -307,7 +307,7 @@ describe('Collections API', function () {
describe('Automatic Collection Filtering', function () {
it('Creates an automatic Collection with a featured filter', async function () {
const collection = {
title: 'Test Collection',
title: 'Test Featured Collection',
description: 'Test Collection Description',
type: 'automatic',
filter: 'featured:true'