Skip to content

Commit

Permalink
Announcement categories
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Phoen committed Apr 8, 2023
1 parent 9c08e09 commit 2f5aa27
Show file tree
Hide file tree
Showing 22 changed files with 688 additions and 83 deletions.
6 changes: 6 additions & 0 deletions .changeset/loud-kiwis-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@k-phoen/backstage-plugin-announcements-backend': minor
'@k-phoen/backstage-plugin-announcements': minor
---

Introduce announcement categories
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @ts-check

/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
await knex.schema.createTable('categories', table => {
table.comment('The table for announcement categories.');
table.string('slug').notNullable().primary().comment('Category slug');
table.string('title').notNullable().comment('Title of the category.');
});

await knex.schema.alterTable('announcements', table => {
table.string('category').comment('Announcement category');

table
.foreign('category', 'category_fk')
.references('slug')
.inTable('categories')
.onDelete('SET NULL');
});
};

/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.alterTable('announcements', table => {
table.dropForeign('category', 'category_fk');
table.dropColumn('category');
});

await knex.schema.dropTable('categories');
};
1 change: 1 addition & 0 deletions plugins/announcements-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"knex": "^2.0.0",
"luxon": "^3.1.0",
"node-fetch": "^2.6.7",
"slugify": "^1.6.6",
"uuid": "^9.0.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
Expand Down
6 changes: 6 additions & 0 deletions plugins/announcements-backend/src/service/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { DateTime } from 'luxon';

export type Announcement = {
id: string;
category?: Category;
publisher: string;
title: string;
excerpt: string;
body: string;
created_at: DateTime;
};

export type Category = {
slug: string;
title: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,35 @@ import { Announcement } from '../model';

const announcementsTable = 'announcements';

type AnnouncementUpsert = {
id: string;
category?: string;
publisher: string;
title: string;
excerpt: string;
body: string;
created_at: DateTime;
};

export type DbAnnouncement = {
id: string;
category?: string;
publisher: string;
title: string;
excerpt: string;
body: string;
created_at: string;
};

export type DbAnnouncementWithCategory = DbAnnouncement & {
category_slug?: string;
category_title?: string;
};

type AnnouncementsFilters = {
max?: number;
offset?: number;
category?: string;
};

type AnnouncementsList = {
Expand All @@ -38,9 +55,12 @@ const timestampToDateTime = (input: Date | string): DateTime => {
return result;
};

const announcementToDB = (announcement: Announcement): DbAnnouncement => {
const announcementUpsertToDB = (
announcement: AnnouncementUpsert,
): DbAnnouncement => {
return {
id: announcement.id,
category: announcement.category,
title: announcement.title,
excerpt: announcement.excerpt,
body: announcement.body,
Expand All @@ -49,9 +69,18 @@ const announcementToDB = (announcement: Announcement): DbAnnouncement => {
};
};

const DBToAnnouncement = (announcementDb: DbAnnouncement): Announcement => {
const DBToAnnouncementWithCategory = (
announcementDb: DbAnnouncementWithCategory,
): Announcement => {
return {
id: announcementDb.id,
category:
announcementDb.category && announcementDb.category_title
? {
slug: announcementDb.category,
title: announcementDb.category_title,
}
: undefined,
title: announcementDb.title,
excerpt: announcementDb.excerpt,
body: announcementDb.body,
Expand All @@ -66,14 +95,33 @@ export class AnnouncementsDatabase {
async announcements(
request: AnnouncementsFilters,
): Promise<AnnouncementsList> {
const countResult = await this.db<DbAnnouncement>(announcementsTable)
.count<Record<string, number>>('id', { as: 'total' })
.first();
const queryBuilder = this.db<DbAnnouncement>(announcementsTable).orderBy(
'created_at',
'desc',
);
const countQueryBuilder = this.db<DbAnnouncement>(announcementsTable).count<
Record<string, number>
>('id', { as: 'total' });

if (request.category) {
countQueryBuilder.where('category', request.category);
}

const countResult = await countQueryBuilder.first();

const queryBuilder = this.db<DbAnnouncementWithCategory>(announcementsTable)
.select(
'id',
'publisher',
'announcements.title',
'excerpt',
'body',
'category',
'created_at',
'categories.title as category_title',
)
.orderBy('created_at', 'desc')
.leftJoin('categories', 'announcements.category', 'categories.slug');

if (request.category) {
queryBuilder.where('category', request.category);
}
if (request.offset) {
queryBuilder.offset(request.offset);
}
Expand All @@ -83,34 +131,55 @@ export class AnnouncementsDatabase {

return {
count: countResult && countResult.total ? countResult.total : 0,
results: (await queryBuilder.select()).map(DBToAnnouncement),
results: (await queryBuilder.select()).map(DBToAnnouncementWithCategory),
};
}

async announcementByID(id: string): Promise<Announcement | undefined> {
const dbAnnouncement = await this.db<DbAnnouncement>(announcementsTable)
const dbAnnouncement = await this.db<DbAnnouncementWithCategory>(
announcementsTable,
)
.select(
'id',
'publisher',
'announcements.title',
'excerpt',
'body',
'category',
'created_at',
'categories.title as category_title',
)
.leftJoin('categories', 'announcements.category', 'categories.slug')
.where('id', id)
.first();
if (!dbAnnouncement) {
return undefined;
}

return DBToAnnouncement(dbAnnouncement);
return DBToAnnouncementWithCategory(dbAnnouncement);
}

async deleteAnnouncementByID(id: string): Promise<void> {
return this.db<DbAnnouncement>(announcementsTable).where('id', id).delete();
}

async insertAnnouncement(announcement: Announcement) {
async insertAnnouncement(
announcement: AnnouncementUpsert,
): Promise<Announcement> {
await this.db<DbAnnouncement>(announcementsTable).insert(
announcementToDB(announcement),
announcementUpsertToDB(announcement),
);

return (await this.announcementByID(announcement.id))!;
}

async updateAnnouncement(announcement: Announcement) {
async updateAnnouncement(
announcement: AnnouncementUpsert,
): Promise<Announcement> {
await this.db<DbAnnouncement>(announcementsTable)
.where('id', announcement.id)
.update(announcementToDB(announcement));
.update(announcementUpsertToDB(announcement));

return (await this.announcementByID(announcement.id))!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Knex } from 'knex';
import { Category } from '../model';

const categoriesTable = 'categories';

export class CategoriesDatabase {
constructor(private readonly db: Knex) {}

async categories(): Promise<Category[]> {
const queryBuilder = this.db<Category>(categoriesTable).orderBy(
'title',
'asc',
);

return queryBuilder.select();
}

async delete(slug: string): Promise<void> {
return this.db<Category>(categoriesTable).where('slug', slug).delete();
}

async insert(category: Category): Promise<void> {
await this.db<Category>(categoriesTable).insert(category);
}

async update(category: Category): Promise<void> {
await this.db<Category>(categoriesTable)
.where('slug', category.slug)
.update(category);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
resolvePackagePath,
} from '@backstage/backend-common';
import { AnnouncementsDatabase } from './AnnouncementsDatabase';
import { CategoriesDatabase } from './CategoriesDatabase';

const migrationsDir = resolvePackagePath(
'@k-phoen/backstage-plugin-announcements-backend',
Expand All @@ -16,6 +17,7 @@ const migrationsDir = resolvePackagePath(
*/
export type PersistenceContext = {
announcementsStore: AnnouncementsDatabase;
categoriesStore: CategoriesDatabase;
};

/**
Expand All @@ -36,5 +38,6 @@ export const initializePersistenceContext = async (

return {
announcementsStore: new AnnouncementsDatabase(client),
categoriesStore: new CategoriesDatabase(client),
};
};
6 changes: 4 additions & 2 deletions plugins/announcements-backend/src/service/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AnnouncementsDatabase } from './persistence/AnnouncementsDatabase';
import { PersistenceContext } from './persistence/persistenceContext';
import { createRouter } from './router';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { CategoriesDatabase } from './persistence/CategoriesDatabase';

describe('createRouter', () => {
let app: express.Express;
Expand All @@ -26,6 +27,7 @@ describe('createRouter', () => {
insertAnnouncement: insertAnnouncementMock,
updateAnnouncement: updateAnnouncementMock,
} as unknown as AnnouncementsDatabase,
categoriesStore: {} as unknown as CategoriesDatabase,
};

const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
Expand Down Expand Up @@ -59,7 +61,7 @@ describe('createRouter', () => {
jest.resetAllMocks();
});

describe('GET /', () => {
describe('GET /announcements', () => {
it('returns ok', async () => {
announcementsMock.mockReturnValueOnce([
{
Expand All @@ -72,7 +74,7 @@ describe('createRouter', () => {
},
] as Announcement[]);

const response = await request(app).get('/');
const response = await request(app).get('/announcements');

expect(response.status).toEqual(200);
expect(announcementsMock).toHaveBeenCalledWith({});
Expand Down
Loading

0 comments on commit 2f5aa27

Please sign in to comment.